You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add an action that fills still-untranslated localization keys with machine (LLM) translations so apps never ship untranslated strings, validates each output preserves the source's placeholders, and writes results back under a clear marker. Human translations replace them on the next sync; AI output is never uploaded to GlotPress.
Filed for the WordPress-iOS continuous-localization work ("Faster Releases" RFC). Reference implementation: wordpress-mobile/WordPress-iOS#25675 — fastlane/helpers/ai_translator.rb + the backfill_missing_translations lane. Depends on#736 (placeholder primitive). WPiOS will adopt the toolkit version once it lands and delete the project-side copy.
Motivation
In the continuous-localization model, translations are downloaded daily and ship as-is. Between human-translation passes, locales have gaps — without a backstop, non-English locales ship English (or the raw key). Backfilling the gaps with an LLM as a clearly-marked, never-uploaded stopgap means the app never shows an untranslated string; human translations from GlotPress overwrite the AI entries on the next sync.
Behavior
For each target locale, compute missing keys = base-language keys absent from the locale file.
Batch missing (key → source value) and send to the LLM with strict rules:
Preserve every format specifier exactly — same count, order, and positional indices (%@, %1$d, %%).
Preserve leading/trailing whitespace and punctuation; concise and natural for a mobile UI.
Return only a JSON object key → translation (no prose/markdown/fences).
Write validated translations into the locale file under a marker comment (e.g. /* Machine-translated — pending human translation */), with correct file-format escaping.
Resilient: a failed batch/locale must not crash the job (best-effort); honor provider retry (429/5xx).
Provider / model
WPiOS uses Claudeclaude-sonnet-4-6 (to match the model already used by its CI Claude integration). The toolkit currently only ships openai_ask (OpenAI, gpt-4.1). Design decision needed:
(a) Add an Anthropic-backed action (matches WPiOS), or
(b) a provider-agnostic action with pluggable backends (OpenAI via existing openai_ask conventions + Anthropic), or
(c) extend openai_ask.
Recommendation: provider-agnostic core with an Anthropic adapter first, mirroring openai_ask's conventions:
Anthropic uses https://api.anthropic.com/v1/messages, headers x-api-key + anthropic-version: 2023-06-01 (not Authorization: Bearer), body { model, max_tokens, messages: [{ role: "user", content }] }, response content[].text where type == "text". Use exact model-id strings (no date suffix). Keep output per call under ~16K tokens (batch accordingly) to avoid non-streaming timeouts.
SDK vs raw HTTP: WPiOS used the official anthropic gem; openai_ask uses raw Net::HTTP (no gem dep). Recommend rawNet::HTTP for the toolkit to avoid a heavy dependency and match openai_ask. (Open question.)
Proposed options (ConfigItems)
Option
Notes
base_strings / locale_strings
Hashes or file paths.
target_locales
Locale codes + a code → human-language-name map for the prompt.
model
Default the project's choice (claude-sonnet-4-6).
api_token
env_name: ANTHROPIC_API_TOKEN, sensitive: true.
batch_size, max_tokens
Batching + per-call ceiling (WPiOS: 40 / 8192).
marker_comment
Text marking machine translations in the file.
dry_run
Return translations without writing (previews/tests).
The prompt needs locale-code → human language name (e.g. pt-BR → "Brazilian Portuguese"). WPiOS hardcodes a LANGUAGE_NAMES map. The toolkit already plans a LocaleHelper (see #296 and the TODO in WPiOS localization.rb) — this action should consume that rather than re-hardcode.
Dependency
Hard dependency on#736 — the per-string placeholders_compatible?(source, translation) check is what makes machine translation safe to ship.
Cross-platform
The translate + validate core is platform-agnostic (operates on { key => value }). File read/write/escape is platform-specific (iOS .strings, Android strings.xml). Recommend common/ core + ios_*/android_* wrappers. Complements the existing android_download_translations / ios_download_strings_files_from_glotpress (which fetch human translations).
Edge cases
Empty/whitespace source values → skip.
"Overloaded" keys where the key is the English text → changing English yields a new key, so backfill only fills truly missing keys (no special-casing needed).
Large missing sets → chunk; mind cost (below).
Regional English locales (en-GB/en-AU/en-CA) → still "translate" (spelling/locale conventions).
Model returns prose/code-fenced JSON despite instructions → extract the JSON object defensively.
Marker idempotency: the next human-translation download overwrites the locale file, dropping the AI section, then re-backfill — so the AI section is regenerated each run (see Cost).
.stringsdict / plurals → open question (likely v2).
Cost / efficiency
The naive daily flow re-translates all still-missing strings every run (no caching). For ~30 locales this is real recurring cost. Consider: only translating keys new since last run, caching AI outputs keyed by (source, locale), or a budget guard. (WPiOS accepted the naive cost initially.) Document the tradeoff; make batching/throttling configurable.
Testing
RSpec with a mocked LLM client (never hit the API in tests). Assert: missing-key detection; prompt construction; JSON extraction (incl. fenced/prose-wrapped); placeholder-drop behavior; file write/escape round-trip.
Summary
Add an action that fills still-untranslated localization keys with machine (LLM) translations so apps never ship untranslated strings, validates each output preserves the source's placeholders, and writes results back under a clear marker. Human translations replace them on the next sync; AI output is never uploaded to GlotPress.
Motivation
In the continuous-localization model, translations are downloaded daily and ship as-is. Between human-translation passes, locales have gaps — without a backstop, non-English locales ship English (or the raw key). Backfilling the gaps with an LLM as a clearly-marked, never-uploaded stopgap means the app never shows an untranslated string; human translations from GlotPress overwrite the AI entries on the next sync.
Behavior
(key → source value)and send to the LLM with strict rules:%@,%1$d,%%).key → translation(no prose/markdown/fences)./* Machine-translated — pending human translation */), with correct file-format escaping.Provider / model
WPiOS uses Claude
claude-sonnet-4-6(to match the model already used by its CI Claude integration). The toolkit currently only shipsopenai_ask(OpenAI,gpt-4.1). Design decision needed:openai_askconventions + Anthropic), oropenai_ask.Recommendation: provider-agnostic core with an Anthropic adapter first, mirroring
openai_ask's conventions:ANTHROPIC_API_TOKENenv,sensitive: trueConfigItem (cf.OPENAI_API_TOKEN).https://api.anthropic.com/v1/messages, headersx-api-key+anthropic-version: 2023-06-01(notAuthorization: Bearer), body{ model, max_tokens, messages: [{ role: "user", content }] }, responsecontent[].textwheretype == "text". Use exact model-id strings (no date suffix). Keep output per call under ~16K tokens (batch accordingly) to avoid non-streaming timeouts.anthropicgem;openai_askuses rawNet::HTTP(no gem dep). Recommend rawNet::HTTPfor the toolkit to avoid a heavy dependency and matchopenai_ask. (Open question.)Proposed options (ConfigItems)
base_strings/locale_stringstarget_localesmodelclaude-sonnet-4-6).api_tokenenv_name: ANTHROPIC_API_TOKEN,sensitive: true.batch_size,max_tokensmarker_commentdry_runReturn value: per-locale translations applied + count dropped for placeholder mismatch.
Language-name map
The prompt needs locale-code → human language name (e.g.
pt-BR→ "Brazilian Portuguese"). WPiOS hardcodes aLANGUAGE_NAMESmap. The toolkit already plans aLocaleHelper(see #296 and the TODO in WPiOSlocalization.rb) — this action should consume that rather than re-hardcode.Dependency
Hard dependency on #736 — the per-string
placeholders_compatible?(source, translation)check is what makes machine translation safe to ship.Cross-platform
The translate + validate core is platform-agnostic (operates on
{ key => value }). File read/write/escape is platform-specific (iOS.strings, Androidstrings.xml). Recommendcommon/core +ios_*/android_*wrappers. Complements the existingandroid_download_translations/ios_download_strings_files_from_glotpress(which fetch human translations).Edge cases
en-GB/en-AU/en-CA) → still "translate" (spelling/locale conventions)..stringsdict/ plurals → open question (likely v2).Cost / efficiency
The naive daily flow re-translates all still-missing strings every run (no caching). For ~30 locales this is real recurring cost. Consider: only translating keys new since last run, caching AI outputs keyed by
(source, locale), or a budget guard. (WPiOS accepted the naive cost initially.) Document the tradeoff; make batching/throttling configurable.Testing
Acceptance criteria
openai_askconventions.dry_run.actions/README.mdentry.Open questions
openai_ask).Net::HTTP.LocaleHelperintegration (#296) for language names..stringsdict/ Android plurals.