localization-i18n
Use this skill when working with internationalization (i18n), localization (l10n), translation workflows, right-to-left (RTL) layout support, pluralization rules, or ICU MessageFormat syntax. Triggers on translating strings, setting up i18n libraries (react-intl, i18next, FormatJS), handling plural forms, formatting dates/numbers/currencies for locales, building translation pipelines, configuring RTL stylesheets, or writing ICU message patterns with select/plural/selectordinal.
engineering i18nl10nlocalizationtranslationrtlicu-message-formatWhat is localization-i18n?
Use this skill when working with internationalization (i18n), localization (l10n), translation workflows, right-to-left (RTL) layout support, pluralization rules, or ICU MessageFormat syntax. Triggers on translating strings, setting up i18n libraries (react-intl, i18next, FormatJS), handling plural forms, formatting dates/numbers/currencies for locales, building translation pipelines, configuring RTL stylesheets, or writing ICU message patterns with select/plural/selectordinal.
localization-i18n
localization-i18n is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Working with internationalization (i18n), localization (l10n), translation workflows, right-to-left (RTL) layout support, pluralization rules, or ICU MessageFormat syntax.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill localization-i18n- The localization-i18n skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Internationalization (i18n) is the process of designing software so it can be adapted to different languages and regions without engineering changes. Localization (l10n) is the actual adaptation - translating strings, formatting dates and currencies, supporting right-to-left scripts, and handling pluralization rules that vary wildly across languages. This skill gives an agent the knowledge to set up i18n infrastructure, write correct ICU MessageFormat patterns, handle RTL layouts, manage translation workflows, and avoid the common traps that cause garbled UIs in non-English locales.
Tags
i18n l10n localization translation rtl icu-message-format
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair localization-i18n with these complementary skills:
Frequently Asked Questions
What is localization-i18n?
Use this skill when working with internationalization (i18n), localization (l10n), translation workflows, right-to-left (RTL) layout support, pluralization rules, or ICU MessageFormat syntax. Triggers on translating strings, setting up i18n libraries (react-intl, i18next, FormatJS), handling plural forms, formatting dates/numbers/currencies for locales, building translation pipelines, configuring RTL stylesheets, or writing ICU message patterns with select/plural/selectordinal.
How do I install localization-i18n?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill localization-i18n in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support localization-i18n?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Localization & Internationalization (i18n)
Internationalization (i18n) is the process of designing software so it can be adapted to different languages and regions without engineering changes. Localization (l10n) is the actual adaptation - translating strings, formatting dates and currencies, supporting right-to-left scripts, and handling pluralization rules that vary wildly across languages. This skill gives an agent the knowledge to set up i18n infrastructure, write correct ICU MessageFormat patterns, handle RTL layouts, manage translation workflows, and avoid the common traps that cause garbled UIs in non-English locales.
When to use this skill
Trigger this skill when the user:
- Wants to add i18n/l10n support to a web or mobile application
- Needs to write or debug ICU MessageFormat strings (plural, select, selectordinal)
- Asks about handling right-to-left (RTL) languages like Arabic or Hebrew
- Wants to set up a translation workflow or integrate a TMS (translation management system)
- Needs to format dates, numbers, or currencies for specific locales
- Asks about pluralization rules for different languages
- Wants to configure i18n libraries like react-intl, i18next, FormatJS, or vue-i18n
- Needs to extract translatable strings from source code
Do NOT trigger this skill for:
- General string manipulation unrelated to translations
- Timezone handling without a localization context (use a datetime skill instead)
Key principles
Never concatenate translated strings - String concatenation breaks in languages with different word order. Use ICU MessageFormat placeholders instead:
"Hello, {name}"not"Hello, " + name. This is the single most common i18n bug.Externalize all user-facing strings from day one - Retrofitting i18n is 10x harder than building it in. Every user-visible string belongs in a message catalog, never hardcoded in source. Even if you only ship English today.
Design for text expansion - German text is 30-35% longer than English. Japanese can be shorter. UI layouts must accommodate expansion without clipping or overflow. Use flexible containers, never fixed widths on text elements.
Locale is not language -
en-USanden-GBare the same language but format dates, currencies, and numbers differently. Always use full BCP 47 locale tags (language-region), not just language codes.Pluralization is not just singular/plural - English has 2 plural forms. Arabic has
- Polish has 4. Russian has 3. Always use CLDR plural rules through ICU MessageFormat
rather than
count === 1 ? "item" : "items"conditionals.
- Polish has 4. Russian has 3. Always use CLDR plural rules through ICU MessageFormat
rather than
Core concepts
ICU MessageFormat is the industry standard for parameterized, translatable strings.
It handles interpolation, pluralization, gender selection, and number/date formatting
in a single syntax. The key constructs are {variable} for simple interpolation,
{count, plural, ...} for plurals, {gender, select, ...} for gender/category
selection, and {count, selectordinal, ...} for ordinal numbers ("1st", "2nd", "3rd").
See references/icu-message-format.md for the full syntax guide.
CLDR Plural Rules define how languages categorize numbers into plural forms. The
Unicode CLDR defines six categories: zero, one, two, few, many, other.
English uses only one and other. Arabic uses all six. Every plural ICU message
must include the other category as a fallback. See references/pluralization.md.
RTL (Right-to-Left) layout affects Arabic, Hebrew, Persian, and Urdu scripts. RTL
is not just mirroring text - it requires flipping the entire layout direction, swapping
padding/margins, mirroring icons with directional meaning, and using CSS logical
properties (inline-start/inline-end instead of left/right).
See references/rtl-layout.md.
Translation workflows connect developers to translators. The typical pipeline is: extract strings from source code into message catalogs (JSON/XLIFF/PO files), send catalogs to translators (via TMS or manual handoff), receive translations, compile them into the app's locale bundles, and validate completeness. Missing translations should fall back to the default locale, never show raw message keys.
Common tasks
Set up react-intl (FormatJS) in a React app
Install the library and wrap the app with IntlProvider.
npm install react-intlimport { IntlProvider, FormattedMessage } from 'react-intl';
const messages = {
en: { greeting: 'Hello, {name}!' },
fr: { greeting: 'Bonjour, {name} !' },
};
function App({ locale }) {
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<FormattedMessage id="greeting" values={{ name: 'World' }} />
</IntlProvider>
);
}Always load only the messages for the active locale to minimize bundle size.
Set up i18next in a Node.js or React app
npm install i18next react-i18next i18next-browser-languagedetectorimport i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: { welcome: 'Welcome, {{name}}!' } },
ja: { translation: { welcome: 'ようこそ、{{name}}さん!' } },
},
fallbackLng: 'en',
interpolation: { escapeValue: false },
});i18next uses
{{double braces}}for interpolation by default, not ICU{single braces}. Enable ICU MessageFormat with thei18next-icuplugin if you want standard ICU syntax.
Write ICU plural messages
You have {count, plural,
=0 {no messages}
one {# message}
other {# messages}
}.The # symbol is replaced with the formatted number. Always include other as the
fallback category. For languages with more plural forms (Arabic, Polish, Russian),
translators add the additional categories (zero, two, few, many).
See references/icu-message-format.md for select, selectordinal, and nested patterns.
Write ICU select messages (gender/category)
{gender, select,
male {He liked your post}
female {She liked your post}
other {They liked your post}
}The other branch is required and acts as the default. Select works for any
categorical variable, not just gender.
Format dates, numbers, and currencies per locale
// Numbers
new Intl.NumberFormat('de-DE').format(1234567.89);
// -> "1.234.567,89"
// Currency
new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY',
}).format(5000);
// -> "¥5,000"
// Dates
new Intl.DateTimeFormat('fr-FR', {
dateStyle: 'long',
}).format(new Date('2025-03-14'));
// -> "14 mars 2025"Always use
Intl.NumberFormatandIntl.DateTimeFormat(or library equivalents). Never manually format numbers with string operations - decimal separators, grouping separators, and currency symbol positions vary by locale.
Configure RTL layout with CSS logical properties
/* Instead of physical directions: */
.card {
margin-left: 16px; /* DON'T */
padding-right: 8px; /* DON'T */
text-align: left; /* DON'T */
}
/* Use logical properties: */
.card {
margin-inline-start: 16px; /* DO */
padding-inline-end: 8px; /* DO */
text-align: start; /* DO */
}Set the document direction with <html dir="rtl" lang="ar">. For bidirectional content,
use the dir="auto" attribute on user-generated content containers.
See references/rtl-layout.md for the full migration guide.
Extract translatable strings from source code
For react-intl / FormatJS projects:
npx formatjs extract 'src/**/*.{ts,tsx}' --out-file lang/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'For i18next projects, use i18next-parser:
npx i18next-parser 'src/**/*.{js,jsx,ts,tsx}'Run extraction in CI to catch untranslated strings before they reach production.
Handle missing translations with fallback chains
// i18next fallback chain
i18n.init({
fallbackLng: {
'pt-BR': ['pt', 'en'],
'zh-Hant': ['zh-Hans', 'en'],
default: ['en'],
},
});The fallback order should go: specific locale -> language family -> default language.
Never show raw message keys (app.greeting.title) to users - always ensure the
fallback chain terminates at a fully-translated locale.
Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| String concatenation for translations | Word order differs across languages; "Welcome to " + city fails in Japanese |
Use ICU placeholders: "Welcome to {city}" |
Hardcoded plural logic (n === 1) |
Only works for English; breaks for Arabic (6 forms), Polish (4 forms), Russian (3 forms) | Use ICU {count, plural, ...} with CLDR rules |
Using left/right CSS properties |
Breaks RTL layouts for Arabic, Hebrew, Persian | Use CSS logical properties: inline-start/inline-end |
| Translating string fragments | "Click " + <Link>here</Link> + " to continue" is untranslatable as a whole |
Use rich text formatting: "Click <link>here</link> to continue" with component interpolation |
| Embedding numbers in strings | "Page 1 of 5" via concatenation skips locale-aware number formatting |
Use "Page {current} of {total}" with Intl.NumberFormat |
| Storing translations in code | Translations scattered across components make extraction and updates impossible | Centralize in JSON/XLIFF message catalogs, one file per locale |
| Assuming text length is constant | German is ~35% longer than English; UI clips or overflows | Design flexible layouts, test with pseudolocalization |
Gotchas
i18next
{{double braces}}vs ICU{single braces}silently produce different output - These two interpolation syntaxes are not interchangeable. Using ICU-style{name}in a project configured for i18next double-brace mode renders the literal string{name}to users. Thei18next-icuplugin must be installed and configured before switching syntax. Always verify interpolation works end-to-end with a real locale before migrating your message catalog.Intl.DateTimeFormatandIntl.NumberFormatconstructors are expensive - cache them - Creating a newIntl.NumberFormat('de-DE', {...})on every render or request has measurable overhead on high-traffic paths. Cache formatter instances keyed by locale and options string, or use a memoization wrapper. This is a common source of unexpected latency on locale-switching UIs.Pluralization fallback to
othermasks missing plural forms in production - ICU'sotheris required and acts as a catch-all. If a translator omits thefewform for Polish and a user has exactly 3 items, theotherform renders silently - wrong grammar but no error. Run pluralization validation against CLDR's expected forms per locale in CI to catch missing categories before release.RTL flipping CSS is not enough - icon direction and component orientation also need adjustment - Applying
dir="rtl"flips text direction but does not automatically mirror directional icons (arrows, chevrons, back buttons), progress bars, or timeline components. Each directional UI element needs explicit RTL handling. Use a design system that supportsdir-aware icon variants rather than flipping with CSS transforms.String extraction tools miss dynamic message IDs - If your code constructs a message ID at runtime (
t('error.' + code)), extraction tools likei18next-parserorformatjs extractcannot statically analyze it and will omit the key from the catalog. Translators never see it; users see raw keys. Use static string literals for all message IDs; handle dynamic content as parameters within a static message.
References
For detailed content on specific topics, read the relevant file from references/:
references/icu-message-format.md- Full ICU syntax: plural, select, selectordinal, nested patterns, number/date skeletonsreferences/pluralization.md- CLDR plural rules by language, plural categories, and common pitfallsreferences/rtl-layout.md- Complete RTL migration guide: CSS logical properties, bidirectional text, icon mirroringreferences/translation-workflows.md- TMS integration, XLIFF/JSON/PO formats, CI extraction, pseudolocalization
Only load a references file if the current task requires deep detail on that topic.
References
icu-message-format.md
ICU MessageFormat - Full Syntax Guide
ICU MessageFormat is the Unicode standard for parameterized, translatable messages.
It is supported by react-intl (FormatJS), i18next-icu, messageformat.js, Java's
java.text.MessageFormat, and most professional translation management systems.
Simple interpolation
Hello, {name}!
You have {count} items in your cart.Variable names inside {braces} are replaced at runtime. The variable name must
match exactly what the code passes.
Plural - {variable, plural, ...}
Selects a message variant based on a numeric value using CLDR plural rules.
Syntax
{count, plural,
=0 {No items}
one {# item}
other {# items}
}Plural categories
The six CLDR categories (not all languages use all of them):
| Category | Used by | Example trigger values |
|---|---|---|
zero |
Arabic, Latvian | 0 (in Arabic) |
one |
English, French, German, Portuguese | 1 |
two |
Arabic, Hebrew, Slovenian | 2 |
few |
Polish, Czech, Russian, Arabic | 2-4 (Polish), 3-10 (Arabic) |
many |
Polish, Russian, Arabic | 5-20 (Polish), 11-99 (Arabic) |
other |
ALL languages (required) | Everything else |
The # symbol
Inside a plural branch, # is replaced with the locale-formatted value of the
plural variable:
{count, plural,
one {# file was deleted}
other {# files were deleted}
}With count = 1000 and locale en-US: "1,000 files were deleted"
Exact matches with =N
=0, =1, =2 etc. match exact numeric values and take priority over categories:
{count, plural,
=0 {Your inbox is empty}
=1 {You have one new message}
other {You have # new messages}
}Rule: always include other
Every plural message MUST have an other branch. Omitting it is a runtime error
in most implementations.
Select - {variable, select, ...}
Selects a message variant based on a string value. Commonly used for gender but works for any categorical variable.
Syntax
{role, select,
admin {Administrator settings}
editor {Editor dashboard}
other {Viewer mode}
}The other branch is required and acts as the default for unrecognized values.
Gender example
{gender, select,
male {He will attend the event}
female {She will attend the event}
other {They will attend the event}
}Selectordinal - {variable, selectordinal, ...}
Handles ordinal numbers ("1st", "2nd", "3rd", "4th").
{position, selectordinal,
one {#st place}
two {#nd place}
few {#rd place}
other {#th place}
}- 1 -> "1st place" (matches
one) - 2 -> "2nd place" (matches
two) - 3 -> "3rd place" (matches
few) - 4 -> "4th place" (matches
other) - 21 -> "21st place" (matches
one)
Nested patterns
ICU messages can be nested. A common case is combining gender select with plurals:
{gender, select,
male {{count, plural,
one {He has # new notification}
other {He has # new notifications}
}}
female {{count, plural,
one {She has # new notification}
other {She has # new notifications}
}}
other {{count, plural,
one {They have # new notification}
other {They have # new notifications}
}}
}Keep nesting to 2 levels maximum. Deeper nesting makes messages very hard for translators to work with. If you need 3+ levels, split into separate messages.
Number and date formatting
Number skeletons
The total is {amount, number, ::currency/USD}.
Progress: {ratio, number, ::percent}.Date skeletons
Joined on {date, date, medium}.
Last seen {time, time, short}.Standard date/time styles: short, medium, long, full.
Custom skeletons (ICU 67+)
{date, date, ::yyyyMMdd}
{amount, number, ::compact-short}Escaping literal braces
To include literal { or } in a message, wrap the literal text in single quotes:
Wrap code in '{' curly braces '}'.A single ' is escaped with '':
It''s a beautiful day.Rich text / tags (FormatJS extension)
FormatJS extends ICU with XML-like tags for component interpolation:
Please <link>click here</link> to continue.
Read the <bold>terms of service</bold>.In React:
<FormattedMessage
id="cta"
defaultMessage="Please <link>click here</link> to continue."
values={{
link: (chunks) => <a href="/next">{chunks}</a>,
}}
/>This keeps the full sentence available for translators instead of breaking it into fragments.
Best practices for message authors
- Keep messages as complete sentences - never split a sentence across multiple keys
- Include enough context in the message ID or description for translators
- Limit nesting to 2 levels
- Always provide
otherfor plural and select - Use
#for the formatted number in plural branches, never re-interpolate the count - Prefer named variables (
{userName}) over positional ones - Add translator comments explaining context when the meaning is ambiguous
pluralization.md
Pluralization - CLDR Plural Rules
Pluralization is one of the hardest parts of i18n because languages have wildly different rules. English has 2 forms (singular/plural), Arabic has 6, Polish has 4, Japanese has 1 (no plural distinction). The Unicode CLDR (Common Locale Data Repository) defines the authoritative plural rules for every language.
The six CLDR plural categories
| Category | Meaning | Example languages |
|---|---|---|
zero |
Special form for zero | Arabic, Latvian, Welsh |
one |
Singular or special "one" form | English, French, German, Spanish, Hindi |
two |
Dual form | Arabic, Hebrew, Slovenian |
few |
Paucal / small numbers | Polish (2-4), Czech (2-4), Russian (2-4 mod 10), Arabic (3-10) |
many |
Large numbers | Polish (5+), Russian (5+ mod 10), Arabic (11-99) |
other |
General / default (ALWAYS required) | All languages |
Plural rules by language
English (2 forms: one, other)
one: n = 1
other: everything else{count, plural,
one {# item}
other {# items}
}French (2 forms: one, other)
one: n = 0 or n = 1 (NOTE: 0 is singular in French!)
other: everything elseThis is a common gotcha - in French, 0 pomme (singular), not 0 pommes.
German (2 forms: one, other)
Same as English: one for 1, other for everything else.
Russian (3 forms: one, few, many, other)
one: n mod 10 = 1 AND n mod 100 != 11
few: n mod 10 in 2..4 AND n mod 100 not in 12..14
many: n mod 10 = 0 OR n mod 10 in 5..9 OR n mod 100 in 11..14
other: (used for non-integer values)Examples:
- 1 -> one: "1 файл" (file)
- 2 -> few: "2 файла" (files, genitive singular)
- 5 -> many: "5 файлов" (files, genitive plural)
- 21 -> one: "21 файл"
- 22 -> few: "22 файла"
- 11 -> many: "11 файлов"
Polish (3 forms: one, few, many, other)
one: n = 1
few: n mod 10 in 2..4 AND n mod 100 not in 12..14
many: n != 1 AND n mod 10 in 0..1, OR n mod 10 in 5..9, OR n mod 100 in 12..14
other: (non-integer values)Arabic (6 forms: zero, one, two, few, many, other)
zero: n = 0
one: n = 1
two: n = 2
few: n mod 100 in 3..10
many: n mod 100 in 11..99
other: everything else (including 100, 1000, etc.)Arabic is the most complex major language for pluralization.
Japanese, Chinese, Korean, Vietnamese (1 form: other)
These languages have no grammatical plural. Only the other category is used.
{count, plural,
other {#個のアイテム}
}Hebrew (3 forms: one, two, other)
one: n = 1
two: n = 2
other: everything elseCommon pluralization mistakes
Mistake 1: Hardcoding English plural logic
// WRONG - only works for English
const label = count === 1 ? 'item' : 'items';
// CORRECT - use ICU MessageFormat
const message = intl.formatMessage({
id: 'cart.items',
defaultMessage: '{count, plural, one {# item} other {# items}}',
}, { count });Mistake 2: Forgetting the other category
// WRONG - missing `other`, will crash for non-integer values
{count, plural,
one {# item}
}
// CORRECT
{count, plural,
one {# item}
other {# items}
}Mistake 3: Only providing English plural forms in source messages
When writing source messages (usually English), only provide one and other.
Translators for Arabic, Polish, etc. will add zero, two, few, many during
translation. Your source messages don't need all six categories.
Mistake 4: Using =0 when zero is intended
=0 is an exact match for the number 0, used in all languages.
zero is a CLDR plural category, used only in languages that have a grammatical
zero form (like Arabic).
// =0 shows in ALL languages when count is exactly 0
{count, plural,
=0 {No items}
one {# item}
other {# items}
}
// zero category only activates in languages with a grammatical zero formUse =0 for "empty state" messages that apply universally. Let translators handle
the zero category for language-specific forms.
Mistake 5: Assuming decimal numbers follow integer rules
In many languages, decimal numbers (1.5, 2.0, 0.5) have different plural rules
than integers. CLDR rules use i (integer part) and f/t (fractional parts)
in their definitions. Test with decimals, not just integers.
Testing pluralization
Always test these values for each plural message:
| Value | Why |
|---|---|
| 0 | Empty state |
| 1 | Singular (one) |
| 2 | Dual form languages (Arabic, Hebrew) |
| 5 | Triggers many in Russian, Polish |
| 11 | Gotcha: many in Russian (not one even though it ends in 1) |
| 21 | Gotcha: one in Russian (mod 10 = 1, mod 100 != 11) |
| 0.5 | Decimal - may use other even in English |
| 1.0 | May or may not match one depending on implementation |
CLDR plural rule reference
The authoritative source for all plural rules is: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
When in doubt, check this reference rather than guessing.
rtl-layout.md
RTL Layout - Complete Migration Guide
Right-to-left (RTL) support is required for Arabic, Hebrew, Persian (Farsi), and Urdu. RTL is not just "flip the text" - it requires rethinking the entire visual layout direction, including margins, padding, icons, and reading flow.
RTL languages
| Language | Script | Direction | BCP 47 examples |
|---|---|---|---|
| Arabic | Arabic | RTL | ar, ar-SA, ar-EG |
| Hebrew | Hebrew | RTL | he, he-IL |
| Persian (Farsi) | Arabic | RTL | fa, fa-IR |
| Urdu | Arabic | RTL | ur, ur-PK |
| Pashto | Arabic | RTL | ps |
| Kurdish (Sorani) | Arabic | RTL | ckb |
Step 1: Set the document direction
<!-- For RTL locales -->
<html dir="rtl" lang="ar">
<!-- For LTR locales -->
<html dir="ltr" lang="en">Set dir dynamically based on the active locale:
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'ps', 'ckb'];
function isRtl(locale) {
const lang = locale.split('-')[0];
return rtlLocales.includes(lang);
}
document.documentElement.dir = isRtl(locale) ? 'rtl' : 'ltr';
document.documentElement.lang = locale;Step 2: Replace physical CSS properties with logical properties
This is the most impactful change. Logical properties automatically flip based on
the document's dir attribute.
Property mapping
| Physical (DON'T) | Logical (DO) |
|---|---|
margin-left |
margin-inline-start |
margin-right |
margin-inline-end |
padding-left |
padding-inline-start |
padding-right |
padding-inline-end |
border-left |
border-inline-start |
border-right |
border-inline-end |
left |
inset-inline-start |
right |
inset-inline-end |
text-align: left |
text-align: start |
text-align: right |
text-align: end |
float: left |
float: inline-start |
float: right |
float: inline-end |
border-radius: 4px 0 0 4px |
border-start-start-radius: 4px; border-end-start-radius: 4px |
Shorthand logical properties
/* Physical shorthand - values are top/right/bottom/left */
.box {
margin: 10px 20px 10px 5px; /* DON'T - left/right are hardcoded */
}
/* Logical shorthand - values are block/inline */
.box {
margin-block: 10px; /* top and bottom */
margin-inline: 5px 20px; /* start and end */
}Size properties
| Physical | Logical |
|---|---|
width |
inline-size |
height |
block-size |
min-width |
min-inline-size |
max-height |
max-block-size |
Step 3: Handle Flexbox and Grid
Flexbox row direction automatically flips in RTL contexts. No changes needed
for basic flex layouts if you're using logical properties for gaps and padding.
/* This automatically reverses order in RTL */
.nav {
display: flex;
flex-direction: row;
gap: 16px;
}For Grid, grid-column-start and grid-column-end do NOT flip. Use logical
equivalents or rely on direction inheritance:
/* Explicit placement - does NOT flip */
.sidebar {
grid-column: 1 / 2; /* Always left column */
}
/* Better: use named areas that respond to direction */
.layout {
display: grid;
grid-template-areas: "sidebar content";
}
[dir="rtl"] .layout {
grid-template-areas: "content sidebar";
}Step 4: Mirror directional icons
Icons with directional meaning must be mirrored in RTL:
Mirror these:
- Back/forward arrows
- Navigation chevrons
- Reply/forward icons
- Progress indicators
- List bullets
- External link indicators
Do NOT mirror these:
- Clocks (time goes clockwise universally)
- Checkmarks
- Media play/pause (universal convention)
- Logos and brand marks
- Physical world representations (maps, compasses)
CSS mirroring approach
[dir="rtl"] .icon-directional {
transform: scaleX(-1);
}Better: use a dedicated RTL icon set or CSS class
.icon-back {
background-image: url('arrow-left.svg');
}
[dir="rtl"] .icon-back {
background-image: url('arrow-right.svg');
}Step 5: Handle bidirectional text (bidi)
When RTL and LTR text appear together (usernames, URLs, code snippets, numbers in Arabic text), use Unicode bidi controls or HTML attributes:
HTML dir="auto"
For user-generated content where you don't know the direction:
<p dir="auto">{userComment}</p>The browser determines direction from the first strong character.
Unicode bidi isolation
<!-- Wrap embedded opposite-direction text -->
<bdi>{userName}</bdi> left a comment.<bdi> (bidirectional isolation) prevents the embedded text from disrupting
the surrounding text's direction.
CSS bidi isolation
.user-content {
unicode-bidi: isolate;
}Step 6: Numbers in RTL
Arabic text is RTL, but numbers in Arabic are written left-to-right (even in Arabic script). The browser handles this automatically for standard numbers.
Arabic-Indic numerals (used in some Arabic locales):
| Western | Arabic-Indic |
|---|---|
| 0 | ٠ |
| 1 | ١ |
| 2 | ٢ |
| 3 | ٣ |
Use Intl.NumberFormat to respect the locale's numbering system:
new Intl.NumberFormat('ar-SA', { numberingSystem: 'arab' }).format(1234);
// -> "١٬٢٣٤"
new Intl.NumberFormat('ar-SA').format(1234);
// -> "1,234" (default, western numerals)Testing RTL
- Quick test: Add
dir="rtl"to<html>in your browser's dev tools - Pseudolocalization: Use a tool that reverses strings and adds RTL marks
- Visual regression: Screenshot tests in both LTR and RTL modes
- Real language testing: Test with actual Arabic or Hebrew content, not just mirrored English - real text reveals bidi edge cases
- Common breakpoints: Check text truncation, overflow, and tooltip positioning
Browser support
CSS logical properties have excellent support (95%+ global coverage as of 2025).
The main gap is older Safari versions (< 15). If you need to support them, use
a PostCSS plugin like postcss-logical to generate physical property fallbacks.
translation-workflows.md
Translation Workflows
A translation workflow connects developers writing code to translators producing localized strings. A good workflow is automated, auditable, and prevents untranslated strings from reaching production.
The standard pipeline
1. Developer writes code with message keys and default messages
2. CI extracts translatable strings into message catalogs
3. Catalogs are sent to TMS (translation management system)
4. Translators translate strings in the TMS
5. Translated catalogs are pulled back into the codebase
6. App loads the correct locale bundle at runtime
7. Missing translations fall back to the default localeMessage catalog formats
JSON (most common for web)
Used by react-intl, i18next, vue-i18n, and most modern web frameworks.
Flat structure (i18next default):
{
"greeting": "Hello, {{name}}!",
"cart.items": "You have {{count}} items",
"cart.empty": "Your cart is empty"
}Nested structure (i18next with namespaces):
{
"cart": {
"items": "You have {{count}} items",
"empty": "Your cart is empty"
}
}ICU format (react-intl / FormatJS):
{
"greeting": {
"defaultMessage": "Hello, {name}!",
"description": "Greeting shown on the home page"
},
"cart.items": {
"defaultMessage": "{count, plural, one {# item} other {# items}}",
"description": "Item count in shopping cart"
}
}XLIFF (XML Localization Interchange File Format)
Industry standard for professional translation. Required by many TMS platforms.
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="2.0" srcLang="en" trgLang="fr">
<file id="messages">
<unit id="greeting">
<segment>
<source>Hello, {name}!</source>
<target>Bonjour, {name} !</target>
</segment>
<notes>
<note category="description">Greeting shown on the home page</note>
</notes>
</unit>
</file>
</xliff>PO/POT (Gettext)
Common in Python (Django), PHP, and C projects.
#: src/components/Header.js:42
#. Greeting shown on the home page
msgid "Hello, {name}!"
msgstr "Bonjour, {name} !"
#: src/components/Cart.js:15
msgid "{count, plural, one {# item} other {# items}}"
msgstr "{count, plural, one {# article} other {# articles}}"String extraction
FormatJS (react-intl)
# Extract from TypeScript/JavaScript source
npx formatjs extract 'src/**/*.{ts,tsx,js,jsx}' \
--out-file lang/en.json \
--id-interpolation-pattern '[sha512:contenthash:base64:6]'
# Compile extracted messages for production (flattens, strips descriptions)
npx formatjs compile lang/en.json --out-file compiled/en.json
npx formatjs compile lang/fr.json --out-file compiled/fr.jsoni18next-parser
npx i18next-parser --config i18next-parser.config.jsConfig example:
// i18next-parser.config.js
module.exports = {
locales: ['en', 'fr', 'de', 'ja', 'ar'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
input: ['src/**/*.{js,jsx,ts,tsx}'],
defaultNamespace: 'translation',
keySeparator: '.',
namespaceSeparator: ':',
};gettext (Python/Django)
# Extract strings
python manage.py makemessages -l fr -l de -l ar
# Compile after translation
python manage.py compilemessagesTranslation management systems (TMS)
| TMS | Strengths | Format support |
|---|---|---|
| Crowdin | Developer-friendly, GitHub/GitLab integration, OTA updates | JSON, XLIFF, PO, Android XML, iOS strings |
| Lokalise | Fast UI, API-first, CLI tool for CI | JSON, XLIFF, PO, ARB, YAML |
| Phrase (formerly PhraseApp) | Enterprise features, in-context editor | JSON, XLIFF, PO, YAML, properties |
| Transifex | Open-source friendly, large translator community | PO, JSON, XLIFF, YAML |
| Weblate | Self-hosted option, open source | PO, JSON, XLIFF, Android XML |
Typical TMS integration
# .crowdin.yml example
project_id: "123456"
api_token_env: CROWDIN_TOKEN
files:
- source: /lang/en.json
translation: /lang/%locale%.json# Push source strings to TMS
crowdin upload sources
# Pull translations
crowdin downloadCI/CD integration
Pre-merge checks
Add these to your CI pipeline:
# GitHub Actions example
- name: Extract i18n strings
run: npx formatjs extract 'src/**/*.{ts,tsx}' --out-file lang/en.json
- name: Check for untranslated strings
run: |
node scripts/check-translations.jsTranslation completeness check script:
// scripts/check-translations.js
const en = require('../lang/en.json');
const fr = require('../lang/fr.json');
const enKeys = Object.keys(en);
const frKeys = Object.keys(fr);
const missing = enKeys.filter(key => !frKeys.includes(key));
if (missing.length > 0) {
console.error(`Missing French translations: ${missing.length}`);
missing.forEach(key => console.error(` - ${key}`));
process.exit(1);
}Post-merge automation
After merging new strings to main:
- CI extracts strings and pushes to TMS
- TMS notifies translators of new strings
- Translators complete translations
- TMS opens a PR with new translations (or pushes directly)
- CI validates completeness and merges
Pseudolocalization
Pseudolocalization transforms English strings to simulate translation issues without actual translation. It catches layout bugs, truncation, and hardcoded strings early.
What pseudolocalization does
| Transformation | Purpose | Example |
|---|---|---|
| Accented characters | Test encoding, font support | Hello -> Hello |
| Text expansion (~35%) | Test layout with longer text | Save -> [Saavee____] |
| Brackets/markers | Find untranslated strings | Save -> [Save] |
| Bidi markers | Test RTL readiness | Adds RLM/LRM characters |
FormatJS pseudolocalization
npx formatjs compile lang/en.json \
--out-file compiled/pseudo.json \
--pseudo-locale en-XAi18next pseudolocalization
Use the i18next-pseudo plugin:
import i18n from 'i18next';
import Pseudo from 'i18next-pseudo';
i18n.use(new Pseudo({ enabled: process.env.NODE_ENV === 'development' }));Best practices
- Extract strings in CI, not manually - Manual extraction misses strings and drifts
- Use translator comments - Add
descriptionfields explaining context - Never machine-translate and ship - Machine translation is a draft, not a final product
- Version your message catalogs - Track which strings were added/changed per release
- Set up OTA (over-the-air) updates - Update translations without a full app deploy
- Test with pseudolocalization in development, real languages in staging
- Monitor translation coverage per locale in your CI dashboard
Frequently Asked Questions
What is localization-i18n?
Use this skill when working with internationalization (i18n), localization (l10n), translation workflows, right-to-left (RTL) layout support, pluralization rules, or ICU MessageFormat syntax. Triggers on translating strings, setting up i18n libraries (react-intl, i18next, FormatJS), handling plural forms, formatting dates/numbers/currencies for locales, building translation pipelines, configuring RTL stylesheets, or writing ICU message patterns with select/plural/selectordinal.
How do I install localization-i18n?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill localization-i18n in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support localization-i18n?
localization-i18n works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.