Files
kilocode/packages/kilo-vscode/tests/unit/agent-manager-i18n-split.test.ts
T
Babak S d6380b246b feat(vscode): add Persian (Farsi) UI language (#12424)
* feat(vscode): add Persian (Farsi) UI language

Add Farsi (fa) as a first-class UI locale for the VS Code extension, with
automatic right-to-left layout (the extension already supports RTL via Arabic).

- Register fa in language-utils (Locale union, LOCALES, RTL_LOCALES),
  language.tsx (label + merged dict layers), kiloclaw, the extension-host and
  cli-backend translation bundles, and both package.json language enums.
- Add complete fa.ts dictionaries for all kilo-vscode i18n namespaces
  (sidebar, agent manager, kiloclaw, extension host, autocomplete, cli-backend).
- Extend i18n tests to enforce fa key completeness and placeholder alignment.

Persian language contributed by Babak Safabahar.

* fix(vscode): translate remaining Persian UI strings

* fix(vscode): make question dock and suggestions RTL-aware

The interactive question dock pinned option labels/descriptions to the left
via a hardcoded text-align: left, so they rendered left-aligned even under an
RTL locale (Persian/Arabic). Use logical text-align: start and logical padding
on the header, and add dir=auto to question text, option label/description,
collapsed preview, review rows and the suggestion text so mixed-script content
renders with correct bidi.

Contributed by Babak Safabahar.

* fix(i18n): sync Persian translations with latest en.ts

Resync fa.ts after rebasing on main:
- Translate the 27 newly added English keys into Persian.
- Remove the 23 obsolete memory.* keys that no longer exist in en.ts.
- Translate error.chain.configDirectoryTypo and other single-quoted values
  that were previously left in English.

All {{placeholder}} tokens are preserved; brand/technical terms (MCP, VS Code,
Vercel, Kilo Code, TerminalBench) intentionally stay untranslated. The i18n
key-completeness and placeholder-alignment tests pass.

Contributed by Babak Safabahar.

* fix(i18n): sync Persian translations with latest main

Translate the 45 newly added keys after rebasing on upstream main, including
the four session.prompts.* keys requested by the reviewer:
  - session.prompts.navLabel  -> ناوبر پرامپت
  - session.prompts.tick      -> پرامپت {{index}} از {{total}}: {{prompt}}
  - session.prompts.noAnswer  -> هنوز پاسخی وجود ندارد
  - session.prompts.queued    -> در صف انتظار

All {{placeholder}} tokens preserved; brand terms stay untranslated.
i18n tests pass (32/32). Contributed by Babak Safabahar.

* fix(i18n): improve contextual accuracy of 8 Persian translations

Revise the 8 keys flagged by the reviewer as too literal:
  - migration.whatsNew.features.agentManager.title: was 'مدیر عامل'; now 'Agent Manager' (product name, not translated)
  - agentManager.dialog.createWorktree: was 'ایجاد درخت‌کاری'; now 'ایجاد Worktree' (git technical term)
  - agentManager.dialog.versionHint: 'worktree' kept in English
  - sidebar.session.configureWorktree.tooltip: 'worktree'/'Agent Manager' kept in English
  - sidebar.session.showChanges.tooltip.empty: 'working tree' kept in English
  - diffViewer.source.unstaged.tooltip: 'working tree'/'stage' kept in English
  - settings.language.description: phrasing improved
  - prompt.placeholder.default: already natural; minor polish

Also sync fa.ts with any additional new keys added since the last commit.
i18n tests pass (32/32). Contributed by Babak Safabahar.

* Update packages/kilo-vscode/webview-ui/src/i18n/fa.ts

Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com>

---------

Co-authored-by: Johnny Eric Amancio <johnnyeric@gmail.com>
Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com>
2026-07-29 15:21:22 +02:00

163 lines
5.8 KiB
TypeScript

import { describe, it, expect } from "bun:test"
import { dict as appEn } from "../../webview-ui/src/i18n/en"
import { dict as appZh } from "../../webview-ui/src/i18n/zh"
import { dict as appZht } from "../../webview-ui/src/i18n/zht"
import { dict as appKo } from "../../webview-ui/src/i18n/ko"
import { dict as appDe } from "../../webview-ui/src/i18n/de"
import { dict as appEs } from "../../webview-ui/src/i18n/es"
import { dict as appFr } from "../../webview-ui/src/i18n/fr"
import { dict as appDa } from "../../webview-ui/src/i18n/da"
import { dict as appJa } from "../../webview-ui/src/i18n/ja"
import { dict as appPl } from "../../webview-ui/src/i18n/pl"
import { dict as appRu } from "../../webview-ui/src/i18n/ru"
import { dict as appAr } from "../../webview-ui/src/i18n/ar"
import { dict as appNo } from "../../webview-ui/src/i18n/no"
import { dict as appBr } from "../../webview-ui/src/i18n/br"
import { dict as appTh } from "../../webview-ui/src/i18n/th"
import { dict as appBs } from "../../webview-ui/src/i18n/bs"
import { dict as appTr } from "../../webview-ui/src/i18n/tr"
import { dict as appNl } from "../../webview-ui/src/i18n/nl"
import { dict as appUk } from "../../webview-ui/src/i18n/uk"
import { dict as appIt } from "../../webview-ui/src/i18n/it"
import { dict as amEn } from "../../webview-ui/agent-manager/i18n/en"
import { dict as amZh } from "../../webview-ui/agent-manager/i18n/zh"
import { dict as amZht } from "../../webview-ui/agent-manager/i18n/zht"
import { dict as amKo } from "../../webview-ui/agent-manager/i18n/ko"
import { dict as amDe } from "../../webview-ui/agent-manager/i18n/de"
import { dict as amEs } from "../../webview-ui/agent-manager/i18n/es"
import { dict as amFr } from "../../webview-ui/agent-manager/i18n/fr"
import { dict as amDa } from "../../webview-ui/agent-manager/i18n/da"
import { dict as amJa } from "../../webview-ui/agent-manager/i18n/ja"
import { dict as amPl } from "../../webview-ui/agent-manager/i18n/pl"
import { dict as amRu } from "../../webview-ui/agent-manager/i18n/ru"
import { dict as amAr } from "../../webview-ui/agent-manager/i18n/ar"
import { dict as amNo } from "../../webview-ui/agent-manager/i18n/no"
import { dict as amBr } from "../../webview-ui/agent-manager/i18n/br"
import { dict as amTh } from "../../webview-ui/agent-manager/i18n/th"
import { dict as amBs } from "../../webview-ui/agent-manager/i18n/bs"
import { dict as amTr } from "../../webview-ui/agent-manager/i18n/tr"
import { dict as amNl } from "../../webview-ui/agent-manager/i18n/nl"
import { dict as amUk } from "../../webview-ui/agent-manager/i18n/uk"
import { dict as amIt } from "../../webview-ui/agent-manager/i18n/it"
import { dict as amFa } from "../../webview-ui/agent-manager/i18n/fa"
const PREFIX = "agentManager."
const locales = {
en: amEn,
zh: amZh,
zht: amZht,
ko: amKo,
de: amDe,
es: amEs,
fr: amFr,
da: amDa,
ja: amJa,
pl: amPl,
ru: amRu,
ar: amAr,
no: amNo,
br: amBr,
th: amTh,
bs: amBs,
tr: amTr,
nl: amNl,
uk: amUk,
it: amIt,
fa: amFa,
}
const appLocales = {
en: appEn,
zh: appZh,
zht: appZht,
ko: appKo,
de: appDe,
es: appEs,
fr: appFr,
da: appDa,
ja: appJa,
pl: appPl,
ru: appRu,
ar: appAr,
no: appNo,
br: appBr,
th: appTh,
bs: appBs,
tr: appTr,
nl: appNl,
uk: appUk,
it: appIt,
}
function placeholders(text: string): string[] {
return Array.from(text.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g))
.flatMap((match) => (match[1] ? [match[1]] : []))
.sort()
}
describe("Agent Manager i18n split", () => {
it("keeps agent manager keys out of general locale dictionaries", () => {
for (const [locale, dict] of Object.entries(appLocales)) {
const keys = Object.keys(dict)
expect(
keys.some((key) => key.startsWith(PREFIX)),
`locale ${locale} contains agent manager keys`,
).toBeFalse()
}
})
it("keeps every agent manager locale dictionary scoped to agentManager.* keys", () => {
for (const [locale, dict] of Object.entries(locales)) {
const keys = Object.keys(dict)
expect(keys.length, `locale ${locale} should have agent manager keys`).toBeGreaterThan(0)
const invalid = keys.filter((key) => !key.startsWith(PREFIX))
expect(invalid, `locale ${locale} has non-agent-manager keys`).toEqual([])
}
})
it("keeps every agent manager locale keyset aligned with english", () => {
const baseKeys = Object.keys(amEn)
for (const [locale, dict] of Object.entries(locales)) {
const keySet = new Set(Object.keys(dict))
const missing = baseKeys.filter((key) => !keySet.has(key))
const extra = Array.from(keySet).filter((key) => !(key in amEn))
expect(missing, `locale ${locale} is missing agent manager keys`).toEqual([])
expect(extra, `locale ${locale} has unexpected agent manager keys`).toEqual([])
}
})
it("keeps interpolation placeholders aligned with english", () => {
for (const [locale, dict] of Object.entries(locales)) {
if (locale === "en") continue
for (const [key, value] of Object.entries(amEn)) {
const localized = (dict as Record<string, string>)[key]
expect(localized, `missing key ${key} in locale ${locale}`).toBeDefined()
if (!localized) continue
const baseVars = placeholders(value)
const localeVars = placeholders(localized)
expect(localeVars, `placeholder mismatch for ${key} in locale ${locale}`).toEqual(baseVars)
}
}
})
it("contains required core keys in every locale", () => {
const required = [
"agentManager.local",
"agentManager.session.new",
"agentManager.apply.error",
"agentManager.import.failed",
]
for (const [locale, dict] of Object.entries(locales)) {
for (const key of required) {
expect((dict as Record<string, string>)[key], `missing key ${key} in locale ${locale}`).toBeDefined()
}
}
})
})