Vibecode Tolgee Cloud
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Tolgee Cloud, use a managed localization workspace instead of operating the open-source stack. The hard boundary is managed hosting, backups, collaboration, machine translation, and support, plus translation memory, collaboration, and deployment integrations.
You are building a lean indie version of Tolgee Cloud. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Tolgee Cloud · indie build A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Format | Messages stored in a standard format with named placeholders | positional placeholders reorder wrongly in languages with different word order | | Plurals | Full plural category support per language | English has two forms, several languages have four to six, and picking two breaks them | | Staleness | Translations invalidated when their source changes | this is the failure that reaches production most often | | Delivery | Versioned bundles per locale, fetched at build or runtime | a translation change should not require a deployment, but it should be atomic | | Runtime | TypeScript with PostgreSQL | keys, versions and staleness are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the bundle publisher. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Keys, versions and staleness relationships. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Plural category data for your locales** · free - Why: Designing around English gives two forms, and several languages need four to six, which cannot be retrofitted without revisiting every plural string. - Get it: Take the plural rules from the standard locale data for every language you intend to support, including at least one with more than two categories. - Verify: Category lists exist per locale and at least one locale has four or more - [ ] **At least four target languages including a difficult one** · free - Why: Placeholder reordering, plural complexity and length constraints all only appear when the targets are genuinely different from the source. - Get it: Choose targets that differ structurally, including one with more plural categories and one substantially longer than English. - Verify: Four locales are configured, one with four or more plural forms - [ ] **A real application's string set** · free - Why: Context problems, length constraints and unused keys only become visible against a real codebase with a few hundred keys and some accumulated cruft. - Get it: Export the strings from an application you actually work on, including the keys nobody is sure are still used. - Verify: At least three hundred keys are imported with their code locations ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. - managed hosting, backups, collaboration, machine translation, and support - professional translator marketplace - advanced translation memory - website proxy network - enterprise workflows and integrations If one of those is essential to you, that is the reason to keep paying for Tolgee Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Tolgee Cloud The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me translation management where a changed source string cannot ship its old translation to replace Tolgee Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Format: Messages stored in a standard format with named placeholders · positional placeholders reorder wrongly in languages with different word order - Plurals: Full plural category support per language · English has two forms, several languages have four to six, and picking two breaks them - Staleness: Translations invalidated when their source changes · this is the failure that reaches production most often - Delivery: Versioned bundles per locale, fetched at build or runtime · a translation change should not require a deployment, but it should be atomic - Runtime: TypeScript with PostgreSQL · keys, versions and staleness are relational ### Data model (create this before Phase 1) - keys(id, namespace, name, description, screenshot_path, max_length, created_at, deprecated_at) - source_strings(key_id, text, plural_forms_json, version, updated_at) · the source of truth - translations(id, key_id, locale, text, plural_forms_json, source_version, state, translated_by, at) - state · current, stale, missing or needs_review. Stale means the source moved after this was written - locales(code, plural_categories_json, direction, fallback_locale) - bundles(id, locale, version, content_hash, published_at) · immutable, so a client fetches a consistent set - usages(key_id, file, line, last_seen_at) · so an unused key can be found and removed - A translation records the source version it was made from, which is the whole mechanism for detecting staleness. ### Phase 1 · Messages that survive being translated Build: Store messages in a standard format with named placeholders and structured plural forms. Validate that a translation uses the same placeholders as its source. Reject a translation that drops or invents one. Trap: Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. Done when: A translation missing a placeholder present in the source is rejected with the specific placeholder named. A translation inventing a placeholder is rejected. Placeholders are named rather than positional throughout. A message with two substitutions can be reordered freely in a translation without breaking. Do not build yet: No plurals, staleness or delivery. ### Phase 2 · Plurals in languages that are not English Build: Store the plural categories each locale actually uses and require a form for each. Refuse a translation missing a required category. Handle a source language with fewer categories than the target. Trap: Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. Done when: A locale requiring four plural categories cannot be saved with two, tested with a real one. A locale requiring six is supported. The source language having fewer categories than the target does not prevent a complete translation. The correct form is selected at runtime for a range of numbers per locale, verified against expected outputs. Do not build yet: No staleness or delivery yet. ### Phase 3 · A changed source invalidates its translations Build: Version source strings. When a source changes, mark every translation made from the previous version as stale. Distinguish a meaning change from a typo fix, and let the editor say which. Trap: Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. Done when: Editing a source string marks translations made from the previous version as stale. A change marked as not affecting meaning does not invalidate, and records who marked it. Stale translations are still served rather than falling back to the source, with the staleness visible to translators. The count of stale strings per locale is reported. Do not build yet: No delivery or usage tracking yet. ### Phase 4 · Context, because a word is not a sentence Build: Attach a description, a screenshot and a maximum length to keys. Surface which keys are used where in the codebase. Flag translations exceeding the length constraint. Trap: A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. Done when: A key can carry a description, a screenshot and a maximum length. Keys used in code are linked to their locations and unused keys are surfaced. A translation exceeding the length constraint is flagged before publishing. Keys with no context at all are reported as incomplete. Do not build yet: No delivery or bundles yet. ### Phase 5 · Shipping translations without shipping code Build: Publish immutable versioned bundles per locale. Clients fetch a version and get a consistent set. Handle a missing translation with a documented fallback chain rather than an empty string. Trap: Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. Done when: A client fetching a bundle version gets a consistent set, verified by publishing mid-fetch. Bundles are immutable once published. A missing translation follows a documented fallback chain and never renders as an empty string. Rolling back to a previous bundle version is possible and immediate. Do not build yet: Nothing after this. ### Out of scope (and why) - Machine translation quality, which is a model question rather than a workflow one and where the useful part is post-editing. - Translator marketplace and vendor management, which is most of what the hosted services actually provide. - Right-to-left layout handling, which is a client-side concern and a substantial one. - Translation memory and terminology at professional scale, which is a specialised field with mature tools. ### README must contain - That placeholders are named rather than positional, because many languages reorder - That plural categories are per locale, since several languages need four to six forms rather than two - That a source edit marks translations stale, and a typo fix can be marked as not affecting meaning - That stale translations are still served, because a stale translation beats an untranslated string - That bundles are immutable and versioned so a client is never half updated ===== AGENTS.md ===== # Agent instructions · Tolgee Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Messages stored in a standard format with named placeholders, Full plural category support per language, Translations invalidated when their source changes, Versioned bundles per locale, fetched at build or runtime, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ===== BUILD_PLAN.md ===== # Build plan · Tolgee Cloud A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Messages that survive being translated A string can be translated into a language that orders words differently. ### Steps 1. Use named placeholders throughout Positional ones swap values in languages that reorder. Files: `src/messages.ts` 2. Validate the placeholder set matches A pure function, unit tested with a dropped and an invented placeholder. Files: `src/validate.ts`, `src/validate.test.ts` 3. Reject rather than warn A missing placeholder is a runtime error in someone's language. Files: `src/translations.ts` ### Done when - [ ] A dropped placeholder is rejected by name - [ ] An invented placeholder is rejected - [ ] Placeholders are named throughout - [ ] Reordering in a translation does not break ### Watch out - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. ## Phase 2 · Plurals in languages that are not English Every locale gets exactly the plural forms its language requires. ### Steps 1. Store plural categories per locale From the standard data, not from an assumption about English. Files: `src/locales.ts` 2. Require every category the locale uses Two forms is an English answer to a general question. Files: `src/plurals.ts`, `src/plurals.test.ts` 3. Select the right form at runtime Tested across a range of numbers per locale, against expected output. Files: `src/select.ts` ### Done when - [ ] A four-category locale cannot be saved with two - [ ] A six-category locale is supported - [ ] A source with fewer categories still translates fully - [ ] Runtime selection matches expected output per locale ### Watch out - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. ## Phase 3 · A changed source invalidates its translations Editing English marks the other twenty languages as stale. ### Steps 1. Version sources and record the version per translation The mechanism the whole phase rests on. Files: `src/versions.ts` 2. Distinguish meaning changes from typo fixes Invalidating on every edit gets the feature turned off. Files: `src/edits.ts` 3. Keep serving stale translations A stale translation beats an English string in a French interface. Files: `src/serve.ts` ### Done when - [ ] A source edit marks translations stale - [ ] A typo fix does not invalidate, and records who said so - [ ] Stale translations are still served - [ ] Stale counts are reported per locale ### Watch out - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. ## Phase 4 · Context, because a word is not a sentence A translator can see where a string appears and how much room it has. ### Steps 1. Attach context to the key A bare word is ambiguous in ways English speakers do not notice. Files: `src/keys.ts` 2. Link keys to code locations So unused keys can be removed and context can be found. Files: `src/usage.ts` 3. Enforce length constraints German is longer. The button is not. Files: `src/constraints.ts` ### Done when - [ ] Keys carry description, screenshot and length - [ ] Code usage is linked and unused keys surfaced - [ ] Over-length translations are flagged - [ ] Context-free keys are reported ### Watch out - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ## Not in this build - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. ## After v1, if you want it - In-context editing on a running application, which is the fastest way to catch bad context - Machine translation as a first pass for post-editing, which is where it genuinely helps - Length warnings driven by rendered width rather than character count ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Keys, translations and version history. DATABASE_URL=postgres://localhost:5432/i18n # Required. The source of truth. Edits to it mark other locales stale. SOURCE_LOCALE=en # Required. Documented, and never resulting in an empty string. FALLBACK_CHAIN=regional,base,source # Required. A stale translation beats an English string in a French interface. SERVE_STALE_TRANSLATIONS=always # Required. Where immutable bundles are served from. BUNDLE_CDN_URL=https://i18n.example.com/bundles # Required. Whether a context-free key blocks or warns. A bare word is ambiguous. REQUIRE_KEY_CONTEXT=warn
You are building a lean indie version of Tolgee Cloud. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Tolgee Cloud · indie build A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Format | Messages stored in a standard format with named placeholders | positional placeholders reorder wrongly in languages with different word order | | Plurals | Full plural category support per language | English has two forms, several languages have four to six, and picking two breaks them | | Staleness | Translations invalidated when their source changes | this is the failure that reaches production most often | | Delivery | Versioned bundles per locale, fetched at build or runtime | a translation change should not require a deployment, but it should be atomic | | Runtime | TypeScript with PostgreSQL | keys, versions and staleness are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the bundle publisher. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Keys, versions and staleness relationships. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Plural category data for your locales** · free - Why: Designing around English gives two forms, and several languages need four to six, which cannot be retrofitted without revisiting every plural string. - Get it: Take the plural rules from the standard locale data for every language you intend to support, including at least one with more than two categories. - Verify: Category lists exist per locale and at least one locale has four or more - [ ] **At least four target languages including a difficult one** · free - Why: Placeholder reordering, plural complexity and length constraints all only appear when the targets are genuinely different from the source. - Get it: Choose targets that differ structurally, including one with more plural categories and one substantially longer than English. - Verify: Four locales are configured, one with four or more plural forms - [ ] **A real application's string set** · free - Why: Context problems, length constraints and unused keys only become visible against a real codebase with a few hundred keys and some accumulated cruft. - Get it: Export the strings from an application you actually work on, including the keys nobody is sure are still used. - Verify: At least three hundred keys are imported with their code locations ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. - managed hosting, backups, collaboration, machine translation, and support - professional translator marketplace - advanced translation memory - website proxy network - enterprise workflows and integrations If one of those is essential to you, that is the reason to keep paying for Tolgee Cloud, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Tolgee Cloud The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me translation management where a changed source string cannot ship its old translation to replace Tolgee Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Format: Messages stored in a standard format with named placeholders · positional placeholders reorder wrongly in languages with different word order - Plurals: Full plural category support per language · English has two forms, several languages have four to six, and picking two breaks them - Staleness: Translations invalidated when their source changes · this is the failure that reaches production most often - Delivery: Versioned bundles per locale, fetched at build or runtime · a translation change should not require a deployment, but it should be atomic - Runtime: TypeScript with PostgreSQL · keys, versions and staleness are relational ### Data model (create this before Phase 1) - keys(id, namespace, name, description, screenshot_path, max_length, created_at, deprecated_at) - source_strings(key_id, text, plural_forms_json, version, updated_at) · the source of truth - translations(id, key_id, locale, text, plural_forms_json, source_version, state, translated_by, at) - state · current, stale, missing or needs_review. Stale means the source moved after this was written - locales(code, plural_categories_json, direction, fallback_locale) - bundles(id, locale, version, content_hash, published_at) · immutable, so a client fetches a consistent set - usages(key_id, file, line, last_seen_at) · so an unused key can be found and removed - A translation records the source version it was made from, which is the whole mechanism for detecting staleness. ### Phase 1 · Messages that survive being translated Build: Store messages in a standard format with named placeholders and structured plural forms. Validate that a translation uses the same placeholders as its source. Reject a translation that drops or invents one. Trap: Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. Done when: A translation missing a placeholder present in the source is rejected with the specific placeholder named. A translation inventing a placeholder is rejected. Placeholders are named rather than positional throughout. A message with two substitutions can be reordered freely in a translation without breaking. Do not build yet: No plurals, staleness or delivery. ### Phase 2 · Plurals in languages that are not English Build: Store the plural categories each locale actually uses and require a form for each. Refuse a translation missing a required category. Handle a source language with fewer categories than the target. Trap: Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. Done when: A locale requiring four plural categories cannot be saved with two, tested with a real one. A locale requiring six is supported. The source language having fewer categories than the target does not prevent a complete translation. The correct form is selected at runtime for a range of numbers per locale, verified against expected outputs. Do not build yet: No staleness or delivery yet. ### Phase 3 · A changed source invalidates its translations Build: Version source strings. When a source changes, mark every translation made from the previous version as stale. Distinguish a meaning change from a typo fix, and let the editor say which. Trap: Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. Done when: Editing a source string marks translations made from the previous version as stale. A change marked as not affecting meaning does not invalidate, and records who marked it. Stale translations are still served rather than falling back to the source, with the staleness visible to translators. The count of stale strings per locale is reported. Do not build yet: No delivery or usage tracking yet. ### Phase 4 · Context, because a word is not a sentence Build: Attach a description, a screenshot and a maximum length to keys. Surface which keys are used where in the codebase. Flag translations exceeding the length constraint. Trap: A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. Done when: A key can carry a description, a screenshot and a maximum length. Keys used in code are linked to their locations and unused keys are surfaced. A translation exceeding the length constraint is flagged before publishing. Keys with no context at all are reported as incomplete. Do not build yet: No delivery or bundles yet. ### Phase 5 · Shipping translations without shipping code Build: Publish immutable versioned bundles per locale. Clients fetch a version and get a consistent set. Handle a missing translation with a documented fallback chain rather than an empty string. Trap: Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. Done when: A client fetching a bundle version gets a consistent set, verified by publishing mid-fetch. Bundles are immutable once published. A missing translation follows a documented fallback chain and never renders as an empty string. Rolling back to a previous bundle version is possible and immediate. Do not build yet: Nothing after this. ### Out of scope (and why) - Machine translation quality, which is a model question rather than a workflow one and where the useful part is post-editing. - Translator marketplace and vendor management, which is most of what the hosted services actually provide. - Right-to-left layout handling, which is a client-side concern and a substantial one. - Translation memory and terminology at professional scale, which is a specialised field with mature tools. ### README must contain - That placeholders are named rather than positional, because many languages reorder - That plural categories are per locale, since several languages need four to six forms rather than two - That a source edit marks translations stale, and a typo fix can be marked as not affecting meaning - That stale translations are still served, because a stale translation beats an untranslated string - That bundles are immutable and versioned so a client is never half updated ===== AGENTS.md ===== # Agent instructions · Tolgee Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Messages stored in a standard format with named placeholders, Full plural category support per language, Translations invalidated when their source changes, Versioned bundles per locale, fetched at build or runtime, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ===== BUILD_PLAN.md ===== # Build plan · Tolgee Cloud A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Messages that survive being translated A string can be translated into a language that orders words differently. ### Steps 1. Use named placeholders throughout Positional ones swap values in languages that reorder. Files: `src/messages.ts` 2. Validate the placeholder set matches A pure function, unit tested with a dropped and an invented placeholder. Files: `src/validate.ts`, `src/validate.test.ts` 3. Reject rather than warn A missing placeholder is a runtime error in someone's language. Files: `src/translations.ts` ### Done when - [ ] A dropped placeholder is rejected by name - [ ] An invented placeholder is rejected - [ ] Placeholders are named throughout - [ ] Reordering in a translation does not break ### Watch out - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. ## Phase 2 · Plurals in languages that are not English Every locale gets exactly the plural forms its language requires. ### Steps 1. Store plural categories per locale From the standard data, not from an assumption about English. Files: `src/locales.ts` 2. Require every category the locale uses Two forms is an English answer to a general question. Files: `src/plurals.ts`, `src/plurals.test.ts` 3. Select the right form at runtime Tested across a range of numbers per locale, against expected output. Files: `src/select.ts` ### Done when - [ ] A four-category locale cannot be saved with two - [ ] A six-category locale is supported - [ ] A source with fewer categories still translates fully - [ ] Runtime selection matches expected output per locale ### Watch out - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. ## Phase 3 · A changed source invalidates its translations Editing English marks the other twenty languages as stale. ### Steps 1. Version sources and record the version per translation The mechanism the whole phase rests on. Files: `src/versions.ts` 2. Distinguish meaning changes from typo fixes Invalidating on every edit gets the feature turned off. Files: `src/edits.ts` 3. Keep serving stale translations A stale translation beats an English string in a French interface. Files: `src/serve.ts` ### Done when - [ ] A source edit marks translations stale - [ ] A typo fix does not invalidate, and records who said so - [ ] Stale translations are still served - [ ] Stale counts are reported per locale ### Watch out - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. ## Phase 4 · Context, because a word is not a sentence A translator can see where a string appears and how much room it has. ### Steps 1. Attach context to the key A bare word is ambiguous in ways English speakers do not notice. Files: `src/keys.ts` 2. Link keys to code locations So unused keys can be removed and context can be found. Files: `src/usage.ts` 3. Enforce length constraints German is longer. The button is not. Files: `src/constraints.ts` ### Done when - [ ] Keys carry description, screenshot and length - [ ] Code usage is linked and unused keys surfaced - [ ] Over-length translations are flagged - [ ] Context-free keys are reported ### Watch out - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ## Not in this build - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. ## After v1, if you want it - In-context editing on a running application, which is the fastest way to catch bad context - Machine translation as a first pass for post-editing, which is where it genuinely helps - Length warnings driven by rendered width rather than character count ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Keys, translations and version history. DATABASE_URL=postgres://localhost:5432/i18n # Required. The source of truth. Edits to it mark other locales stale. SOURCE_LOCALE=en # Required. Documented, and never resulting in an empty string. FALLBACK_CHAIN=regional,base,source # Required. A stale translation beats an English string in a French interface. SERVE_STALE_TRANSLATIONS=always # Required. Where immutable bundles are served from. BUNDLE_CDN_URL=https://i18n.example.com/bundles # Required. Whether a context-free key blocks or warns. A bare word is ambiguous. REQUIRE_KEY_CONTEXT=warn
You are building a production product version of Tolgee Cloud. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Tolgee Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Tolgee Cloud, use a managed localization workspace instead of operating the open-source stack. The hard boundary is managed hosting, backups, collaboration, machine translation, and support, plus translation memory, collaboration, and deployment integrations. ## Product outcome A localisation workspace where a changed English string cannot quietly ship twenty stale translations, and where languages needing six plural forms get six. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - self-hosted deployment - source locale files - optional DeepL or OpenAI API key ## Explicit non-goals for v1 - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. - managed hosting, backups, collaboration, machine translation, and support - professional translator marketplace - advanced translation memory - website proxy network - enterprise workflows and integrations ## Success criteria - A dropped placeholder is confirmed rejected by name - A four-category locale is confirmed unable to save with two forms - A source edit is confirmed to mark translations stale - Stale translations are confirmed still served - A mid-publish fetch is confirmed to get a consistent set ===== BRIEF.md ===== # Build brief · Tolgee Cloud The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me translation management where a changed source string cannot ship its old translation to replace Tolgee Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Format: Messages stored in a standard format with named placeholders · positional placeholders reorder wrongly in languages with different word order - Plurals: Full plural category support per language · English has two forms, several languages have four to six, and picking two breaks them - Staleness: Translations invalidated when their source changes · this is the failure that reaches production most often - Delivery: Versioned bundles per locale, fetched at build or runtime · a translation change should not require a deployment, but it should be atomic - Runtime: TypeScript with PostgreSQL · keys, versions and staleness are relational ### Data model (create this before Phase 1) - keys(id, namespace, name, description, screenshot_path, max_length, created_at, deprecated_at) - source_strings(key_id, text, plural_forms_json, version, updated_at) · the source of truth - translations(id, key_id, locale, text, plural_forms_json, source_version, state, translated_by, at) - state · current, stale, missing or needs_review. Stale means the source moved after this was written - locales(code, plural_categories_json, direction, fallback_locale) - bundles(id, locale, version, content_hash, published_at) · immutable, so a client fetches a consistent set - usages(key_id, file, line, last_seen_at) · so an unused key can be found and removed - A translation records the source version it was made from, which is the whole mechanism for detecting staleness. ### Phase 1 · Messages that survive being translated Build: Store messages in a standard format with named placeholders and structured plural forms. Validate that a translation uses the same placeholders as its source. Reject a translation that drops or invents one. Trap: Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. Done when: A translation missing a placeholder present in the source is rejected with the specific placeholder named. A translation inventing a placeholder is rejected. Placeholders are named rather than positional throughout. A message with two substitutions can be reordered freely in a translation without breaking. Do not build yet: No plurals, staleness or delivery. ### Phase 2 · Plurals in languages that are not English Build: Store the plural categories each locale actually uses and require a form for each. Refuse a translation missing a required category. Handle a source language with fewer categories than the target. Trap: Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. Done when: A locale requiring four plural categories cannot be saved with two, tested with a real one. A locale requiring six is supported. The source language having fewer categories than the target does not prevent a complete translation. The correct form is selected at runtime for a range of numbers per locale, verified against expected outputs. Do not build yet: No staleness or delivery yet. ### Phase 3 · A changed source invalidates its translations Build: Version source strings. When a source changes, mark every translation made from the previous version as stale. Distinguish a meaning change from a typo fix, and let the editor say which. Trap: Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. Done when: Editing a source string marks translations made from the previous version as stale. A change marked as not affecting meaning does not invalidate, and records who marked it. Stale translations are still served rather than falling back to the source, with the staleness visible to translators. The count of stale strings per locale is reported. Do not build yet: No delivery or usage tracking yet. ### Phase 4 · Context, because a word is not a sentence Build: Attach a description, a screenshot and a maximum length to keys. Surface which keys are used where in the codebase. Flag translations exceeding the length constraint. Trap: A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. Done when: A key can carry a description, a screenshot and a maximum length. Keys used in code are linked to their locations and unused keys are surfaced. A translation exceeding the length constraint is flagged before publishing. Keys with no context at all are reported as incomplete. Do not build yet: No delivery or bundles yet. ### Phase 5 · Shipping translations without shipping code Build: Publish immutable versioned bundles per locale. Clients fetch a version and get a consistent set. Handle a missing translation with a documented fallback chain rather than an empty string. Trap: Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. Done when: A client fetching a bundle version gets a consistent set, verified by publishing mid-fetch. Bundles are immutable once published. A missing translation follows a documented fallback chain and never renders as an empty string. Rolling back to a previous bundle version is possible and immediate. Do not build yet: Nothing after this. ### Out of scope (and why) - Machine translation quality, which is a model question rather than a workflow one and where the useful part is post-editing. - Translator marketplace and vendor management, which is most of what the hosted services actually provide. - Right-to-left layout handling, which is a client-side concern and a substantial one. - Translation memory and terminology at professional scale, which is a specialised field with mature tools. ### README must contain - That placeholders are named rather than positional, because many languages reorder - That plural categories are per locale, since several languages need four to six forms rather than two - That a source edit marks translations stale, and a typo fix can be marked as not affecting meaning - That stale translations are still served, because a stale translation beats an untranslated string - That bundles are immutable and versioned so a client is never half updated ===== ARCHITECTURE.md ===== # Architecture · Tolgee Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Format | Messages stored in a standard format with named placeholders | positional placeholders reorder wrongly in languages with different word order | | Plurals | Full plural category support per language | English has two forms, several languages have four to six, and picking two breaks them | | Staleness | Translations invalidated when their source changes | this is the failure that reaches production most often | | Delivery | Versioned bundles per locale, fetched at build or runtime | a translation change should not require a deployment, but it should be atomic | | Runtime | TypeScript with PostgreSQL | keys, versions and staleness are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Messages | Named placeholders and validation. | Any format, provided placeholders are named rather than positional. | | Plurals | Per-locale category requirements. | Any implementation, provided categories come from locale data rather than assumption. | | Staleness | Source versioning and invalidation. | Any mechanism, provided a typo fix can be excluded from invalidation. | | Context | Descriptions, screenshots and constraints. | Any store. A key without context is an incomplete key. | | Bundles | Immutable versioned delivery. | Any transport, provided a client cannot fetch a half-updated set. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Keys, translations and version history. - `SOURCE_LOCALE` · required · The source of truth. Edits to it mark other locales stale. - `FALLBACK_CHAIN` · required · Documented, and never resulting in an empty string. - `SERVE_STALE_TRANSLATIONS` · required · A stale translation beats an English string in a French interface. - `BUNDLE_CDN_URL` · required · Where immutable bundles are served from. - `REQUIRE_KEY_CONTEXT` · required · Whether a context-free key blocks or warns. A bare word is ambiguous. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Tolgee Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Messages stored in a standard format with named placeholders, Full plural category support per language, Translations invalidated when their source changes, Versioned bundles per locale, fetched at build or runtime, TypeScript with PostgreSQL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. - Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. ===== MILESTONES.md ===== # Delivery milestones · Tolgee Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Messages that survive being translated A string can be translated into a language that orders words differently. ### Steps 1. Use named placeholders throughout Positional ones swap values in languages that reorder. Files: `src/messages.ts` 2. Validate the placeholder set matches A pure function, unit tested with a dropped and an invented placeholder. Files: `src/validate.ts`, `src/validate.test.ts` 3. Reject rather than warn A missing placeholder is a runtime error in someone's language. Files: `src/translations.ts` ### Done when - [ ] A dropped placeholder is rejected by name - [ ] An invented placeholder is rejected - [ ] Placeholders are named throughout - [ ] Reordering in a translation does not break ### Watch out - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. ## M2 · Plurals in languages that are not English Every locale gets exactly the plural forms its language requires. ### Steps 1. Store plural categories per locale From the standard data, not from an assumption about English. Files: `src/locales.ts` 2. Require every category the locale uses Two forms is an English answer to a general question. Files: `src/plurals.ts`, `src/plurals.test.ts` 3. Select the right form at runtime Tested across a range of numbers per locale, against expected output. Files: `src/select.ts` ### Done when - [ ] A four-category locale cannot be saved with two - [ ] A six-category locale is supported - [ ] A source with fewer categories still translates fully - [ ] Runtime selection matches expected output per locale ### Watch out - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. ## M3 · A changed source invalidates its translations Editing English marks the other twenty languages as stale. ### Steps 1. Version sources and record the version per translation The mechanism the whole phase rests on. Files: `src/versions.ts` 2. Distinguish meaning changes from typo fixes Invalidating on every edit gets the feature turned off. Files: `src/edits.ts` 3. Keep serving stale translations A stale translation beats an English string in a French interface. Files: `src/serve.ts` ### Done when - [ ] A source edit marks translations stale - [ ] A typo fix does not invalidate, and records who said so - [ ] Stale translations are still served - [ ] Stale counts are reported per locale ### Watch out - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. ## M4 · Context, because a word is not a sentence A translator can see where a string appears and how much room it has. ### Steps 1. Attach context to the key A bare word is ambiguous in ways English speakers do not notice. Files: `src/keys.ts` 2. Link keys to code locations So unused keys can be removed and context can be found. Files: `src/usage.ts` 3. Enforce length constraints German is longer. The button is not. Files: `src/constraints.ts` ### Done when - [ ] Keys carry description, screenshot and length - [ ] Code usage is linked and unused keys surfaced - [ ] Over-length translations are flagged - [ ] Context-free keys are reported ### Watch out - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ## M5 · Shipping translations without shipping code (production only) A locale updates atomically and a client never sees a half-updated bundle. ### Steps 1. Publish immutable versioned bundles So a client is never half updated. Files: `src/bundles.ts` 2. Document the fallback chain Regional to base to source. Never an empty string. Files: `src/fallback.ts` 3. Make rollback immediate A bad translation shipped is fixed by pointing at the previous version. Files: `src/publish.ts` ### Done when - [ ] A mid-publish fetch gets a consistent set - [ ] Bundles are immutable once published - [ ] Missing translations follow a documented fallback - [ ] Rollback to a previous version is immediate ### Watch out - Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. ===== OPERATIONS.md ===== # Operations · Tolgee Cloud ## Backup Translations represent paid human work and are expensive to recreate. Back up keys, translations and their source versions together, since a translation without its source version cannot have its staleness computed. ## Restore Restore and confirm source versions survived alongside translations, since losing them makes every translation's staleness unknown and the only safe assumption is that all of them are stale. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Stale count per locale and the proportion of keys with no context. The first is the number that predicts a wrong string in production, and the second predicts a wrong translation being written in the first place. ## Incident checklist An interface showing a mixture of two translation versions means bundles are not being served atomically. A wrong translation in production that reads fine usually means a source string changed and the staleness was marked as a typo fix when it was not. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A dropped placeholder is confirmed rejected by name - [ ] A four-category locale is confirmed unable to save with two forms - [ ] A source edit is confirmed to mark translations stale - [ ] Stale translations are confirmed still served - [ ] A mid-publish fetch is confirmed to get a consistent set ## Launch constraint Do not market omitted Tolgee Cloud capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Keys, translations and version history. DATABASE_URL=postgres://localhost:5432/i18n # Required. The source of truth. Edits to it mark other locales stale. SOURCE_LOCALE=en # Required. Documented, and never resulting in an empty string. FALLBACK_CHAIN=regional,base,source # Required. A stale translation beats an English string in a French interface. SERVE_STALE_TRANSLATIONS=always # Required. Where immutable bundles are served from. BUNDLE_CDN_URL=https://i18n.example.com/bundles # Required. Whether a context-free key blocks or warns. A bare word is ambiguous. REQUIRE_KEY_CONTEXT=warn
# Tolgee Cloud · indie build A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Format | Messages stored in a standard format with named placeholders | positional placeholders reorder wrongly in languages with different word order | | Plurals | Full plural category support per language | English has two forms, several languages have four to six, and picking two breaks them | | Staleness | Translations invalidated when their source changes | this is the failure that reaches production most often | | Delivery | Versioned bundles per locale, fetched at build or runtime | a translation change should not require a deployment, but it should be atomic | | Runtime | TypeScript with PostgreSQL | keys, versions and staleness are relational | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the bundle publisher. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Keys, versions and staleness relationships. - Get it: Docker or your package manager. - Verify: psql connects and creates a table - [ ] **Plural category data for your locales** · free - Why: Designing around English gives two forms, and several languages need four to six, which cannot be retrofitted without revisiting every plural string. - Get it: Take the plural rules from the standard locale data for every language you intend to support, including at least one with more than two categories. - Verify: Category lists exist per locale and at least one locale has four or more - [ ] **At least four target languages including a difficult one** · free - Why: Placeholder reordering, plural complexity and length constraints all only appear when the targets are genuinely different from the source. - Get it: Choose targets that differ structurally, including one with more plural categories and one substantially longer than English. - Verify: Four locales are configured, one with four or more plural forms - [ ] **A real application's string set** · free - Why: Context problems, length constraints and unused keys only become visible against a real codebase with a few hundred keys and some accumulated cruft. - Get it: Export the strings from an application you actually work on, including the keys nobody is sure are still used. - Verify: At least three hundred keys are imported with their code locations ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. - managed hosting, backups, collaboration, machine translation, and support - professional translator marketplace - advanced translation memory - website proxy network - enterprise workflows and integrations If one of those is essential to you, that is the reason to keep paying for Tolgee Cloud, and the README should say so rather than pretend.
# Build brief · Tolgee Cloud The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me translation management where a changed source string cannot ship its old translation to replace Tolgee Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Format: Messages stored in a standard format with named placeholders · positional placeholders reorder wrongly in languages with different word order - Plurals: Full plural category support per language · English has two forms, several languages have four to six, and picking two breaks them - Staleness: Translations invalidated when their source changes · this is the failure that reaches production most often - Delivery: Versioned bundles per locale, fetched at build or runtime · a translation change should not require a deployment, but it should be atomic - Runtime: TypeScript with PostgreSQL · keys, versions and staleness are relational ### Data model (create this before Phase 1) - keys(id, namespace, name, description, screenshot_path, max_length, created_at, deprecated_at) - source_strings(key_id, text, plural_forms_json, version, updated_at) · the source of truth - translations(id, key_id, locale, text, plural_forms_json, source_version, state, translated_by, at) - state · current, stale, missing or needs_review. Stale means the source moved after this was written - locales(code, plural_categories_json, direction, fallback_locale) - bundles(id, locale, version, content_hash, published_at) · immutable, so a client fetches a consistent set - usages(key_id, file, line, last_seen_at) · so an unused key can be found and removed - A translation records the source version it was made from, which is the whole mechanism for detecting staleness. ### Phase 1 · Messages that survive being translated Build: Store messages in a standard format with named placeholders and structured plural forms. Validate that a translation uses the same placeholders as its source. Reject a translation that drops or invents one. Trap: Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. Done when: A translation missing a placeholder present in the source is rejected with the specific placeholder named. A translation inventing a placeholder is rejected. Placeholders are named rather than positional throughout. A message with two substitutions can be reordered freely in a translation without breaking. Do not build yet: No plurals, staleness or delivery. ### Phase 2 · Plurals in languages that are not English Build: Store the plural categories each locale actually uses and require a form for each. Refuse a translation missing a required category. Handle a source language with fewer categories than the target. Trap: Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. Done when: A locale requiring four plural categories cannot be saved with two, tested with a real one. A locale requiring six is supported. The source language having fewer categories than the target does not prevent a complete translation. The correct form is selected at runtime for a range of numbers per locale, verified against expected outputs. Do not build yet: No staleness or delivery yet. ### Phase 3 · A changed source invalidates its translations Build: Version source strings. When a source changes, mark every translation made from the previous version as stale. Distinguish a meaning change from a typo fix, and let the editor say which. Trap: Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. Done when: Editing a source string marks translations made from the previous version as stale. A change marked as not affecting meaning does not invalidate, and records who marked it. Stale translations are still served rather than falling back to the source, with the staleness visible to translators. The count of stale strings per locale is reported. Do not build yet: No delivery or usage tracking yet. ### Phase 4 · Context, because a word is not a sentence Build: Attach a description, a screenshot and a maximum length to keys. Surface which keys are used where in the codebase. Flag translations exceeding the length constraint. Trap: A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. Done when: A key can carry a description, a screenshot and a maximum length. Keys used in code are linked to their locations and unused keys are surfaced. A translation exceeding the length constraint is flagged before publishing. Keys with no context at all are reported as incomplete. Do not build yet: No delivery or bundles yet. ### Phase 5 · Shipping translations without shipping code Build: Publish immutable versioned bundles per locale. Clients fetch a version and get a consistent set. Handle a missing translation with a documented fallback chain rather than an empty string. Trap: Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. Done when: A client fetching a bundle version gets a consistent set, verified by publishing mid-fetch. Bundles are immutable once published. A missing translation follows a documented fallback chain and never renders as an empty string. Rolling back to a previous bundle version is possible and immediate. Do not build yet: Nothing after this. ### Out of scope (and why) - Machine translation quality, which is a model question rather than a workflow one and where the useful part is post-editing. - Translator marketplace and vendor management, which is most of what the hosted services actually provide. - Right-to-left layout handling, which is a client-side concern and a substantial one. - Translation memory and terminology at professional scale, which is a specialised field with mature tools. ### README must contain - That placeholders are named rather than positional, because many languages reorder - That plural categories are per locale, since several languages need four to six forms rather than two - That a source edit marks translations stale, and a typo fix can be marked as not affecting meaning - That stale translations are still served, because a stale translation beats an untranslated string - That bundles are immutable and versioned so a client is never half updated
# Agent instructions · Tolgee Cloud indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Messages stored in a standard format with named placeholders, Full plural category support per language, Translations invalidated when their source changes, Versioned bundles per locale, fetched at build or runtime, TypeScript with PostgreSQL. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one.
# Build plan · Tolgee Cloud A localisation workspace whose two real problems are that plural and gender rules differ enormously between languages, and that a source string edited in English silently leaves twenty translations describing something else until someone notices in production. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Messages that survive being translated A string can be translated into a language that orders words differently. ### Steps 1. Use named placeholders throughout Positional ones swap values in languages that reorder. Files: `src/messages.ts` 2. Validate the placeholder set matches A pure function, unit tested with a dropped and an invented placeholder. Files: `src/validate.ts`, `src/validate.test.ts` 3. Reject rather than warn A missing placeholder is a runtime error in someone's language. Files: `src/translations.ts` ### Done when - [ ] A dropped placeholder is rejected by name - [ ] An invented placeholder is rejected - [ ] Placeholders are named throughout - [ ] Reordering in a translation does not break ### Watch out - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. ## Phase 2 · Plurals in languages that are not English Every locale gets exactly the plural forms its language requires. ### Steps 1. Store plural categories per locale From the standard data, not from an assumption about English. Files: `src/locales.ts` 2. Require every category the locale uses Two forms is an English answer to a general question. Files: `src/plurals.ts`, `src/plurals.test.ts` 3. Select the right form at runtime Tested across a range of numbers per locale, against expected output. Files: `src/select.ts` ### Done when - [ ] A four-category locale cannot be saved with two - [ ] A six-category locale is supported - [ ] A source with fewer categories still translates fully - [ ] Runtime selection matches expected output per locale ### Watch out - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. ## Phase 3 · A changed source invalidates its translations Editing English marks the other twenty languages as stale. ### Steps 1. Version sources and record the version per translation The mechanism the whole phase rests on. Files: `src/versions.ts` 2. Distinguish meaning changes from typo fixes Invalidating on every edit gets the feature turned off. Files: `src/edits.ts` 3. Keep serving stale translations A stale translation beats an English string in a French interface. Files: `src/serve.ts` ### Done when - [ ] A source edit marks translations stale - [ ] A typo fix does not invalidate, and records who said so - [ ] Stale translations are still served - [ ] Stale counts are reported per locale ### Watch out - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. ## Phase 4 · Context, because a word is not a sentence A translator can see where a string appears and how much room it has. ### Steps 1. Attach context to the key A bare word is ambiguous in ways English speakers do not notice. Files: `src/keys.ts` 2. Link keys to code locations So unused keys can be removed and context can be found. Files: `src/usage.ts` 3. Enforce length constraints German is longer. The button is not. Files: `src/constraints.ts` ### Done when - [ ] Keys carry description, screenshot and length - [ ] Code usage is linked and unused keys surfaced - [ ] Over-length translations are flagged - [ ] Context-free keys are reported ### Watch out - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ## Not in this build - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. ## After v1, if you want it - In-context editing on a running application, which is the fastest way to catch bad context - Machine translation as a first pass for post-editing, which is where it genuinely helps - Length warnings driven by rendered width rather than character count
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Keys, translations and version history. DATABASE_URL=postgres://localhost:5432/i18n # Required. The source of truth. Edits to it mark other locales stale. SOURCE_LOCALE=en # Required. Documented, and never resulting in an empty string. FALLBACK_CHAIN=regional,base,source # Required. A stale translation beats an English string in a French interface. SERVE_STALE_TRANSLATIONS=always # Required. Where immutable bundles are served from. BUNDLE_CDN_URL=https://i18n.example.com/bundles # Required. Whether a context-free key blocks or warns. A bare word is ambiguous. REQUIRE_KEY_CONTEXT=warn
# Tolgee Cloud · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Tolgee Cloud, use a managed localization workspace instead of operating the open-source stack. The hard boundary is managed hosting, backups, collaboration, machine translation, and support, plus translation memory, collaboration, and deployment integrations. ## Product outcome A localisation workspace where a changed English string cannot quietly ship twenty stale translations, and where languages needing six plural forms get six. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - PostgreSQL - self-hosted deployment - source locale files - optional DeepL or OpenAI API key ## Explicit non-goals for v1 - Machine translation quality. - Translator marketplace and vendor management. - Right-to-left layout handling. - Professional translation memory and terminology. - managed hosting, backups, collaboration, machine translation, and support - professional translator marketplace - advanced translation memory - website proxy network - enterprise workflows and integrations ## Success criteria - A dropped placeholder is confirmed rejected by name - A four-category locale is confirmed unable to save with two forms - A source edit is confirmed to mark translations stale - Stale translations are confirmed still served - A mid-publish fetch is confirmed to get a consistent set
# Build brief · Tolgee Cloud The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me translation management where a changed source string cannot ship its old translation to replace Tolgee Cloud. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Format: Messages stored in a standard format with named placeholders · positional placeholders reorder wrongly in languages with different word order - Plurals: Full plural category support per language · English has two forms, several languages have four to six, and picking two breaks them - Staleness: Translations invalidated when their source changes · this is the failure that reaches production most often - Delivery: Versioned bundles per locale, fetched at build or runtime · a translation change should not require a deployment, but it should be atomic - Runtime: TypeScript with PostgreSQL · keys, versions and staleness are relational ### Data model (create this before Phase 1) - keys(id, namespace, name, description, screenshot_path, max_length, created_at, deprecated_at) - source_strings(key_id, text, plural_forms_json, version, updated_at) · the source of truth - translations(id, key_id, locale, text, plural_forms_json, source_version, state, translated_by, at) - state · current, stale, missing or needs_review. Stale means the source moved after this was written - locales(code, plural_categories_json, direction, fallback_locale) - bundles(id, locale, version, content_hash, published_at) · immutable, so a client fetches a consistent set - usages(key_id, file, line, last_seen_at) · so an unused key can be found and removed - A translation records the source version it was made from, which is the whole mechanism for detecting staleness. ### Phase 1 · Messages that survive being translated Build: Store messages in a standard format with named placeholders and structured plural forms. Validate that a translation uses the same placeholders as its source. Reject a translation that drops or invents one. Trap: Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. Done when: A translation missing a placeholder present in the source is rejected with the specific placeholder named. A translation inventing a placeholder is rejected. Placeholders are named rather than positional throughout. A message with two substitutions can be reordered freely in a translation without breaking. Do not build yet: No plurals, staleness or delivery. ### Phase 2 · Plurals in languages that are not English Build: Store the plural categories each locale actually uses and require a form for each. Refuse a translation missing a required category. Handle a source language with fewer categories than the target. Trap: Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. Done when: A locale requiring four plural categories cannot be saved with two, tested with a real one. A locale requiring six is supported. The source language having fewer categories than the target does not prevent a complete translation. The correct form is selected at runtime for a range of numbers per locale, verified against expected outputs. Do not build yet: No staleness or delivery yet. ### Phase 3 · A changed source invalidates its translations Build: Version source strings. When a source changes, mark every translation made from the previous version as stale. Distinguish a meaning change from a typo fix, and let the editor say which. Trap: Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. Done when: Editing a source string marks translations made from the previous version as stale. A change marked as not affecting meaning does not invalidate, and records who marked it. Stale translations are still served rather than falling back to the source, with the staleness visible to translators. The count of stale strings per locale is reported. Do not build yet: No delivery or usage tracking yet. ### Phase 4 · Context, because a word is not a sentence Build: Attach a description, a screenshot and a maximum length to keys. Surface which keys are used where in the codebase. Flag translations exceeding the length constraint. Trap: A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. Done when: A key can carry a description, a screenshot and a maximum length. Keys used in code are linked to their locations and unused keys are surfaced. A translation exceeding the length constraint is flagged before publishing. Keys with no context at all are reported as incomplete. Do not build yet: No delivery or bundles yet. ### Phase 5 · Shipping translations without shipping code Build: Publish immutable versioned bundles per locale. Clients fetch a version and get a consistent set. Handle a missing translation with a documented fallback chain rather than an empty string. Trap: Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions. Done when: A client fetching a bundle version gets a consistent set, verified by publishing mid-fetch. Bundles are immutable once published. A missing translation follows a documented fallback chain and never renders as an empty string. Rolling back to a previous bundle version is possible and immediate. Do not build yet: Nothing after this. ### Out of scope (and why) - Machine translation quality, which is a model question rather than a workflow one and where the useful part is post-editing. - Translator marketplace and vendor management, which is most of what the hosted services actually provide. - Right-to-left layout handling, which is a client-side concern and a substantial one. - Translation memory and terminology at professional scale, which is a specialised field with mature tools. ### README must contain - That placeholders are named rather than positional, because many languages reorder - That plural categories are per locale, since several languages need four to six forms rather than two - That a source edit marks translations stale, and a typo fix can be marked as not affecting meaning - That stale translations are still served, because a stale translation beats an untranslated string - That bundles are immutable and versioned so a client is never half updated
# Architecture · Tolgee Cloud ## Stack | Part | Choice | Why | | --- | --- | --- | | Format | Messages stored in a standard format with named placeholders | positional placeholders reorder wrongly in languages with different word order | | Plurals | Full plural category support per language | English has two forms, several languages have four to six, and picking two breaks them | | Staleness | Translations invalidated when their source changes | this is the failure that reaches production most often | | Delivery | Versioned bundles per locale, fetched at build or runtime | a translation change should not require a deployment, but it should be atomic | | Runtime | TypeScript with PostgreSQL | keys, versions and staleness are relational | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Messages | Named placeholders and validation. | Any format, provided placeholders are named rather than positional. | | Plurals | Per-locale category requirements. | Any implementation, provided categories come from locale data rather than assumption. | | Staleness | Source versioning and invalidation. | Any mechanism, provided a typo fix can be excluded from invalidation. | | Context | Descriptions, screenshots and constraints. | Any store. A key without context is an incomplete key. | | Bundles | Immutable versioned delivery. | Any transport, provided a client cannot fetch a half-updated set. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Keys, translations and version history. - `SOURCE_LOCALE` · required · The source of truth. Edits to it mark other locales stale. - `FALLBACK_CHAIN` · required · Documented, and never resulting in an empty string. - `SERVE_STALE_TRANSLATIONS` · required · A stale translation beats an English string in a French interface. - `BUNDLE_CDN_URL` · required · Where immutable bundles are served from. - `REQUIRE_KEY_CONTEXT` · required · Whether a context-free key blocks or warns. A bare word is ambiguous. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Tolgee Cloud product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Messages stored in a standard format with named placeholders, Full plural category support per language, Translations invalidated when their source changes, Versioned bundles per locale, fetched at build or runtime, TypeScript with PostgreSQL. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. - Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions.
# Delivery milestones · Tolgee Cloud Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Messages that survive being translated A string can be translated into a language that orders words differently. ### Steps 1. Use named placeholders throughout Positional ones swap values in languages that reorder. Files: `src/messages.ts` 2. Validate the placeholder set matches A pure function, unit tested with a dropped and an invented placeholder. Files: `src/validate.ts`, `src/validate.test.ts` 3. Reject rather than warn A missing placeholder is a runtime error in someone's language. Files: `src/translations.ts` ### Done when - [ ] A dropped placeholder is rejected by name - [ ] An invented placeholder is rejected - [ ] Placeholders are named throughout - [ ] Reordering in a translation does not break ### Watch out - Positional placeholders assume the target language keeps the same word order, which many do not, so a sentence with two substitutions comes out with the values swapped and nobody testing in English ever sees it. Use named placeholders and validate the set matches between source and translation. ## M2 · Plurals in languages that are not English Every locale gets exactly the plural forms its language requires. ### Steps 1. Store plural categories per locale From the standard data, not from an assumption about English. Files: `src/locales.ts` 2. Require every category the locale uses Two forms is an English answer to a general question. Files: `src/plurals.ts`, `src/plurals.test.ts` 3. Select the right form at runtime Tested across a range of numbers per locale, against expected output. Files: `src/select.ts` ### Done when - [ ] A four-category locale cannot be saved with two - [ ] A six-category locale is supported - [ ] A source with fewer categories still translates fully - [ ] Runtime selection matches expected output per locale ### Watch out - Designing the plural model around English gives you two forms, and then Arabic needs six and Russian and Polish need four, so the translations for those languages are either wrong or impossible to express. Model the categories per locale from the outset, since retrofitting means revisiting every plural string in every language. ## M3 · A changed source invalidates its translations Editing English marks the other twenty languages as stale. ### Steps 1. Version sources and record the version per translation The mechanism the whole phase rests on. Files: `src/versions.ts` 2. Distinguish meaning changes from typo fixes Invalidating on every edit gets the feature turned off. Files: `src/edits.ts` 3. Keep serving stale translations A stale translation beats an English string in a French interface. Files: `src/serve.ts` ### Done when - [ ] A source edit marks translations stale - [ ] A typo fix does not invalidate, and records who said so - [ ] Stale translations are still served - [ ] Stale counts are reported per locale ### Watch out - Treating every source edit as invalidating everything means fixing a typo sends twenty languages back for retranslation, and the team turns the feature off within a month. Let the editor mark a change as not affecting meaning, record who said so, and keep the invalidation for changes that actually alter what the string says. ## M4 · Context, because a word is not a sentence A translator can see where a string appears and how much room it has. ### Steps 1. Attach context to the key A bare word is ambiguous in ways English speakers do not notice. Files: `src/keys.ts` 2. Link keys to code locations So unused keys can be removed and context can be found. Files: `src/usage.ts` 3. Enforce length constraints German is longer. The button is not. Files: `src/constraints.ts` ### Done when - [ ] Keys carry description, screenshot and length - [ ] Code usage is linked and unused keys surfaced - [ ] Over-length translations are flagged - [ ] Context-free keys are reported ### Watch out - A translator handed a bare word with no context cannot tell whether it is a noun or a verb, and English is full of words that are both, which produces translations that are grammatically correct and completely wrong. Attach context to the key, and treat a key without any as an incomplete key rather than a normal one. ## M5 · Shipping translations without shipping code (production only) A locale updates atomically and a client never sees a half-updated bundle. ### Steps 1. Publish immutable versioned bundles So a client is never half updated. Files: `src/bundles.ts` 2. Document the fallback chain Regional to base to source. Never an empty string. Files: `src/fallback.ts` 3. Make rollback immediate A bad translation shipped is fixed by pointing at the previous version. Files: `src/publish.ts` ### Done when - [ ] A mid-publish fetch gets a consistent set - [ ] Bundles are immutable once published - [ ] Missing translations follow a documented fallback - [ ] Rollback to a previous version is immediate ### Watch out - Serving translations key by key from a live database means a client can fetch half its strings before a publish and half after, producing an interface in two states, and the bug is unreproducible. Publish immutable bundles and let clients move between whole versions.
# Operations · Tolgee Cloud ## Backup Translations represent paid human work and are expensive to recreate. Back up keys, translations and their source versions together, since a translation without its source version cannot have its staleness computed. ## Restore Restore and confirm source versions survived alongside translations, since losing them makes every translation's staleness unknown and the only safe assumption is that all of them are stale. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Stale count per locale and the proportion of keys with no context. The first is the number that predicts a wrong string in production, and the second predicts a wrong translation being written in the first place. ## Incident checklist An interface showing a mixture of two translation versions means bundles are not being served atomically. A wrong translation in production that reads fine usually means a source string changed and the staleness was marked as a typo fix when it was not. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A dropped placeholder is confirmed rejected by name - [ ] A four-category locale is confirmed unable to save with two forms - [ ] A source edit is confirmed to mark translations stale - [ ] Stale translations are confirmed still served - [ ] A mid-publish fetch is confirmed to get a consistent set ## Launch constraint Do not market omitted Tolgee Cloud capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Keys, translations and version history. DATABASE_URL=postgres://localhost:5432/i18n # Required. The source of truth. Edits to it mark other locales stale. SOURCE_LOCALE=en # Required. Documented, and never resulting in an empty string. FALLBACK_CHAIN=regional,base,source # Required. A stale translation beats an English string in a French interface. SERVE_STALE_TRANSLATIONS=always # Required. Where immutable bundles are served from. BUNDLE_CDN_URL=https://i18n.example.com/bundles # Required. Whether a context-free key blocks or warns. A bare word is ambiguous. REQUIRE_KEY_CONTEXT=warn
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Tolgee Cloud because localization teams pay because language context, review discipline, translators, and release integrations are the hard parts. The recurring cost buys file formats, placeholders, plural rules, screenshots, context, permissions, review, glossary, machine translation, sync, and backups, not just the visible interface.
xmanaged hosting, backups, collaboration, machine translation, and support
xprofessional translator marketplace
xadvanced translation memory
xwebsite proxy network
xenterprise workflows and integrations
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Tolgee Cloud pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 500 keys; 3 seats; 10,000 MT credits; unlimited projects |
| team | — | $56.52/workspace | 2,000 keys; 4 seats; 10,000 MT credits |
| business | — | $206.46/workspace | 5,000 keys; 8 seats; 1,000,000 MT credits |
| advanced | — | $575.55/workspace | 20,000 keys; 20 seats; 1,000,000 MT credits |
| enterprise | custom | — | Negotiable keys, seats and MT credits |
| self-hosted enterprise | custom | — | Custom seats/features and support for on-premises deployment |
free tier500 keys, 3 seats, 10,000 MT credits and unlimited projects
billingmonthly + annual (2 months free); only annual-billing prices were exposed in readable page text
hidden costsMonthly overages apply even to annual subscribers: each extra 1,000 keys is €5/mo; extra seats are €8/€14/€22 per month on Team/Business/Advanced; each extra 1,000 MT credits is €0.35.
verified 2026-08-14 · source ↗
Vibecode Tolgee Cloud
Kinda. The core of Tolgee Cloud is buildable in a weekend with the prompt on this page, but there are real gaps: managed hosting, backups, collaboration, machine translation, and support, professional translator marketplace. Read the honest list above before committing.
How much does Tolgee Cloud cost?
Tolgee Cloud costs about $56.52/month (Team, checked 2026-08-14), which is $678.24 per year.
What do I lose by replacing Tolgee Cloud?
Honestly: managed hosting, backups, collaboration, machine translation, and support; professional translator marketplace; advanced translation memory; website proxy network; enterprise workflows and integrations. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Tolgee Cloud?
Yes: Weblate (A mature alternative localization workspace with Git sync, memory and review, traded for your own operations.) Tolgee (The same core product without the cloud bill; you become the person who notices failed backups.) The prompt is for when you want it exactly your way.