---
name: "localization_workflow"
description: "Localize a product beyond its strings: externalize copy, handle CLDR plural and select rules per language with ICU MessageFormat, format dates, numbers and currency by locale, check right-to-left layout, write translator context notes, run pseudo-localization, and pass a locale QA check that catches truncation and encoding bugs before users do. Use when adding a language, preparing strings for translation, reviewing a localized build, or fixing plural, format or RTL bugs. Ships catalog, plural and format checkers plus a worked pass over a small app in en-US, de-DE and ar-EG."
---

# Purpose

Translate the product, not just the words in it. Most localization bugs are not bad translations. They are strings built in code, plurals that assume English, dates and prices assembled by hand, layouts that clip longer text, and right-to-left screens that were never mirrored. This workflow fixes the code and the process in that order, then proves each locale with checkers and a read-through on a real build.

Tooling in this skill: `scripts/check_catalog.py` checks a target catalog against the source for missing keys, placeholder drift, missing CLDR plural categories and encoding damage. `scripts/format_preview.py` prints the real date, number, currency and plural category for a locale using Babel and CLDR data. `scripts/pseudo_localize.py` builds a test locale that is about 40 percent longer with accented letters and brackets. A full worked pass is in `references/worked-example-three-locales.md`.

# Workflow

## 1. Externalize every user visible string

- Move all copy out of code into a catalog keyed by meaning, for example `cart.count` or `checkout.title`, one catalog per locale. Hardcoded text in components, emails, push notifications, error messages and store listings counts. If it reaches a user, it goes in the catalog.
- Keep one whole sentence in one key. Never concatenate fragments, never build a sentence from a prefix key plus a link key plus a suffix key. Word order differs by language, so a split sentence cannot be translated correctly in every locale.
- Use named placeholders, for example `{name}` and `{count}`. Never rely on position alone. Two placeholders of the same type in one string need two names.
- Give the same English word its own key when context changes its meaning. A heading noun and a button verb, or a verb and a status label, translate differently in many languages. Add a context note or a gettext msgctxt value so the split survives extraction.
- Set a fallback rule and write it down: a missing key falls back to the source locale and gets logged. A raw key name on screen is a bug, not a fallback.

## 2. Write plurals and selection the CLDR way

- Use ICU MessageFormat plural for any string with a count. CLDR defines six categories: zero, one, two, few, many, other. Each language uses a subset, and `other` is always required. English uses one and other, Japanese uses other only, Arabic uses all six, Polish and Russian use one, few, many and other. Full table in `references/cldr-plurals-and-formats.md`.
- Write full sentences inside each plural branch. Translators add the branches their language needs. An exact match such as `=0` for an empty state is fine, and it does not remove the category branches a target language requires.
- Never write `item(s)`, never add an s in code, and never pick a string with `count == 1` in app logic. The rule for 1 differs by language, and in some languages the `one` category also covers 21, 31 and fractions.
- Use select for categorical values, and nest plural inside select when both apply. Put select on the outside, as ICU guidance recommends.
- For gendered languages, do not bolt a pronoun or an adjective ending onto a shared fragment. Either reword the source to avoid the gendered form, or give the translator a select with full sentences and a context note saying who the variable refers to.

## 3. Format values by locale, never by hand

- Format dates, times, numbers, currency, percentages, units and lists with a locale formatter: Intl in JavaScript, ICU or Babel elsewhere, the platform formatter on mobile. Delete helpers that join day, month and year, insert thousand separators, or place a currency symbol in code.
- Check the difference before you trust it: 1234.56 renders as 1,234.56 in en-US, 1.234,56 in de-DE and 1 234,56 in pl-PL. The date 2026-03-08 renders as Mar 8, 2026 in en-US and 08.03.2026 in de-DE. Run `python3 scripts/format_preview.py en-US de-DE ar-EG` for the locales you ship.
- Pass a currency code with every amount. Locale says how to format, the code says what the money is. Symbol placement and fraction digits follow the pair.
- Pass an explicit time zone for any time a user compares across places, such as an order, a meeting or a deadline. Store dates and amounts in neutral form and format only when displaying.
- Sort user visible lists with a locale collator, not by code point order.

## 4. Hand off with context, not just strings

- Fill one context entry per key using `references/translator-context-template.md`: screen, screenshot if available, max length, tone and form of address, every placeholder named with an example value, plural notes, terms that stay untranslated, and what the user just did.
- Write the per locale header once: locale with region (de-DE, not just German), form of address, glossary, and who answers translator questions.
- Mark legal, payment, medical and safety strings for review by a second person in the locale.
- Keep translator answers next to the key in the file. The next release should not ask the same question twice.

## 5. Test with pseudo-localization before translation arrives

- Build the pseudo locale: `python3 scripts/pseudo_localize.py en.json pseudo.json` (default factor 1.4). Plain strings get accented letters, padding and brackets. Text inside balanced braces stays intact, so placeholders and ICU structure survive.
- Run the app in the pseudo locale at the smallest supported screen size. Fix every clipped bracket, overlap and wrap problem you find. A string that stayed plain and short while everything around it grew is probably hardcoded.
- Treat the factor as a test setting, not a fact about a language. Real length checks happen again with real translations in step 7.

## 6. Check right-to-left layout for RTL locales

- Set direction at the document or view level for locales such as Arabic, Hebrew, Persian and Urdu. Use CSS logical properties (start, end, inline-start, inline-end) so one stylesheet serves both directions. Full list in `references/rtl-and-qa-checklist.md`.
- Mirror navigation, back actions, progress and column order. Decide per icon what does not mirror, such as media controls and logos, and write the decision down. Do not apply a global flip transform.
- Test mixed direction text on purpose: a Latin script name inside an Arabic sentence, a price inside a Hebrew sentence, a quoted English title. Wrap embedded opposite direction text with a dir attribute, and watch where commas and parentheses land.

## 7. Run the locale QA pass and sign off

- Run the catalog checker for every target locale and require zero failures: `python3 scripts/check_catalog.py en.json ar.json ar-EG`. It catches missing and extra keys, renamed placeholders, missing plural categories and common encoding damage such as the replacement character.
- Compare one date, one time, one number, one price and one percentage in the app against `format_preview.py` output for the same values.
- Exercise counts 0, 1, 2, 3, 5, 11, 21, 25, 100 and a fraction if the product shows fractions. Read one full sentence per plural branch.
- Read every screen in the locale at the smallest width. Look for truncation, overlap, text in the source language, and raw key names.
- Record the sign off per locale: build, checker result, counts tested, screens read, RTL items checked, open issues with key and screen, reviewer and date. The checklist and sign off form are in `references/rtl-and-qa-checklist.md`.

# Output Contract

When localizing a product or reviewing a localized build, produce:

1. The catalogs: one per locale, whole sentences per key, named placeholders, ICU plural and select where counts or categories appear.
2. The context handoff: one filled template entry per key plus the per locale header, saved where translators will work from it.
3. Checker output per locale: catalog checker result, format preview comparison, and the pseudo locale findings.
4. A QA sign off record per locale with counts tested, screens read, RTL checks and open issues. An issue names the key, the locale, the screen and what the user sees.
5. A short list of code fixes made outside the catalogs: strings externalized, hand built formats deleted, layout or direction fixes.

# Operating Rules

1. Every factual claim about a language rule, a format or a category comes from CLDR, ICU, MDN, W3C or the platform docs. If you cannot verify it, look the locale up in the CLDR plural rules chart or run the format preview tool. Do not guess a plural subset from a related language.
2. Never ship a locale on a catalog check alone. Catalog pass plus format comparison plus a read-through on a real build is the minimum bar.
3. Never split a sentence across keys and never concatenate translated fragments in code. If a design needs a link or a bold word inside a sentence, the whole sentence stays one string with markup inside it.
4. Fix layout bugs in the layout, not by shortening the translation. A translator asked to squeeze text into an English sized button will cut meaning.
5. Keep source copy plain and short where you can. Short source strings expand less, translate faster and leave room in the layout.
6. Do not machine translate and ship without a reviewer who reads the locale. Machine output is a draft for review, flagged as such in the handoff.
