修复侧边栏 i18n 缺失 key 警告 (#170)
* Fix i18n missing-key warnings * Add locale translations for i18n warning keys
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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: 'ファイルをダウンロード',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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: '파일 다운로드',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -612,6 +612,7 @@ export default {
|
||||
unsupportedBackend: '当前 terminal backend 暂不支持文件下载',
|
||||
invalidPath: '无效的文件路径',
|
||||
download: '下载',
|
||||
downloadFile: '下载文件',
|
||||
},
|
||||
|
||||
// 更新日志
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user