Skip to main content

Translating with kapi

kapi fills the KBF archive that neokapi-i18n extracts. It pseudo-translates the archive to check your layout, translates it with AI and your content memory, and checks the result against your terms, and it writes each result back into the same files.

i18n/KBF archiverecyclecontent memorytranslateAI providerqa · term-checkfindingscompile{locale}.json

Each kapi step reads and writes the same archive; neokapi-i18n compile turns it into runtime dicts.

Pseudo-translation for UI checks​

Pseudo-translation shows you at a glance which strings were extracted and which were missed.

kapi pseudo-translate i18n/

With no -o, the default for KBF inputs is in-place: the qps target is added to the same archive. Run again with --target-lang fr to add another locale; the writer is locale-additive and existing targets stay put.

Source Welcome to Acme becomes ▒ Ŵéḷçőḿé tő Âçmé ▒:

  • Accented characters make translated strings visually distinct. A string that stays plain English is an extraction gap, and it stands out in the UI.
  • Markers (▒) mark start and end, so truncation is obvious (Welcome to Ac… never wraps to ▒ Ŵéḷç…).
  • Expansion (adding 30–50% more characters) mimics German, French and Russian string growth so layout bugs surface before you ship.

Then neokapi-i18n compile it and load it as any other locale:

neokapi-i18n compile i18n/ --out public/translations

Your dev server then serves a qps locale. Add a language picker, and send pseudo-translated screenshots to design review.

Pseudo-translation in CI​

Add it to CI as a UI-layout smoke test:

.github/workflows/ui-checks.yml
- run: vp neokapi-i18n extract
- run: kapi pseudo-translate i18n/
- run: vp neokapi-i18n compile i18n/ --out public/translations
- run: npm run test:e2e # runs against ?locale=qps

AI translation​

kapi translate sends the KBF directory to an LLM for translation. It preserves placeholders, inline element tokens, and plural / select structure:

kapi translate i18n/ --target-lang fr
kapi translate i18n/ --target-lang de
kapi translate i18n/ --target-lang ja

Each call accumulates a target locale in place. To redirect output to a different file, pass -o target-dir/; the input stays untouched.

kapi supports the AI providers listed under Translation, local Ollama models among them. Select the provider (and optionally the model) with flags, or in a flow's step config:

kapi translate i18n/ --target-lang fr --provider anthropic

API keys are never written into the committed recipe. Supply one, in precedence order, with --api-key, a saved keychain credential (kapi credentials add, then --credential <name>), or the provider's standard environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, …). Local Ollama needs no key.

See Translation for the full provider and configuration surface.

Context carries through​

Every block in the KBF directory carries its element ("button", "p", …), its file and line, its component name, and any data-i18n-note annotation. The AI translator gets that context as part of the prompt, so a <button>Close</button> gets a different translation than an <a>Close</a> in a list-item's delete action.

Where the element alone is too little to disambiguate (two buttons both reading "Open", one a verb and one a state), add the note explicitly with data-i18n-note or t(text, context). That note is part of the key, so the two strings separate for the translator and stay separate.

Translate a subset​

For incremental translations (only the strings that changed), re-extract into the same archive and translate only the untranslated blocks. neokapi-i18n extract is locale-additive, so re-running it adds new source blocks without disturbing existing targets; kapi exec translate --skip-matched then skips any block that already has a target for the locale:

vp neokapi-i18n extract # refresh i18n/ with new/changed blocks
kapi exec translate i18n/ --target-lang fr --skip-matched

kapi sends only the blocks added since the last pass to the LLM, and leaves the translated blocks as they are.

Checks​

kapi exec qa runs placeholder, inline-code, whitespace, and length checks against a translated archive; kapi exec term-check enforces approved terminology:

kapi exec qa i18n/ --target-lang fr # placeholder, code, length, consistency
kapi exec term-check i18n/ --target-lang fr --termstore terms/fr.db # terminology

qa covers:

  • Placeholder and inline-code integrity: every {name} and inline-element token ({=m0}) in the source appears in the target.
  • Length bounds: flag targets that grow or shrink beyond configurable percentages of the source (useful for fixed-width UI containers).
  • Consistency: double spaces, doubled words, leading/trailing whitespace, target-identical-to-source, and more (each individually toggleable).

term-check flags targets that diverge from an approved term, for example a product name that must stay untranslated.

kapi exec reports what a check finds and exits 0 whatever it finds, so a run never fails a build on its own. The command that fails one is kapi check, which exits 3 while a finding fails; see Ship gates and CI.

Reuse past translations from content memory​

kapi has a built-in content memory. Import your past translations:

kapi memory import historical-translations.tmx -s en -t fr

Then pre-fill matches before the AI pass: kapi exec recycle writes exact and high-scoring fuzzy matches into the target, and kapi exec translate --skip-matched translates only what's left:

kapi exec recycle i18n/ --target-lang fr # fill targets from the project content memory
kapi exec translate i18n/ --target-lang fr --skip-matched

Pass --memory <name-or-path> to kapi exec recycle to draw on a specific content memory. See Content memory for the match and fill thresholds.

Terminology consistency​

For an app with a large product vocabulary, a terms store keeps each term translated the same way. Import the term list into a named store, then check translations with kapi exec term-check, which flags any target that diverges from an approved term:

kapi terms import product-terms.csv -s en -t fr --name product-terms
kapi exec term-check i18n/ --target-lang fr --termstore product-terms

To give the translation step the terms as well as checking them afterward, read the terms into the project with kapi store import (the project's own terms govern with nothing bound) or list rules under term_rules: in the step's config. translate reads the matching rules itself and sends them in the prompt's terminology section.

See Terminology.

Putting it together​

A complete package-scripts setup for a multi-locale app:

package.json
{
"scripts": {
"i18n:extract": "vp neokapi-i18n extract",
"i18n:pseudo": "kapi pseudo-translate i18n/",
"i18n:ai": "for lang in fr de ja; do kapi translate i18n/ --target-lang $lang; done",
"i18n:compile": "vp neokapi-i18n compile i18n/ --out public/translations"
}
}

Drive it from a project​

The commands above take their flags on every call, which suits a quick run. For an app you translate every release, a kapi.yaml project file records the content patterns, target languages, flows and defaults once. You then run named flows instead of repeating flags, and the project store keeps content memory and terminology across releases.

kapi init --framework neokapi-i18n scaffolds a layout that keeps every catalog under one i18n/ directory. The bundler writes source catalogs to i18n/src/, and kapi writes per-locale targets to i18n/{lang}/. Because the source lives under i18n/src/, the source glob never matches the generated targets, and you need no sibling i18n-<lang>/ trees:

kapi.yaml
version: v1
name: MyApp
defaults:
source_language: en
target_languages: [de, fr, ja, nb]
# The voice profile `kapi store import context` read from context/voice.yaml,
# bound by its name; the same import reads context/terms.json into the
# project's terms.
voice:
profile: myapp
collections:
- path: "i18n/src/**/*.kbf.json"
format: kbf
target: "i18n/{lang}/{path}.kbf.json"
i18n/
├── src/ source KBF catalogs (from `neokapi-i18n extract`)
└── de/ fr/ ja/ nb/ per-locale targets (from `kapi merge`)
context/
├── terms.json terms, read in by `kapi store import context`
└── voice.yaml voice profile, read in by the same import
.kapi/
└── work/ content memory and pseudo-locale output (ignored by git)

The content memory's local store and the pseudo-locale output are state that kapi can rebuild, so they live under .kapi/work/, a cache in the checkout that git ignores. The terms and the voice profile are files you commit under context/; an edit to context/voice.yaml or context/terms.json takes effect when you run kapi store import context again. See Context portability.

package.json
{
"scripts": {
"i18n:extract": "vp neokapi-i18n extract",
"i18n:translate": "kapi up && kapi merge",
"i18n:compile": "vp neokapi-i18n compile i18n/ --out public/translations"
}
}

kapi up discovers the nearest kapi.yaml recipe (or pass -p kapi.yaml) and runs the project's default flow over every declared target language: content memory reuse, then AI translation, then the bound checks, looping until each locale clears its ship gate or waits for review by a person. kapi merge writes the per-locale catalogs the recipe maps. kapi status reports the coverage per locale without running anything, and kapi run <flow> runs one named flow for a single pass. See Convergence, Flows and the project model.

Drive it from Claude​

The same flows run from your AI assistant. With the kapi MCP server connected, point Claude at your extracted strings and ask it to translate and check them. It calls kapi to translate the KBF archive, runs the checks, and fixes anything that breaks, the same author → check → revise loop you'd run by hand:

"Translate the strings in i18n/ to French and German, keep the placeholders and inline elements intact, then run the checks and fix anything that fails."

Claude translates in place (locale-additive), runs kapi exec qa / kapi exec term-check, and loops on the findings until the archive passes; you neokapi-i18n compile the result as usual. Claude changes the catalog and leaves your components as they are.

See Use with Claude for the MCP setup and the broader agent loop.