Build POEditor

KINDAreplaces $20/mosaves $240/yrback to the verdict

0%0 of 20 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 place where source strings arrive from your repository with their context, translators work per language against the plural forms that language actually needs, suggestions are reviewed before they ship, and approved translations export back in the format your application already reads.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNode 22 with PostgreSQLMessage formatICU MessageFormat, parsed and validatedPlural formsUnicode plural rules through Intl.PluralRulesFilesImport and export standard formats, never a proprietary oneMachine assistanceAn optional translation provider producing suggestions only

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 application and provides Intl.PluralRules, which is where plural categories come from.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher and Intl.PluralRules exists

  2. installfree

    Why Keys, translations and revisions are relational and the history needs to be queryable.

    Get it Install locally or run it in Docker. open ↗

    Verify psql connects and reports 16 or higher

  3. have readyfree

    Why Phase 1 round trips it, and a synthetic file will not have the comments, ordering and edge cases that break a naive parser.

    Get it Take the actual file your application loads, with its real keys and comments.

    Verify It imports and exports byte-identically

  4. decidefree

    Why It determines how many plural forms your keys must accommodate, and adding a four-form language after modelling keys as two forms is a migration rather than a setting.

    Get it List them before Phase 2. If any of them is Arabic, Russian, Polish or another multi-form language, the key shape decision is settled for you.

    Verify The list exists and its plural categories are derived and recorded

  5. decidevaries; this is the cost the hosted marketplace exists to solve

    Why Format validation catches malformed messages, not wrong ones. A language with no reviewer ships text nobody has read.

    Get it Identify a person per language before enabling it. A language without a reviewer should stay disabled rather than shipping machine output.

    Verify Every enabled language has a named reviewer

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), languages(id, project_id, locale, plural_categories_json, enabled)
- keys(id, project_id, name, source_text, context, max_length, screenshot_path, first_seen_at, removed_at)
- translations(id, key_id, language_id, form, text, state, author_id, created_at) · form is the plural category, or none for a simple string
- suggestions(id, key_id, language_id, form, text, source, author_id, created_at, resolved_as)
- revisions(id, translation_id, text, at, author_id) · every change kept, because a regression needs a diff
- imports(id, project_id, format, file_hash, at, keys_added, keys_removed, translations_updated)
- A key removed from source is marked removed rather than deleted, so restoring it does not lose a year of translations.

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_URLsecretrequiredpostgres://user:pass@localhost:5432/i18nYour PostgreSQL connection string.
SOURCE_LOCALErequiredenThe locale keys are authored in. Everything else translates from it.
EXPORT_FORMATrequiredjsonThe format your application reads. Round tripped exactly.
UNTRANSLATED_BEHAVIOURrequiredomitWhether an untranslated key falls back to source text or is omitted. Decide deliberately.
MACHINE_TRANSLATION_API_KEYsecretoptionalreplace-with-keyOptional. Output enters the suggestion queue and never ships unreviewed.
RELEASE_COMPLETENESS_THRESHOLDoptional95Percentage below which a language is reported as blocking a release, if you use that check.

The build, in order

  1. Round trip a real file

    Your application's translation file imports and exports back byte-identically.

    1. Whichever your application reads. Adding a second format later is easier than migrating from a wrong first one.

      Files src/formats/

    2. Comments, order and unknown metadata. Compare byte for byte.

      Files src/formats/roundtrip.test.js

    3. A key you cannot parse stops the import. Silently dropping it loses a translation nobody notices for months.

      Files src/import.js

    done when · tick each as it passes
    watch out
    • Dropping comments, ordering or metadata on import means the first export produces a file that diffs against the original everywhere, and nobody will trust the tool after that. Round trip before doing anything else, and treat any difference as a bug rather than a formatting preference.
  2. Plural forms per language, from the rules

    Each language shows exactly the plural forms it needs, not two.

    1. From Intl.PluralRules for the locale. Store them on the language so the editor can render from data.

      Files src/plurals.js

    2. With the category name and an example number, so a translator knows what few means here.

      Files src/editor.js

    3. An ICU plural message must cover every required category for its language. Missing one is a failed validation, not a warning.

      Files src/validate.js

    done when · tick each as it passes
    watch out
    • A key with only singular and plural fields is untranslatable into Arabic, which uses zero, one, two, few, many and other, or into Russian and Polish, which use one, few, many and other. The shape of the key is the problem and the translator cannot work around it.
    • Do not hard code which languages need which forms. Derive them from the plural rules the platform provides, so adding a language is a configuration change rather than a code change.
  3. Context, because a word is not a translation unit

    A translator can see what a string is for before translating it.

    1. So it lives with the code and cannot drift. A context maintained separately is a context that goes stale.

      Files src/import.js

    2. German is routinely longer than English and a button that fits in one may not in the other.

      Files src/editor.js

    3. Visibly, as a backlog for the developers rather than a problem for the translator.

      Files src/keys.js

    done when · tick each as it passes
    watch out
    • A bare string with no context is ambiguous in most languages: Open is a verb and an adjective, and the correct translation differs. Without context the translator guesses, and the guess is wrong often enough to matter in exactly the languages nobody on your team can check.
  4. Suggestions and review

    Anyone can suggest, a reviewer approves, and nothing ships unreviewed.

    1. Different tables. The exported file reads only approved translations.

      Files src/suggestions.js

    2. Marked with its source so a reviewer knows what they are reading.

      Files src/machine.js

    3. Server side, tested by direct request rather than through the interface.

      Files src/auth.js

    done when · tick each as it passes
    watch out
    • Letting machine translation write directly into the approved translation means shipping text nobody has read in a language nobody on the team speaks, and the failure is invisible until a user reports it. Route it through the same suggestion queue, so the review step is structural rather than a policy people remember.
  5. Export and the incomplete language question

    Exports are valid, and a half-translated language behaves the way you decided.

    1. Suggestions never reach a file. The state machine is the gate.

      Files src/export.js

    2. Parse every message. A file that throws in production is worse than a missing translation.

      Files src/export.js

    3. Source text or omission. Both are defensible; an accidental mixture is not.

      Files docs/fallback.md

    done when · tick each as it passes
    watch out
    • A translation containing a malformed ICU message will throw at runtime in the user's language and not in yours, so it ships and appears only for people who cannot easily report it. Validate every message at export and refuse to write a file containing one.
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.