The FS25 Farm Dashboard ships with 27 user-selectable languages. This document describes how the catalog is structured, how to edit a translation, and how to refill missing strings through Google Cloud Translation v3.
en (source)
de fr es it pl nl pt sv da fi cs el hu ro bg hr sk sl
et lv lt ga mt is nb uk
These appear in both the in-app Settings → Language picker
(web/assests/js/i18n/i18n.js) and the first-run setup wizard
(web/assests/js/setup-i18n.js).
web/locales/
messages/
en.json ← single source of truth (every key the app uses)
de.json … uk.json ← per-locale overrides; only keys that differ from en
.mt-cache.json ← MT cache, sha1(en)|lang → translation, gitignored
audit-keys.mjs ← npm run i18n:audit
find-hardcoded-strings.mjs ← npm run i18n:scan
build-translations.mjs ← npm run i18n:build (writes translations.json)
mt-fill.mjs ← npm run i18n:fill (calls Google Translate)
verify-i18n.mjs ← npm run i18n:verify (coverage gate)
sync-keys-from-en.mjs ← npm run i18n:sync (copy missing keys from en.json into every locale — English fallback until hand-translated)
translations.json ← compiled catalog, what `i18n/i18n.js` + `setup-i18n.js` load
translations.json is generated; never edit it by hand. Every change goes
into messages/en.json (for new English copy) or
messages/<code>.json (for a hand-corrected translation).
- Add or change English copy in
messages/en.json.- Use
{{name}}for runtime placeholders. They survive translation as long as they keep the{{ … }}form; the MT script wraps them in<span translate="no">…</span>before calling Google.
- Use
- Reference it from code:
- In dashboard JS:
import { t } from "./i18n/i18n.js"and callt("namespace.key", { name: value }). - In dashboard HTML:
<span data-i18n="namespace.key"></span>(ordata-i18n-attr="placeholder"for input placeholders, etc.). - In
setup.html:setupT("namespace.key", { name: value })ordata-setup-i18n="namespace.key".
- In dashboard JS:
- Rebuild with
npm run i18n:buildto refreshtranslations.json. - If you added many keys to
en.json, runnpm run i18n:syncso everymessages/<lang>.jsongets the new keys (copied from English), thennpm run i18n:verifymust pass before merge. - Refill non-English locales with
npm run i18n:fill(Google Translate API — optional polish after sync).
Just edit messages/<code>.json and add or replace keys. The build script
validates that:
- every key in a locale file also exists in
en.json, - values are non-empty strings,
- the set of
{{placeholder}}tokens matchesen.json.
A mismatch fails npm run i18n:build with an actionable error. npm run i18n:sync
copies missing keys from en.json into each locale so npm run i18n:verify
can pass in CI; replace copied English with a proper translation when you have one.
mt-fill.mjs walks each locale, identifies keys where the translation is
missing, empty, or identical to English, and translates them via Google's REST
API in HTML mode (so inline <strong>, <code>, <br> are preserved).
Placeholders are protected by substituting {{token}} with XPH<n>X sentinels
before sending and restoring them after. This is more reliable than wrapping in
<span translate="no"> because Google occasionally collapses short trailing
words next to a span (we have observed {{count}} animal becoming just
{{count}} for some locales when wrapped in span). The sentinel scheme keeps
context intact.
Pick one:
- Service account (preferred for v3) — set
GOOGLE_APPLICATION_CREDENTIALS=/abs/path/to/sa.json. The script signs a short-lived JWT and exchanges it for an OAuth2 token automatically and uses Translation v3 (projects/<id>/locations/global:translateText). The service account needs theCloud Translation API Userrole (roles/cloudtranslate.user) on the GCP project. - API key (uses v2) — set
GOOGLE_TRANSLATE_API_KEY=AIza…. Translation v3 does not accept API keys fortranslateText, so the script transparently routes API-key auth through Translation v2 (/language/translate/v2), which fully supports HTML mode and is functionally equivalent for our use case. Restrict the key to the Cloud Translation API in the GCP console.
Never commit either credential. They are read at runtime from the environment only.
# Fill only what is missing across every locale (cheap to re-run, cached)
npm run i18n:fill
# Force-re-translate every key (uses cache for repeats; expensive without cache)
npm run i18n:fill:force
# Limit scope while testing
node web/locales/mt-fill.mjs --langs de,fr --limit 20
# Show the work plan without calling the API
node web/locales/mt-fill.mjs --dry-runAfter a fill, run:
npm run i18n:build # regenerate translations.json
npm run i18n:verify # report per-locale coverage; non-zero exit if gapsSuccessful translations are stored in messages/.mt-cache.json keyed by
sha1(srcString)|targetLang. Re-running npm run i18n:fill after no English
changes is essentially free; deleting the cache and re-running will re-call
Google for every string.
- Placeholder tokens are checked round-trip; entries that lost a
{{token}}during translation are dropped (logged asdropped "<key>" — placeholder drift) and fall back to English instead of being saved. - Empty translations are dropped.
- Lost-content guard: if the non-placeholder text in a translation is more
than 75 % shorter than the source's non-placeholder text (e.g. Google
swallowed a trailing noun), the entry is dropped with
lost meaningful content next to placeholderso it can be re-tried. npm run i18n:verifyexits non-zero if any locale is below 100 % coverage or contains placeholder drift, making it suitable for CI.
The catalog is currently around 600 keys; with 26 non-English locales that is roughly 15 600 strings on a first run. Google Cloud Translation v3 charges per character and a one-off fill of the entire catalog should land well under $10 USD on standard pricing. The cache eliminates repeat cost.
[mt-fill] OAuth token exchange failed— the service account JSON is malformed orprivate_keyis missing newlines. Re-download from the GCP console.Translate API error 403— the Translation API is disabled for the project, or the service account lacksroles/cloudtranslate.user.Translate API error 429— quota hit. The script retries with exponential backoff up to 5 attempts; rerun later if it still fails.placeholder driftwarnings — Google moved or escaped a{{token}}. Editmessages/<code>.jsonby hand for those keys; the rest of the locale is unaffected.npm run i18n:buildcomplains about an unknown key — the locale file contains a key that no longer exists inen.json. Remove it (or rename the English key first).