Files
orca/config/scripts/apply-translation-delta.mjs
T
af14a5b50b fix(i18n): localize provider errors and shared/native UI labels (#34680)
* fix(i18n): localize provider failures and remaining shared UI copy

Translate Linear/Jira failures at the renderer boundary while preserving
English errors and adding optional stable codes for existing clients.
Localize host status, source-control metadata, discard confirmations,
native dialogs, and catalog gaps across all six shipped locales.

Inventory and reproduction paths contributed in #32207.

Co-authored-by: Seiji Kohara <9543980+seijikohara@users.noreply.github.com>
Co-authored-by: Evgenii <413011+smwbev@users.noreply.github.com>

* test(i18n): assert stable task provider failure codes

* test(i18n): exercise goal notice fallback with an older language pack

* fix(i18n): address provider error and live title review findings

Handle missing remote error text, type provider codes and keep codes intact
through connection tests and optimistic edits. Fix the remaining Linear
load fallback and redundant translation calls, apply Japanese wording
corrections, and refresh native titles on language changes.

Co-authored-by: Seiji Kohara <9543980+seijikohara@users.noreply.github.com>

* fix(i18n): finish provider and shared-copy localization for all locales

- Translate the branch's new keys into de, ru, tr and zh-Hant (added to main
  after this branch was cut), with Russian few/many forms.
- Use whole sentences for file deletion failures and the mixed batch notice
  instead of inserting a 'Recycle Bin'/'Trash' word, and count keys for the
  batch titles.
- Translate the workspace/activity options filter count (was a shared
  'N filter(s)' literal) at the renderer call sites.
- Use _one/_other families for MCP server counts, untracked-file discard copy,
  resume-chat counts, discard file counts and visible host counts.
- Localize PR/MR card status labels, the unsupported-agent hint, Linear title/
  description update failures, the Linear 'showing first N' notice and the
  Linear Connect button.

Co-authored-by: Evgenii <413011+smwbev@users.noreply.github.com>

---------

Co-authored-by: Seiji Kohara <9543980+seijikohara@users.noreply.github.com>
Co-authored-by: Evgenii <413011+smwbev@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 14:49:17 -07:00

100 lines
3.6 KiB
JavaScript

import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import { repairTranslatedValue } from './locale-translation-policy.mjs'
const LOCALES_DIR = path.join('src', 'renderer', 'src', 'i18n', 'locales')
const SUPPORTED_LOCALES = new Set(['de', 'es', 'fr', 'ja', 'ko', 'pt', 'ru', 'tr', 'zh', 'zh-Hant'])
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/g
function placeholders(value) {
return [...(value.match(PLACEHOLDER_RE) ?? [])].sort().join('|')
}
function getEntry(catalog, key) {
return key.split('.').reduce((cursor, part) => cursor?.[part], catalog)
}
function setEntry(catalog, key, value) {
const parts = key.split('.')
let cursor = catalog
for (const part of parts.slice(0, -1)) {
if (typeof cursor[part] !== 'object' || cursor[part] === null || Array.isArray(cursor[part])) {
cursor[part] = {}
}
cursor = cursor[part]
}
cursor[parts.at(-1)] = value
}
function getEn(catalog, key, locale) {
// Why: locales with more CLDR plural categories than English (ru few/many) translate from `_other`.
const plural = /^(.*)_(zero|one|two|few|many|other)$/.exec(key)
const value =
getEntry(catalog, key) ??
(plural && new Intl.PluralRules(locale).resolvedOptions().pluralCategories.includes(plural[2])
? getEntry(catalog, `${plural[1]}_other`)
: undefined)
if (typeof value !== 'string') {
throw new Error(`en.json has no string entry for ${key}`)
}
return value
}
async function main() {
const root = process.cwd()
const translationsPath = process.argv[2] ?? '/tmp/translations.json'
const translations = JSON.parse(await fs.readFile(translationsPath, 'utf8'))
const enCatalog = JSON.parse(await fs.readFile(path.join(root, LOCALES_DIR, 'en.json'), 'utf8'))
for (const [locale, entries] of Object.entries(translations)) {
// Why: the locale becomes a path segment, so an unchecked key could write outside LOCALES_DIR.
if (!SUPPORTED_LOCALES.has(locale)) {
throw new Error(`Unsupported locale: ${locale}`)
}
const localePath = path.join(root, LOCALES_DIR, `${locale}.json`)
const localeCatalog = JSON.parse(await fs.readFile(localePath, 'utf8'))
let added = 0
let skipped = 0
for (const [key, rawValue] of Object.entries(entries)) {
if (getEntry(localeCatalog, key) !== undefined) {
console.log(`[${locale}] already present, skip: ${key}`)
skipped += 1
continue
}
const enValue = getEn(enCatalog, key, locale)
if (placeholders(enValue) !== placeholders(rawValue)) {
throw new Error(
`[${locale}] placeholder mismatch for ${key}: en=${placeholders(enValue)} vs locale=${placeholders(rawValue)}`
)
}
const repaired = repairTranslatedValue({
key,
enValue,
localeValue: rawValue,
locale
})
if (placeholders(enValue) !== placeholders(repaired)) {
throw new Error(
`[${locale}] repair broke placeholders for ${key}: ${JSON.stringify(repaired)}`
)
}
if (repaired !== rawValue) {
console.log(
`[${locale}] policy repair adjusted ${key}: ${JSON.stringify(rawValue)} -> ${JSON.stringify(repaired)}`
)
}
setEntry(localeCatalog, key, repaired)
added += 1
}
// Why: write-then-rename so an interrupted run can't leave the catalog truncated.
const tmpPath = `${localePath}.${process.pid}.tmp`
await fs.writeFile(tmpPath, `${JSON.stringify(localeCatalog, null, 2)}\n`, 'utf8')
await fs.rename(tmpPath, localePath)
console.log(`[${locale}] added ${added}, skipped ${skipped}`)
}
}
await main()