Localization label management

Strings are data,
not files.

Every string lives in one database, where translators work without touching code. Approved work freezes into immutable releases you promote and roll back in one step.

How it works

In private development. No card, no trial clock, no sales call.

checkout.cart.item_count

Source

{count, plural,one {# item in your cart}other {# items in your cart}}
Item count

Rendered

  • en-GB English other

    0 items in your cart

  • en-GB English one

    1 item in your cart

  • en-GB English other

    5 items in your cart

  • de-DE Deutsch other

    0 Produkte in Ihrem Warenkorb

  • de-DE Deutsch one

    1 Produkt in Ihrem Warenkorb

  • de-DE Deutsch other

    5 Produkte in Ihrem Warenkorb

  • ja-JP 日本語 other

    カートに商品が0点あります

  • ja-JP 日本語 other

    カートに商品が1点あります

  • ja-JP 日本語 other

    カートに商品が5点あります

  • ar-EG العربية zero

    لا توجد منتجات في سلتك

  • ar-EG العربية one

    منتج واحد في سلتك

  • ar-EG العربية few

    ٥ منتجات في سلتك

  • pt-BR Português one

    0 item no seu carrinho

  • pt-BR Português one

    1 item no seu carrinho

  • pt-BR Português other

    5 itens no seu carrinho

Plural categories come from CLDR, not from a guess in the view layer.

How it works

Today

How most teams do this now

Most product teams keep their strings in locale files in the repository and move them between developers and translators by hand. None of that is unreasonable — it is what the tooling makes easy. It stops being reasonable at the moment somebody needs a straight answer about production.

  • Files drift apart

    en.json moves on main while de.json moves on a feature branch, and the two only meet at a merge nobody volunteered to review.

  • The spreadsheet round trip

    Strings go out as a sheet and come back as a sheet. The paste is the integration, and it has no tests.

  • A brace goes missing

    One malformed plural ships, and the string that breaks is in a language nobody on the team reads well enough to notice.

  • Nobody can say what is live

    The answer is somewhere between a git tag, a build number and a deploy log, and reconstructing it takes longer than the fix would have.

  • Rollback means a release

    Changing one word in production means a new build. On mobile it means the app store, and the app store means days.

your-app/src/locales

  • en.json
  • de.json edited on two branches
  • ja.json
  • ar.json

four files, four owners

exported
pasted back

strings-for-translation.xlsx

emailed out, emailed back

Drift Three keys added since the export, and one file edited on two branches. Nothing here knows about the others.

A repository holding four locale files, with strings exported to a spreadsheet and pasted back by hand. Three keys have been added since the export and one file has been edited on two branches; the gap between the repository and the spreadsheet is marked as drift.

How it works

Six states, and nothing between them

A string moves through the same sequence every time, and its position is always something you can look up rather than something you have to reconstruct from a chat thread.

Work

Keys, translations and approvals accumulate on a branch

  • main
  • feature/checkout-v2
  • Developer Key added
  • Translator Locales filled
  • Reviewer Approved

Freeze

The approved set is sealed into a numbered release

Release 47

Frozen 8c41f0a

2,481 keys

The snapshot is content-addressed and immutable. Two builds from the same release produce byte-identical bundles.

Ship

Environments point at releases, and bundles follow

Environments

  • Development 47
  • Staging 47
  • Production 45 44

CDN Cached immutably by version

  • React @lexiolab/react 45 44
  • Flutter lexiolab_flutter 45 44

No build, no redeploy, no app store. The environment points at a different release and the CDN serves it.

Three stages left to right. Work: keys, translations and approvals accumulate on a feature branch alongside main. Freeze: the approved set is sealed as Release 47, with a content hash. Ship: development and staging point at release 47 while production is still on release 45, and bundles are delivered over a CDN to a React application and a Flutter application.

Branches

A merge that cannot conflict in a language you do not read

A branch holds two things: keys that do not exist yet, and proposed changes to the English source. Translations attach to the key itself rather than to the branch, so they are never copied and never merged. That leaves exactly two things that can collide — two people creating the same key name, or two people rewording the same English string — and one person reading English can settle both.

The common approach

The branch copies every language

Each locale file is duplicated onto the branch and edited there. Merging means reconciling every file that moved on either side.

branch carries endejaar

3 conflicts In German, Japanese and Arabic. Each one needs somebody who reads that language before the branch can land.

LexioLab

The branch carries keys and English

New keys and edits to the source string live on the branch. Translations stay attached to the key, on the far side of the merge, untouched.

branch carries +2 keys1 en edit

1 conflict In English. Anyone on the team can read it, decide, and move on.

Two branch models side by side. In the common approach the branch copies all four locale files and the merge produces three conflicts, in German, Japanese and Arabic. In LexioLab the branch carries two new keys and one English edit, translations remain attached to the keys, and the merge produces one conflict, in English.

Capabilities

What makes this different from a folder of JSON

Most localization tooling is a nicer editor for the same fragile files. This is a different data model, and the difference shows up on the day something goes wrong in production.

Segments, so a screen loads only its own strings

Keys belong to a segment that maps to the i18n namespace you already use, and a bundle is requested by segment. The checkout screen never carries the strings for settings and onboarding.

checkout · 214 keys settings · 96 onboarding · 71

ICU stored canonically, validated on write

Plurals, select and gender are parsed and held as structure rather than as a string that happens to compile. A message that would not compile is refused where it is written.

Placeholder parity, enforced

A translation that drops {count}, or invents one the source never had, cannot be approved. The check runs on save rather than at build time, when whoever could fix it has moved on.

Soft delete, with purge eligibility

A removed key stops appearing in new releases and keeps answering for the builds already in the field. It becomes eligible to purge once no supported release still refers to it.

Approval policy per language, per environment

Require review in the three languages your largest markets read, and let the rest land unblocked. It is configuration on the environment, not a convention somebody has to remember.

A full audit trail

Every edit, approval, merge and promotion is recorded with its author and its time. When a wrong string reaches production, how it got there is a question with an answer.

Every language in CLDR, added by you

Choose a language and its direction, plural categories and number formatting arrive with it. Adding Welsh does not require that we anticipated Welsh, and there is no ticket to raise.

cy-GB ltr zero · one · two · few · many · other 6 forms

Per-language fonts, delivered with the bundle

Set a face per language, including one you upload, and it ships beside the strings that need it. A script that needs its own font gets it without a second deployment.

Import with a dry run

react-i18next JSON, Flutter ARB and CSV, with a dry run that reports what would be created, merged and skipped. Nothing is written until you have read that report.

Roles

Built for four different people

A developer, a translator, a reviewer and an administrator each need a different thing from the same set of strings, and giving all four the same screen is how localization tools end up serving none of them well. What follows is the actual interface, rebuilt at reduced fidelity.

Add the key where you already are

Keys are added on a branch with a description and a screen reference, and pulled as a bundle in CI. You do not open a spreadsheet, and you do not chase anybody for a string.

Key detail

checkout cart.item_count feat/checkout-v2

Description

Cart summary. The number of items a customer has added, shown above the total.

Source · en-GB

{count, plural, one {# item} other {# items}

Screen

Checkout / Cart

Coverage

  • en-GB Complete
  • de-DE Complete
  • ar-EG Complete
  • ja-JP In review
  • pt-BR In review
  • tr-TR Missing
A key detail panel: the segment, key and branch across the top, then the key’s description, the ICU source message with the count argument and the hash placeholder marked as chips, the screen it appears on, and a strip of six locales showing which are complete, which await review and which are missing.

A form, not a syntax

The source string, the context it appears in, and one field for each plural form the language actually has. There is no brace to balance, because there is never a brace.

Plural forms — Arabic

العربية Arabic

English

1 item · 3 items

  • zero 0 لا توجد عناصر
  • one 1 عنصر واحد
  • two 2 عنصران
  • few 3–10 count عناصر
  • many 11–99 count عنصرًا
  • other 100, 1.5 count عنصر

Arabic has six plural categories in CLDR and English has two. LexioLab asks each language for the number of fields it needs, and assembles the ICU message itself.

The Arabic plural editor: the English source at the top, then six labelled fields — zero, one, two, few, many and other — each showing the counts it applies to and a right-to-left Arabic value. The count placeholder sits inside the fields as a chip rather than as syntax.

Read both, then decide

Source and translation sit side by side with the placeholder and ICU checks already run. Approval is a judgement about meaning rather than an inspection for broken syntax.

Review — German

checkout cart.item_count feat/checkout-v2

English · source

count items

German · de-DE

count Artikel

Checks

  • Placeholders match — Passes
  • Message compiles — Passes
  • All plural forms filled — Passes

Approve

A review panel: the segment, key and branch across the top, the English source on the left and the German translation on the right, and three checks below them, all passing — placeholders match, the message compiles, and every plural form is filled.

Decide what is live

Environments point at releases, and promotion moves the pointer. The policy governing a promotion is configuration you can read, not a convention held by whoever set it up.

Environments

Environments

  • Development Release 47 Current
  • Staging Release 47 Current
  • Production Release 45 2 releases behind

Approval policy

Production requires approval in en-GB, de-DE and ja-JP

Promote 47 to production

An environment board of three rows: development and staging both on release 47, and production on release 45, two releases behind. Below it the approval policy for production, and a control that promotes release 47.
Metal letterpress sorts at a shallow angle, filling the frame: rows of letterforms in relief, the nearest sharp and the rest falling away.

The database is the source of truth. Bundles are build artifacts. Releases are immutable.

Integration

Two SDKs and a CLI that behaves in CI

Fetch a release at build time or at runtime. Ship a bundled fallback so a cold start with no network still renders real copy rather than raw keys.

React

app/checkout/CartSummary.tsx
import { LexioProvider, useTranslation } from '@lexiolab/react';
import bundled from './l10n/en-GB.json';

export const App = () => (
  <LexioProvider project="storefront" env="prod" fallback={bundled}>
    <CartSummary count={3} />
  </LexioProvider>
);

function CartSummary({ count }: { count: number }) {
  // The segment is the namespace; plurals resolve against the active locale.
  const { t } = useTranslation('checkout');
  return <p>{t('cart.item_count', { count })}</p>;
}

Flutter

lib/main.dart
void main() => runApp(MaterialApp(
      localizationsDelegates: [
        // Ships inside the binary: a cold start with no network renders real
        // copy rather than raw keys.
        LexioDelegate(project: 'storefront', env: Env.prod, fallback: bundledLabels),
        ...GlobalMaterialLocalizations.delegates,
      ],
      supportedLocales: Lexio.supportedLocales,
      home: const CartSummary(),
    ));

// In build(), resolved against the active locale's CLDR plural rules.
Text(context.t('checkout.cart.item_count', {'count': items.length}));

CLI

CI · build step
$ ll pull --env prod --out lib/l10n
  storefront · release 47 · 6 locales · 2,481 keys
  wrote lib/l10n/{en-GB,de-DE,ar-EG,ja-JP,pt-BR,tr-TR}.json

$ ll verify --require-complete en-GB,de-DE,ja-JP
  2,481 keys · 3 required locales
  0 missing · 0 ICU errors · 0 placeholder mismatches
  • Bundles are served from a CDN and cached immutably by version.
  • A build-time snapshot ships inside the binary, so a cold start with no network renders real copy.
  • The SDK checks a version on launch. It does not make a request per string.
  • A rollback reaches clients without an app release — the environment points at a different release.

Pricing

You are not billed for your translators

Most localization platforms meter the people doing the translating, and that is the line item teams complain about — it is what turns a translation workflow back into a spreadsheet somebody emails around. Translator and reviewer seats here are unlimited and free, and so are projects, languages, keys and releases.

Included

Translators and reviewers are free

Unlimited translator and reviewer seats, at no cost and with no ceiling. Metering the people who do the translating is what pushes a team back into a spreadsheet, which is the workflow this product exists to replace.

  • Unlimited translator seats
  • Unlimited reviewer seats
  • Unlimited projects, languages and keys
  • Unlimited releases and full history

Billed per seat

Admin and developer seats

The people who configure environments, set approval policy and cut releases. Everyone else works for free, and nothing you store is counted or capped.

  • Admin seats — environments, policy, promotion
  • Developer seats — keys, branches, SDK and CLI access
  • Every capability on this page, on every seat

A billable seat is a person who can change what ships: add or rename keys, edit approval policy, or promote a release to an environment. Reading, translating and reviewing are free, and so is every project, language and key you keep.

What LexioLab bills for and what it does not
Admin seats Per seat
Developer seats Per seat
Translator seats Unlimited, free
Reviewer seats Unlimited, free
Projects Unlimited
Languages and keys Unlimited
Releases and history Unlimited

Questions

The things engineering leads ask first

Including the ones with answers you may not like. This is a pre-release product and we would rather you find that out here than in a call.

How do we migrate what we already have?

Import your existing files — react-i18next JSON, Flutter ARB or CSV — with a dry run that reports what would be created, merged and skipped before anything is written. Those files then stop being authoritative: the bundles your build produces are generated artifacts, and editing one by hand is a change the next build discards.

What happens to our app if LexioLab is down?

Nothing. Both SDKs take a bundled fallback that ships inside your binary, so an app that starts cold with no network renders real copy rather than raw keys. A fetched release replaces it when one arrives, and released bundles are served from a CDN and cached immutably by version rather than fetched from us on demand.

Can we get our data out?

Yes, in the same formats you can import: nested and flat JSON, Flutter ARB and XLIFF 2.0, for any release or for the current state of any branch. Export exists so that leaving is possible. A localization tool you cannot get your data out of is a liability rather than a platform, and we would rather say that plainly than be asked.

Do translators need paid seats?

No. Translator and reviewer seats are unlimited and free. Only admin and developer seats are billed — the people who can change what ships — and there is no cap on projects, languages, keys or releases.

How do plurals and right-to-left languages work?

From CLDR, not from judgement. A translator working in Arabic is shown six labelled fields because Arabic has six plural categories; Japanese has one. Direction comes with the language, previews render right-to-left properly, and LexioLab assembles the ICU message itself — so a translator is never handed the syntax that would let them break it.

What happens when we roll back — do users need to update the app?

No. An environment points at a release, and rolling back repoints it at the previous one. The CDN serves that version under its own immutable URL and clients pick it up on their next version check. No build, no redeploy, no app store review.

Who can see and edit which languages?

Access is granted per language, so a translator working in German sees German. Approval is configured per language per environment, which is how you require review in the three languages your largest markets read while letting the rest land unblocked. Every change, approval and promotion is recorded with its author.

Does this replace react-i18next, or work with it?

Either. The bundles LexioLab generates are ordinary JSON in the shape react-i18next already expects, so you can keep your existing setup and change only where the files come from. The React SDK replaces that layer if you would rather it did, and segments map to the namespaces you are already using.

Request access

Tell us what you are building and where localization currently costs you time. Concrete answers are more use to us than polite ones.

Optional, but it is the field we read first.

What you send here is used to decide who to onboard next. Nothing else, and nowhere else.

Sending your request