修复侧边栏 i18n 缺失 key 警告 (#170)

* Fix i18n missing-key warnings

* Add locale translations for i18n warning keys
This commit is contained in:
Zhicheng Han
2026-04-24 02:31:42 +02:00
committed by GitHub
parent b4359ccddb
commit 03c18c210d
11 changed files with 139 additions and 9 deletions
+2 -9
View File
@@ -1,12 +1,5 @@
import { createI18n } from 'vue-i18n'
import en from './locales/en'
import zh from './locales/zh'
import ja from './locales/ja'
import ko from './locales/ko'
import fr from './locales/fr'
import es from './locales/es'
import de from './locales/de'
import pt from './locales/pt'
import { messages } from './messages'
const saved = localStorage.getItem('hermes_locale')
const detected = navigator.language.slice(0, 2)
@@ -28,5 +21,5 @@ export const i18n = createI18n({
legacy: false,
locale: resolveLocale(saved, detected),
fallbackLocale: 'en',
messages: { en, zh, ja, ko, fr, es, de, pt },
messages,
})
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten',
updateFailed: 'Aktualisierung fehlgeschlagen',
logout: 'Abmelden',
nodeVersionWarning: 'Node.js v{version} erkannt. Bitte aktualisieren Sie auf Version 23 oder neuer.',
changelog: 'Anderungsprotokoll',
noChangelog: 'Kein Anderungsprotokoll verfugbar',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: 'Aktuelles Terminal-Backend unterstutzt keine Datei-Downloads',
invalidPath: 'Ungultiger Dateipfad',
download: 'Herunterladen',
downloadFile: 'Datei herunterladen',
},
}
+2
View File
@@ -97,6 +97,7 @@ export default {
updateSuccess: 'Update complete, please restart the server',
updateFailed: 'Update failed',
logout: 'Sign Out',
nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.',
changelog: 'Changelog',
noChangelog: 'No changelog available',
},
@@ -609,6 +610,7 @@ export default {
unsupportedBackend: 'Current terminal backend does not support file download',
invalidPath: 'Invalid file path',
download: 'Download',
downloadFile: 'Download file',
},
// Changelog
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: 'Actualizacion completa, por favor reinicia el servidor',
updateFailed: 'Error al actualizar',
logout: 'Cerrar sesion',
nodeVersionWarning: 'Se detecto Node.js v{version}. Actualiza a la version 23 o posterior.',
changelog: 'Registro de cambios',
noChangelog: 'No hay registro de cambios',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: 'El backend del terminal actual no admite la descarga de archivos',
invalidPath: 'Ruta de archivo invalida',
download: 'Descargar',
downloadFile: 'Descargar archivo',
},
}
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur',
updateFailed: 'Echec de la mise a jour',
logout: 'Deconnexion',
nodeVersionWarning: 'Node.js v{version} detecte. Veuillez passer a la version 23 ou ulterieure.',
changelog: 'Journal des modifications',
noChangelog: 'Aucun journal disponible',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: 'Le backend de terminal actuel ne prend pas en charge le telechargement de fichiers',
invalidPath: 'Chemin de fichier invalide',
download: 'Telecharger',
downloadFile: 'Telecharger le fichier',
},
}
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: '更新が完了しました。サーバーを再起動してください',
updateFailed: '更新に失敗しました',
logout: 'ログアウト',
nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。',
changelog: '更新履歴',
noChangelog: '更新履歴はありません',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: '現在のターミナルバックエンドはファイルのダウンロードに対応していません',
invalidPath: '無効なファイルパス',
download: 'ダウンロード',
downloadFile: 'ファイルをダウンロード',
},
}
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
updateFailed: '업데이트 실패',
logout: '로그아웃',
nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.',
changelog: '변경 이력',
noChangelog: '변경 이력이 없습니다',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: '현재 터미널 백엔드는 파일 다운로드를 지원하지 않습니다',
invalidPath: '잘못된 파일 경로',
download: '다운로드',
downloadFile: '파일 다운로드',
},
}
+2
View File
@@ -85,6 +85,7 @@ export default {
updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor',
updateFailed: 'Falha na atualizacao',
logout: 'Sair',
nodeVersionWarning: 'Node.js v{version} detectado. Atualize para a versao 23 ou posterior.',
changelog: 'Registro de alteracoes',
noChangelog: 'Nenhum registro disponivel',
},
@@ -584,5 +585,6 @@ export default {
unsupportedBackend: 'O backend de terminal atual nao suporta download de arquivos',
invalidPath: 'Caminho de arquivo invalido',
download: 'Baixar',
downloadFile: 'Baixar arquivo',
},
}
+1
View File
@@ -612,6 +612,7 @@ export default {
unsupportedBackend: '当前 terminal backend 暂不支持文件下载',
invalidPath: '无效的文件路径',
download: '下载',
downloadFile: '下载文件',
},
// 更新日志
+50
View File
@@ -0,0 +1,50 @@
import de from './locales/de'
import en from './locales/en'
import es from './locales/es'
import fr from './locales/fr'
import ja from './locales/ja'
import ko from './locales/ko'
import pt from './locales/pt'
import zh from './locales/zh'
export type LocaleMessages = Record<string, unknown>
export const rawMessages = {
en,
zh,
ja,
ko,
fr,
es,
de,
pt,
} satisfies Record<string, LocaleMessages>
function isPlainObject(value: unknown): value is LocaleMessages {
return !!value && typeof value === 'object' && !Array.isArray(value)
}
export function mergeMessagesWithFallback(
fallback: LocaleMessages,
locale: LocaleMessages,
): LocaleMessages {
const merged: LocaleMessages = { ...fallback }
for (const [key, value] of Object.entries(locale)) {
const fallbackValue = fallback[key]
merged[key] = isPlainObject(fallbackValue) && isPlainObject(value)
? mergeMessagesWithFallback(fallbackValue, value)
: value
}
return merged
}
export const messages = Object.fromEntries(
Object.entries(rawMessages).map(([locale, localeMessages]) => [
locale,
locale === 'en'
? localeMessages
: mergeMessagesWithFallback(en, localeMessages),
]),
) as typeof rawMessages
+72
View File
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest'
import { readdirSync, readFileSync } from 'fs'
import { join, relative } from 'path'
import { changelog } from '@/data/changelog'
import { messages, rawMessages } from '@/i18n/messages'
const SOURCE_ROOT = join(process.cwd(), 'packages/client/src')
function walkFiles(dir: string, files: string[] = []): string[] {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name)
if (entry.isDirectory()) {
walkFiles(path, files)
} else if (/\.(ts|vue)$/.test(entry.name) && !path.includes('/i18n/locales/')) {
files.push(path)
}
}
return files
}
function collectLiteralTranslationKeys(): string[] {
const keys = new Set<string>()
const translationCall = /(?:\b|\$)t\(\s*['"]([^'"]+)['"]/g
for (const file of walkFiles(SOURCE_ROOT)) {
const source = readFileSync(file, 'utf8')
for (const match of source.matchAll(translationCall)) {
keys.add(match[1])
}
}
for (const entry of changelog) {
for (const change of entry.changes) {
keys.add(change)
}
}
return [...keys].sort()
}
function hasPath(messages: Record<string, unknown>, key: string): boolean {
let current: unknown = messages
for (const part of key.split('.')) {
if (!current || typeof current !== 'object' || !(part in current)) return false
current = (current as Record<string, unknown>)[part]
}
return typeof current !== 'undefined'
}
describe('i18n locale coverage', () => {
it('defines every statically referenced translation key in the English source locale', () => {
const missing = collectLiteralTranslationKeys().filter((key) => !hasPath(rawMessages.en, key))
expect(missing).toEqual([])
})
it('defines every statically referenced translation key in effective runtime messages', () => {
const requiredKeys = collectLiteralTranslationKeys()
const missing = Object.entries(messages).flatMap(([locale, localeMessages]) =>
requiredKeys
.filter((key) => !hasPath(localeMessages, key))
.map((key) => `${locale}: ${key}`),
)
expect(missing).toEqual([])
})
it('keeps the coverage scanner rooted in client source files', () => {
expect(relative(process.cwd(), SOURCE_ROOT)).toBe('packages/client/src')
})
})