mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* 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>
100 lines
3.6 KiB
JavaScript
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()
|