Build Loco

KINDAreplaces $5.95/mosaves $71.4/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 20 and TypeScript with a local web editor, one folderStorageSQLite, one row per message per plural categoryLocale dataCLDR plural rules from a maintained package, version recorded per projectInternal formICU MessageFormat as the stored representation for every messageAdaptersOne module per format, each with an import, an export and a round-trip test

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/loco.dbProjects, keys, messages and history. This is the translation memory you have.
SCREENSHOT_DIRrequired./data/screenshotsPer-key screenshots shown beside the editing boxes. Back up with the database.
CLDR_PACKAGE_VERSIONrequired46.0.0Recorded on each project so a changed category set is explainable.
REQUIRED_LOCALESrequireden,fr,plThe command line exits non-zero when any of these has untranslated keys.
EXPORT_DIRrequired./localesWhere the command line writes. Exports are deterministic and safe to commit.
MT_API_KEYsecretoptionalredactedOptional machine translation. Filled cells are marked machine and never counted as translated.

The build, in order

  1. Plural categories from the locale data

    Adding a locale produces exactly the editing boxes its plural rules require, with no code change.

    1. Your hand-written table is wrong for languages you cannot check.

      Files src/locales/plural-rules.tspackage.json

    2. The schema is where this either works or does not.

      Files src/db/schema.sql

    3. Form validation gets bypassed by the import path.

      Files tests/categories.test.ts

    done when · tick each as it passes
    watch 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.
  2. Context is a required field

    No key can be published without a description and a declared placeholder set, and both travel into every export.

    1. A key with no context is a key that comes back wrong.

      Files src/keys/create.ts

    2. A dropped placeholder is a crash, not a typo.

      Files src/messages/validate.tstests/placeholders.test.ts

    3. The context is useless if it stays in your database.

      Files src/adapters/comments.ts

    done when · tick each as it passes
    watch 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.
  3. One internal form, adapters at the edges

    Every format converts to and from one stored representation, and every adapter round trips a hostile fixture.

    1. Pairwise conversion between formats is a combinatorial trap.

      Files src/messages/icu.ts

    2. Two copies means one of them is stale.

      Files src/adapters/android.tssrc/adapters/json.ts

    3. Quotes and apostrophes are where adapters actually break.

      Files tests/roundtrip.test.tstests/fixtures/

    done when · tick each as it passes
    watch 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.
  4. gettext, where the categories have no names

    PO files import and export with their plural indices correctly mapped to named categories.

    1. The file's own rule is the only thing that defines the order.

      Files src/adapters/po/plural-forms.tstests/po-plurals.test.ts

    2. Guessing here puts wrong text in front of real readers.

      Files src/adapters/po/import.ts

    3. Export has to reproduce what the project already uses.

      Files src/adapters/po/export.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more localization.