Build Loco
KINDAreplaces $5.95/mosaves $71.4/yrback to the verdict
A local translation manager whose hard problems are a plural model that comes from locale data rather than from English, making translator context mandatory instead of optional, and export adapters whose escaping and plural indexing are verified by round trip rather than by hope.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the editor, the adapters and the command line.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v20 or higher - installfree
Why Every category set in the project comes from it, and writing these rules by hand is wrong for exactly the languages you cannot check.
Get it Install a package that exposes cardinal plural categories per locale and exposes its data version. open ↗
Verify
Querying a Slavic locale returns four categories and Japanese returns one - have readyfree
Why Adapter correctness is only demonstrable against files that real tools wrote, with the comments, escapes and obsolete entries they leave behind.
Get it Copy your current locale files into a fixtures directory, including at least one PO with plurals and one Android resource file.
Verify
Each imports and round trips without entries in the skipped list - decidefree
Why Each adapter is a small ongoing commitment and the long tail is where a hosted product earns its money, so supporting three well beats supporting eight badly.
Get it List the platforms you ship to and write down the formats that follows, and which you are deliberately not supporting.
Verify
The list is in the README and matches the adapters in the source - decidefree
Why Apple's recommended format changed and the older files are still produced during a build, so the adapter you need depends on where in that chain you plug in.
Get it Read Apple's current localisation documentation, decide which artefact your project consumes, and record the date you checked. open ↗
Verify
The decision and the date are in the README
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- projects(id, name, source_locale, cldr_version, created_at) · the locale data version is recorded because the rules change - locales(project_id, code, plural_categories_json, is_source, required) · categories come from the locale data, never typed by a human - keys(id, project_id, key, context, description, screenshot_path, placeholders_json, max_length, created_at) - messages(id, key_id, locale, category, text, state, updated_at, updated_by) · category is a CLDR tag, 'other' for anything uncounted - state is one of untranslated, machine, needs_review or approved, and machine is never silently promoted - imports(id, project_id, locale, format, file_sha256, at, added, changed, unchanged, skipped_json) · skipped is what the adapter did not understand - history(message_id, ordinal, text, state, at, by) · rolling one string back is most of why a translation manager exists - There is no singular column and no plural column anywhere, only a category whose allowed values come from the locale's own rules.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/loco.db | Projects, keys, messages and history. This is the translation memory you have. |
SCREENSHOT_DIR | required | ./data/screenshots | Per-key screenshots shown beside the editing boxes. Back up with the database. |
CLDR_PACKAGE_VERSION | required | 46.0.0 | Recorded on each project so a changed category set is explainable. |
REQUIRED_LOCALES | required | en,fr,pl | The command line exits non-zero when any of these has untranslated keys. |
EXPORT_DIR | required | ./locales | Where the command line writes. Exports are deterministic and safe to commit. |
MT_API_KEYsecret | optional | redacted | Optional machine translation. Filled cells are marked machine and never counted as translated. |
The build, in order
Plural categories from the locale data
Adding a locale produces exactly the editing boxes its plural rules require, with no code change.
Your hand-written table is wrong for languages you cannot check.
Files
src/locales/plural-rules.tspackage.jsonThe schema is where this either works or does not.
Files
src/db/schema.sqlForm validation gets bypassed by the import path.
Files
tests/categories.test.ts
done when · tick each as it passeswatch out- Modelling a counted message as a singular string and a plural string works for English and breaks the first time somebody adds a language needing four forms, and the usual patch of adding a third column starts a sequence with no end and a schema nobody can query. Store one row per category, take the set from the locale data at render time, and let the editor build itself from that set.
- Copying a table of plural rules into your source is the version of this bug that survives review, because the table is right for the languages you speak and stale for the rest. Take the rules from a maintained package, record its version on the project, and show that version in the interface so a category set that changed is explainable.
Context is a required field
No key can be published without a description and a declared placeholder set, and both travel into every export.
A key with no context is a key that comes back wrong.
Files
src/keys/create.tsA dropped placeholder is a crash, not a typo.
Files
src/messages/validate.tstests/placeholders.test.tsThe context is useless if it stays in your database.
Files
src/adapters/comments.ts
done when · tick each as it passeswatch out- Exporting a key called button.open with the text 'Open' and no description hands a translator a word that is a verb in one language and an adjective in another, and what comes back is wrong in a way nobody on the team can read. Make the description mandatory at creation, refuse to export without it, and put it in the comment field of every format that has one.
- A translation that drops a placeholder, or invents one, crashes the application rather than reading oddly, and it will crash in the locale nobody on the team tests. Declare the placeholder set on the key, compare every saved translation against it, and reject the mismatch at save time with both sets shown.
One internal form, adapters at the edges
Every format converts to and from one stored representation, and every adapter round trips a hostile fixture.
Pairwise conversion between formats is a combinatorial trap.
Files
src/messages/icu.tsTwo copies means one of them is stale.
Files
src/adapters/android.tssrc/adapters/json.tsQuotes and apostrophes are where adapters actually break.
Files
tests/roundtrip.test.tstests/fixtures/
done when · tick each as it passeswatch out- Writing each export straight from the database means the escaping rules get reimplemented once per format and at least one of them is wrong: Android resources treat an apostrophe and a leading at sign specially, gettext uses C string escapes with its own line continuation, and a flat JSON file has to survive whatever the message text contains. Convert to one internal form on import, put the escaping in one function per adapter, and prove it with a round trip.
- Silently dropping what an adapter did not understand means a re-export deletes strings the previous tool wrote, and the deletion looks like a tidy diff. Record every unhandled construct in the skipped list with its line, show the list after every import, and refuse to export a locale where anything was skipped until it is acknowledged.
gettext, where the categories have no names
PO files import and export with their plural indices correctly mapped to named categories.
The file's own rule is the only thing that defines the order.
Files
src/adapters/po/plural-forms.tstests/po-plurals.test.tsGuessing here puts wrong text in front of real readers.
Files
src/adapters/po/import.tsExport has to reproduce what the project already uses.
Files
src/adapters/po/export.ts
done when · tick each as it passeswatch out- Assuming msgstr[0] is 'one' and msgstr[1] is 'other' is right for English and wrong for most languages that have interesting plurals, because the index order comes from that file's own C expression rather than from any shared naming, and nothing in the file tells you which is which. Evaluate the header's expression yourself over sample integers, derive the index for each category, and refuse rather than guess when the result contradicts the locale data.
- Accepting a PO file with no Plural-Forms header and defaulting to the English rule silently assigns every plural translation to the wrong form for any language that is not English, and the file will look fine in a diff. Refuse the import, name the missing header, and say what it should contain for that locale.
Into a build, and back out of a mistakeproduct builder
A command line feeds the build deterministically, and any string can be rolled back.
A noisy diff is a diff nobody reads.
Files
src/export/deterministic.tstests/determinism.test.tsThis is the only thing that stops a bad build shipping.
Files
src/cli/export.tsA key with three of four forms is not a translated key.
Files
src/report/completeness.ts
done when · tick each as it passeswatch out- An export whose key ordering or whitespace shifts between runs produces a diff on every build, people stop reading those diffs, and the bad string ships inside one of them. Sort deterministically, normalise line endings, and put a test in the suite that exports twice and compares bytes.
- A completeness percentage computed over keys rather than over category rows reports a locale as finished while its 'few' forms are all empty, which is exactly the failure this whole build exists to prevent. Count rows per category per locale, and show the missing categories by name.
That is the whole plan for Loco. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A hosted editor with translator accounts.
- Machine translation as a silent default.
- Translation memory and glossary suggestions.
- The long tail of platform formats.
- Format breadth: Loco reads and writes a long tail of formats, including the awkward Apple and Qt ones, with plural rules per locale already correct
- A shareable editor for non-technical translators, with roles, comments and progress tracking
- A stable public API and CLI that CI pipelines can pull from at build time
- Translation history, who changed what, and the ability to roll a string back
- Glossary and translation memory suggestions that improve as the project grows
- Ordinal and range plural support, which are separate rule sets in the same locale data and matter for anything with a leaderboard
- A read-only review link for one locale, which is the smallest useful step towards a translator who will not install anything
- Pseudolocalisation that expands strings and marks placeholders, which finds truncation before a real translation does
Need the files? The project pack on the verdict page hands your agent the whole brief · more localization.