diff --git a/config/scripts/verify-localization-catalog.mjs b/config/scripts/verify-localization-catalog.mjs index a155e8741d3a..8d90a0b83b5d 100644 --- a/config/scripts/verify-localization-catalog.mjs +++ b/config/scripts/verify-localization-catalog.mjs @@ -151,6 +151,30 @@ function formatMissingKeys(label, keys) { return keys.map((key) => `${label}: ${key}`).join('\n') } +// Language packs require nested names even though i18next also accepts flat keys. +function collectDottedCatalogKeys(value, prefix = '') { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return [] + } + return Object.entries(value).flatMap(([key, child]) => { + const keyPath = prefix ? `${prefix}.${key}` : key + return [...(key.includes('.') ? [keyPath] : []), ...collectDottedCatalogKeys(child, keyPath)] + }) +} + +function reportDottedCatalogKeys(fileName, catalog) { + const dottedKeys = collectDottedCatalogKeys(catalog) + if (dottedKeys.length === 0) { + return 0 + } + console.error(`${fileName} has keys with a dot in the name. Write them as nested objects.`) + console.error(formatMissingKeys('dotted', dottedKeys.slice(0, 20))) + if (dottedKeys.length > 20) { + console.error(`...and ${dottedKeys.length - 20} more dotted keys`) + } + return 1 +} + function normalizeInterpolationVariables(value) { return collectInterpolationVariables(value) .map((variable) => variable.slice(2, -2)) @@ -480,6 +504,9 @@ export async function main( } return 0 } + if (reportDottedCatalogKeys('en.json', catalog) !== 0) { + return 1 + } let catalogKeys = new Set(flattenCatalogKeys(catalog)) const references = sharedReferences ?? (await collectLocalizationReferences(root)) @@ -539,6 +566,9 @@ export async function main( const localeName = fileName.replace(/\.json$/, '') const localeCatalogPath = path.join(localesDir, fileName) const localeCatalog = JSON.parse(await fs.readFile(localeCatalogPath, 'utf8')) + if (reportDottedCatalogKeys(fileName, localeCatalog) !== 0) { + return 1 + } const exitCode = verifyLocaleCatalog(catalog, localeName, localeCatalog) if (exitCode !== 0) { console.error('') diff --git a/config/scripts/verify-localization-catalog.test.mjs b/config/scripts/verify-localization-catalog.test.mjs index 4623f345b795..04ce129e17fc 100644 --- a/config/scripts/verify-localization-catalog.test.mjs +++ b/config/scripts/verify-localization-catalog.test.mjs @@ -65,6 +65,30 @@ describe('verify-localization-catalog', () => { }) }) + it.each([ + ['en', false], + ['en', true], + ['es', false] + ])('rejects dotted %s keys without rewriting them (fix: %s)', async (locale, fix) => { + const nested = { auto: { example: { greeting: 'Hello.world' } } } + const { root, localesDir } = makeProject({ + sourceText: 'export {}\n', + enCatalog: locale === 'en' ? { 'auto.example.greeting': 'Hello.world' } : nested, + esCatalog: locale === 'es' ? { auto: { 'example.greeting': 'Hola.world' } } : {} + }) + const catalogPath = path.join(localesDir, `${locale}.json`) + const before = readFileSync(catalogPath, 'utf8') + const report = vi.spyOn(console, 'error').mockImplementation(() => undefined) + try { + await expect(verifyLocalizationCatalog(root, { fix })).resolves.toBe(1) + expect(report).toHaveBeenCalledWith(expect.stringContaining(`${locale}.json`)) + expect(report).toHaveBeenCalledWith('dotted: auto.example.greeting') + expect(readFileSync(catalogPath, 'utf8')).toBe(before) + } finally { + report.mockRestore() + } + }) + it('never overwrites mismatched translations or removes target-only entries', async () => { const { root, localesDir } = makeProject({ sourceText: diff --git a/src/renderer/src/i18n/catalog-nested-settings.test.ts b/src/renderer/src/i18n/catalog-nested-settings.test.ts new file mode 100644 index 000000000000..1d070a6a4ef3 --- /dev/null +++ b/src/renderer/src/i18n/catalog-nested-settings.test.ts @@ -0,0 +1,56 @@ +import i18next from 'i18next' +import { expect, it } from 'vitest' +import { parsePluginLanguagePackArtifact } from '../../../shared/plugins/plugin-language-pack-artifact' +import en from './locales/en.json' +import es from './locales/es.json' +import fr from './locales/fr.json' +import ja from './locales/ja.json' +import ko from './locales/ko.json' +import zh from './locales/zh.json' + +const AGENT_SETTINGS_KEYS = [ + 'environmentName', + 'environmentValue', + 'savedEnvironment', + 'newEnvironment', + 'removeEnvironment', + 'loadingHost', + 'updateHost', + 'readFailed', + 'saveFailed', + 'hostDefaults', + 'save' +] as const + +it.each([ + ['en', en], + ['es', es], + ['fr', fr], + ['ja', ja], + ['ko', ko], + ['zh', zh] +] as const)( + 'keeps %s agent settings usable as nested language-pack translations', + async (locale, catalog) => { + // Bundled security copy is intentionally unavailable to plugin language packs. + expect( + parsePluginLanguagePackArtifact(JSON.stringify({ settings: catalog.settings })) + ).toMatchObject({ ok: true }) + + const instance = i18next.createInstance() + await instance.init({ + lng: locale, + fallbackLng: false, + resources: { [locale]: { translation: catalog } }, + ignoreJSONStructure: false + }) + for (const key of AGENT_SETTINGS_KEYS) { + expect(instance.t(`settings.agents.${key}`, { name: 'PATH' })).toBe( + catalog.settings.agents[key].replace('{{name}}', 'PATH') + ) + } + expect(instance.t('settings.appearance.chat.title')).toBe( + catalog.settings.appearance.chat.title + ) + } +) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 88c89b90fb34..dd2a3b4eaaf6 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -319,7 +319,18 @@ "codexSharedServerWarning": { "title": "Warn when a Codex tab shares a server", "description": "Shows a notice on a Codex you started yourself when it shares a server with other tabs, because its agent status may be wrong." - } + }, + "environmentName": "Environment variable name", + "environmentValue": "Value for {{name}}", + "savedEnvironment": "Saved value · enter to replace", + "newEnvironment": "Value", + "removeEnvironment": "Remove", + "loadingHost": "Loading agent settings…", + "updateHost": "Update this server to edit its agent launch settings.", + "readFailed": "Couldn’t read agent settings. Check the host connection and try again.", + "saveFailed": "Couldn’t save agent settings. Check the host connection and try again.", + "hostDefaults": "Defaults for new agents on the selected host.", + "save": "Save" }, "followSymlinkedDirectoriesKeywords": { "symlink": "symlink", @@ -19791,7 +19802,6 @@ "projectField": { "updateFailed": "Failed to update project field" }, - "settings.appearance.chat.title": "Chat", "mediaPreview": { "unavailable": "Unable to play this media file. The file may be unavailable or use an unsupported codec.", "openFailed": "Could not open this file in the default app.", @@ -19872,17 +19882,6 @@ "updateRuntimeSafely": "Update the remote runtime to safely change workspace links" } }, - "settings.agents.environmentName": "Environment variable name", - "settings.agents.environmentValue": "Value for {{name}}", - "settings.agents.savedEnvironment": "Saved value · enter to replace", - "settings.agents.newEnvironment": "Value", - "settings.agents.removeEnvironment": "Remove", - "settings.agents.loadingHost": "Loading agent settings…", - "settings.agents.updateHost": "Update this server to edit its agent launch settings.", - "settings.agents.readFailed": "Couldn’t read agent settings. Check the host connection and try again.", - "settings.agents.saveFailed": "Couldn’t save agent settings. Check the host connection and try again.", - "settings.agents.hostDefaults": "Defaults for new agents on the selected host.", - "settings.agents.save": "Save", "tabHoverCard": { "browser": "Browser", "editor": "Editor", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 546b6ea2152e..cf4c9694619c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -151,7 +151,18 @@ "codexSharedServerWarning": { "title": "Avisar cuando una pestaña de Codex comparte un servidor", "description": "Muestra un aviso en un Codex que usted mismo inició cuando comparte un servidor con otras pestañas, porque su estado de agente puede ser incorrecto." - } + }, + "environmentName": "Nombre de la variable de entorno", + "environmentValue": "Valor de {{name}}", + "savedEnvironment": "Valor guardado · escribe para reemplazarlo", + "newEnvironment": "Valor", + "removeEnvironment": "Eliminar", + "loadingHost": "Cargando la configuración de agentes…", + "updateHost": "Actualiza este servidor para editar su configuración de inicio de agentes.", + "readFailed": "No se pudo leer la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", + "saveFailed": "No se pudo guardar la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", + "hostDefaults": "Valores predeterminados para nuevos agentes en el host seleccionado.", + "save": "Guardar" }, "followSymlinkedDirectoriesKeywords": { "symlink": "enlace simbólico", @@ -19536,16 +19547,5 @@ }, "projectField": { "updateFailed": "No se pudo actualizar el campo del proyecto" - }, - "settings.agents.environmentName": "Nombre de la variable de entorno", - "settings.agents.environmentValue": "Valor de {{name}}", - "settings.agents.savedEnvironment": "Valor guardado · escribe para reemplazarlo", - "settings.agents.newEnvironment": "Valor", - "settings.agents.removeEnvironment": "Eliminar", - "settings.agents.loadingHost": "Cargando la configuración de agentes…", - "settings.agents.updateHost": "Actualiza este servidor para editar su configuración de inicio de agentes.", - "settings.agents.readFailed": "No se pudo leer la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", - "settings.agents.saveFailed": "No se pudo guardar la configuración de agentes. Comprueba la conexión con el host e inténtalo de nuevo.", - "settings.agents.hostDefaults": "Valores predeterminados para nuevos agentes en el host seleccionado.", - "settings.agents.save": "Guardar" + } } diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 9172662a3d52..4462703bfb44 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -252,7 +252,18 @@ "codexSharedServerWarning": { "title": "Avertir lorsqu'un onglet Codex partage un serveur", "description": "Affiche un avis sur un Codex que vous avez démarré vous-même lorsqu'il partage un serveur avec d'autres onglets, car son statut d'agent peut être erroné." - } + }, + "environmentName": "Nom de la variable d’environnement", + "environmentValue": "Valeur de {{name}}", + "savedEnvironment": "Valeur enregistrée · saisir pour remplacer", + "newEnvironment": "Valeur", + "removeEnvironment": "Supprimer", + "loadingHost": "Chargement des paramètres des agents…", + "updateHost": "Mettez ce serveur à jour pour modifier ses paramètres de lancement des agents.", + "readFailed": "Impossible de lire les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", + "saveFailed": "Impossible d’enregistrer les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", + "hostDefaults": "Paramètres par défaut des nouveaux agents sur l’hôte sélectionné.", + "save": "Enregistrer" }, "followSymlinkedDirectoriesKeywords": { "symlink": "lien symbolique", @@ -19537,16 +19548,5 @@ }, "projectField": { "updateFailed": "Échec de la mise à jour du champ du projet" - }, - "settings.agents.environmentName": "Nom de la variable d’environnement", - "settings.agents.environmentValue": "Valeur de {{name}}", - "settings.agents.savedEnvironment": "Valeur enregistrée · saisir pour remplacer", - "settings.agents.newEnvironment": "Valeur", - "settings.agents.removeEnvironment": "Supprimer", - "settings.agents.loadingHost": "Chargement des paramètres des agents…", - "settings.agents.updateHost": "Mettez ce serveur à jour pour modifier ses paramètres de lancement des agents.", - "settings.agents.readFailed": "Impossible de lire les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", - "settings.agents.saveFailed": "Impossible d’enregistrer les paramètres des agents. Vérifiez la connexion à l’hôte et réessayez.", - "settings.agents.hostDefaults": "Paramètres par défaut des nouveaux agents sur l’hôte sélectionné.", - "settings.agents.save": "Enregistrer" + } } diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 8efe71d8eb08..a0647f056389 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -151,7 +151,18 @@ "codexSharedServerWarning": { "title": "Codex タブがサーバーを共有する場合に警告する", "description": "自分で開始した Codex が他のタブとサーバーを共有している場合、Agent のステータスが間違っている可能性があるため、その Codex に関する通知が表示されます。" - } + }, + "environmentName": "環境変数名", + "environmentValue": "{{name}} の値", + "savedEnvironment": "保存済みの値 · 置き換えるには入力", + "newEnvironment": "値", + "removeEnvironment": "削除", + "loadingHost": "Agent設定を読み込み中…", + "updateHost": "Agentの起動設定を編集するには、このサーバーを更新してください。", + "readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。", + "saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。", + "hostDefaults": "選択したホストで起動する新しいAgentの既定値。", + "save": "保存" }, "followSymlinkedDirectoriesKeywords": { "symlink": "シンボリックリンク", @@ -19536,16 +19547,5 @@ }, "projectField": { "updateFailed": "プロジェクトフィールドを更新できませんでした" - }, - "settings.agents.environmentName": "環境変数名", - "settings.agents.environmentValue": "{{name}} の値", - "settings.agents.savedEnvironment": "保存済みの値 · 置き換えるには入力", - "settings.agents.newEnvironment": "値", - "settings.agents.removeEnvironment": "削除", - "settings.agents.loadingHost": "Agent設定を読み込み中…", - "settings.agents.updateHost": "Agentの起動設定を編集するには、このサーバーを更新してください。", - "settings.agents.readFailed": "Agent設定を読み込めませんでした。ホストへの接続を確認して再試行してください。", - "settings.agents.saveFailed": "Agent設定を保存できませんでした。ホストへの接続を確認して再試行してください。", - "settings.agents.hostDefaults": "選択したホストで起動する新しいAgentの既定値。", - "settings.agents.save": "保存" + } } diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 649dd6709549..1b7a3692b4d6 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -151,7 +151,18 @@ "codexSharedServerWarning": { "title": "Codex 탭이 서버를 공유할 때 경고", "description": "에이전트 상태가 잘못되었을 수 있으므로 서버를 다른 탭과 공유할 때 직접 시작한 Codex에 대한 알림을 표시합니다." - } + }, + "environmentName": "환경 변수 이름", + "environmentValue": "{{name}} 값", + "savedEnvironment": "저장된 값 · 바꾸려면 입력", + "newEnvironment": "값", + "removeEnvironment": "삭제", + "loadingHost": "에이전트 설정 불러오는 중…", + "updateHost": "에이전트 실행 설정을 편집하려면 이 서버를 업데이트하세요.", + "readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", + "saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", + "hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.", + "save": "저장" }, "followSymlinkedDirectoriesKeywords": { "symlink": "심볼릭 링크", @@ -19566,16 +19577,5 @@ }, "projectField": { "updateFailed": "프로젝트 필드를 업데이트하지 못했습니다" - }, - "settings.agents.environmentName": "환경 변수 이름", - "settings.agents.environmentValue": "{{name}} 값", - "settings.agents.savedEnvironment": "저장된 값 · 바꾸려면 입력", - "settings.agents.newEnvironment": "값", - "settings.agents.removeEnvironment": "삭제", - "settings.agents.loadingHost": "에이전트 설정 불러오는 중…", - "settings.agents.updateHost": "에이전트 실행 설정을 편집하려면 이 서버를 업데이트하세요.", - "settings.agents.readFailed": "에이전트 설정을 읽을 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", - "settings.agents.saveFailed": "에이전트 설정을 저장할 수 없습니다. 호스트 연결을 확인하고 다시 시도하세요.", - "settings.agents.hostDefaults": "선택한 호스트의 새 에이전트 기본값입니다.", - "settings.agents.save": "저장" + } } diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index a7734105c618..bbce2deb1286 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -151,7 +151,18 @@ "codexSharedServerWarning": { "title": "当 Codex 选项卡共享服务器时发出警告", "description": "当您自己启动的 Codex 与其他选项卡共享服务器时,在您自己启动的 Codex 上显示一条通知,因为它的智能体状态可能是错误的。" - } + }, + "environmentName": "环境变量名称", + "environmentValue": "{{name}} 的值", + "savedEnvironment": "已保存的值 · 输入以替换", + "newEnvironment": "值", + "removeEnvironment": "删除", + "loadingHost": "正在加载代理设置…", + "updateHost": "请更新此服务器以编辑其代理启动设置。", + "readFailed": "无法读取代理设置。请检查主机连接并重试。", + "saveFailed": "无法保存代理设置。请检查主机连接并重试。", + "hostDefaults": "所选主机上新代理的默认设置。", + "save": "保存" }, "followSymlinkedDirectoriesKeywords": { "symlink": "符号链接", @@ -19550,16 +19561,5 @@ }, "projectField": { "updateFailed": "无法更新项目字段" - }, - "settings.agents.environmentName": "环境变量名称", - "settings.agents.environmentValue": "{{name}} 的值", - "settings.agents.savedEnvironment": "已保存的值 · 输入以替换", - "settings.agents.newEnvironment": "值", - "settings.agents.removeEnvironment": "删除", - "settings.agents.loadingHost": "正在加载代理设置…", - "settings.agents.updateHost": "请更新此服务器以编辑其代理启动设置。", - "settings.agents.readFailed": "无法读取代理设置。请检查主机连接并重试。", - "settings.agents.saveFailed": "无法保存代理设置。请检查主机连接并重试。", - "settings.agents.hostDefaults": "所选主机上新代理的默认设置。", - "settings.agents.save": "保存" + } }