Build POEditor
KINDAreplaces $20/mosaves $240/yrback to the verdict
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.
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 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost:5432/i18n | Your PostgreSQL connection string. |
SOURCE_LOCALE | required | en | The locale keys are authored in. Everything else translates from it. |
EXPORT_FORMAT | required | json | The format your application reads. Round tripped exactly. |
UNTRANSLATED_BEHAVIOUR | required | omit | Whether an untranslated key falls back to source text or is omitted. Decide deliberately. |
MACHINE_TRANSLATION_API_KEYsecret | optional | replace-with-key | Optional. Output enters the suggestion queue and never ships unreviewed. |
RELEASE_COMPLETENESS_THRESHOLD | optional | 95 | Percentage below which a language is reported as blocking a release, if you use that check. |
The build, in order
Round trip a real file
Your application's translation file imports and exports back byte-identically.
Whichever your application reads. Adding a second format later is easier than migrating from a wrong first one.
Files
src/formats/Comments, order and unknown metadata. Compare byte for byte.
Files
src/formats/roundtrip.test.jsA 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 passeswatch 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.
Plural forms per language, from the rules
Each language shows exactly the plural forms it needs, not two.
From Intl.PluralRules for the locale. Store them on the language so the editor can render from data.
Files
src/plurals.jsWith the category name and an example number, so a translator knows what few means here.
Files
src/editor.jsAn 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 passeswatch 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.
Context, because a word is not a translation unit
A translator can see what a string is for before translating it.
So it lives with the code and cannot drift. A context maintained separately is a context that goes stale.
Files
src/import.jsGerman is routinely longer than English and a button that fits in one may not in the other.
Files
src/editor.jsVisibly, as a backlog for the developers rather than a problem for the translator.
Files
src/keys.js
done when · tick each as it passeswatch 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.
Suggestions and review
Anyone can suggest, a reviewer approves, and nothing ships unreviewed.
Different tables. The exported file reads only approved translations.
Files
src/suggestions.jsMarked with its source so a reviewer knows what they are reading.
Files
src/machine.jsServer side, tested by direct request rather than through the interface.
Files
src/auth.js
done when · tick each as it passeswatch 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.
Export and the incomplete language question
Exports are valid, and a half-translated language behaves the way you decided.
Suggestions never reach a file. The state machine is the gate.
Files
src/export.jsParse every message. A file that throws in production is worse than a missing translation.
Files
src/export.jsSource text or omission. Both are defensible; an accidental mixture is not.
Files
docs/fallback.md
done when · tick each as it passeswatch 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.
Keeping it in step with the codeproduct builder
New strings appear for translation automatically, and removed ones do not vanish.
Runnable in continuous integration, reporting added, removed and changed.
Files
src/cli.jsAnd restore on return. Test the full remove-and-restore cycle.
Files
src/import.jsUntranslated and unreviewed counts. This is what a release decision needs.
Files
src/report.js
done when · tick each as it passeswatch out- Deleting a key because it disappeared from the source file loses every translation of it, and a key removed by mistake and restored a week later comes back untranslated in twelve languages. Mark it removed, keep the translations, and restore them if the key returns.
That is the whole plan for POEditor. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A professional translator marketplace.
- Cross-project translation memory.
- The integration catalogue.
- Any judgement about whether a translation is good.
- hosted collaboration, integrations, translation memory, and simple workflow
- professional translator marketplace
- advanced translation memory
- website proxy network
- enterprise workflows and integrations
- Translation memory within one project, which needs no corpus beyond your own history
- Screenshot capture from your test suite, so context arrives automatically
- A pseudo-locale that expands strings, which finds every layout that will break in German
Need the files? The project pack on the verdict page hands your agent the whole brief · more localization.