diff --git a/packages/client/src/i18n/index.ts b/packages/client/src/i18n/index.ts index 78c4e6f..bb359eb 100644 --- a/packages/client/src/i18n/index.ts +++ b/packages/client/src/i18n/index.ts @@ -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, }) diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index a6f6fb4..a011e98 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -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', }, } diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index 6f47da0..72702ba 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -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 diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index 9721c81..ab255d0 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -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', }, } diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 7b300a2..56dcc0e 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -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', }, } diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 5acd0d2..6965b91 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -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: 'ファイルをダウンロード', }, } diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index 440e8b8..d3d2141 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -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: '파일 다운로드', }, } diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index a83424b..faa3905 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -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', }, } diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index d1ea8bc..9dcadc5 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -612,6 +612,7 @@ export default { unsupportedBackend: '当前 terminal backend 暂不支持文件下载', invalidPath: '无效的文件路径', download: '下载', + downloadFile: '下载文件', }, // 更新日志 diff --git a/packages/client/src/i18n/messages.ts b/packages/client/src/i18n/messages.ts new file mode 100644 index 0000000..3ef7317 --- /dev/null +++ b/packages/client/src/i18n/messages.ts @@ -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 + +export const rawMessages = { + en, + zh, + ja, + ko, + fr, + es, + de, + pt, +} satisfies Record + +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 diff --git a/tests/client/i18n-coverage.test.ts b/tests/client/i18n-coverage.test.ts new file mode 100644 index 0000000..0cd586a --- /dev/null +++ b/tests/client/i18n-coverage.test.ts @@ -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() + 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, 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)[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') + }) +})