{
"greeting": "Guten Morgen",
"save": "Speichern",
"cancel": "Abbrechen"
}
01 Localize. Keep it simple.
Write text.
Not keys.
A small localization library built around one obvious idea: text should look like text while you write it.
<LText>Guten Morgen</LText>
Guten Morgen
The component stays the same.
02 / THE SMALL IDEA
Translation keys turn writing into bookkeeping. Locon lets words remain words.
Usually
t('home.greeting_morning')
invent → switch file → add key → sync
With Locon
<LText>Guten Morgen</LText>
write what the person will read
Guten Morgen
Good morning
Your phrase Locon their language
03 / RESOURCE FILES
Flat files.
Plain strings.
Keep one flat object per locale. Keys connect the files; the values remain normal language your team can read and edit.
PROJECT
src/ assets/ i18n/ de.json en.json{
"greeting": "Good morning",
"save": "Save",
"cancel": "Cancel"
}
<LText>Guten Morgen</LText>greeting04 / QUICK START
Two minutes.
One component.
Locon is deliberately small: a provider, a hook, and a drop-in text component for React Native. It ships no native module of its own, so it works in bare apps, Expo Go, and dev clients. For device language detection, install the optional adapter for your setup as shown below.
npm install locon
import Locon from 'locon'
import de from './i18n/de.json'
import en from './i18n/en.json'
const assets = { de, en }
// Your project speaks German.
<Locon assets={assets} projectLocale='de'>
<App />
</Locon>
import { LText } from 'locon'
export function Greeting() {
return <LText>Guten Morgen</LText>
}
05 / DOCUMENTATION
Small API.
Complete picture.
Everything you need for the common path. The repository README contains the complete reference and edge cases.
<Locon />Provide locales once
Pass the imported resource objects to the provider.
projectLocale is the language used for plain text in your source code. Keep the assets map stable instead of
rebuilding it during render, so locale detection and Locon's reverse index stay cached.
const assets = { de, en }
<Locon
assets={assets}
projectLocale='de'
defaultLocale='en'
>
<App />
</Locon>
- assets
- Locale → flat string object
- projectLocale
- Language written inside components
- defaultLocale
- Fallback locale, defaults to
en - currentLocale
- Explicit locale;
nullre-enables autodetect - autodetect
- Detect device language, enabled by default
<LText />Write the actual interface text
Locon finds the matching key by value in the project locale, then reads that key from the current locale. Missing values fall back per key to the default and project locales; unresolved input renders as-is.
import { LText } from 'locon'
<LText>Guten Morgen</LText>
// Explicit keys still work when you need one.
<LText assetKey='greeting'>Hello</LText>
useLocon()Translate and switch locale
Use the hook outside text components or to build your own language selector.
const {
l,
currentLocale,
setLocale,
} = useLocon()
l('Guten Morgen')
setLocale('en')
- l(value)
- Resolve a key or project-language value
- currentLocale
- Currently selected locale code
- setLocale(locale | null)
- Switch locale or resume autodetection
autodetectFollow the device language
With autodetect enabled, Locon finds the best available locale. On Expo it reads
expo-localization; in bare React Native, react-native-localize. Both are optional — without
either, Locon falls back to native APIs and Intl before using the default locale.
# Expo
npx expo install expo-localization
# bare React Native
npm install react-native-localize
cd ios && pod install
Matching starts with the exact tag, then preserves an explicit or region-implied script before widening to a bare language
or another locale in that language. So
pt can land on pt-BR, while zh-TW and zh-Hant-TW choose
zh-Hant over zh-Hans.
params · countInterpolation and plurals
Plain interpolation only needs params. Plurals are a separate feature: pass count and suffix keys
with the CLDR category the locale actually uses — {count} is interpolated for you. The six possible suffixes
are _zero, _one, _two, _few, _many, and
_other.
// Plain interpolation — no plural forms needed
{
"saved_radius": "{m} m radius · saved"
}
l('{m} m radius · saved', { params: { m: 150 } })
// → '150 m radius · saved'
// Plurals: en.json — two forms
{
"day_one": "{count} day",
"day_other": "{count} days"
}
// ru.json — four categories
{
"day_one": "{count} день",
"day_few": "{count} дня",
"day_many": "{count} дней",
"day_other": "{count} дня"
}
// ar.json — all six categories
{
"day_zero": "لا أيام", // 0
"day_one": "يوم واحد", // 1
"day_two": "يومان", // 2
"day_few": "{count} أيام", // 3–10 modulo 100
"day_many": "{count} يومًا", // 11–99 modulo 100
"day_other": "{count} يوم" // e.g. 100 or a decimal
}
l('{count} days', { count: 5 }) // en → '5 days' · ru → '5 дней'
// Locon uses Intl.PluralRules for the target locale
const ruPlural = new Intl.PluralRules('ru')
ruPlural.select(21) // → 'one'
ruPlural.select(22) // → 'few'
ruPlural.select(25) // → 'many'
ruPlural.select(1.5) // → 'other'
Which numbers select each category is language-specific; the comments above are examples, not universal ranges. Always
provide _other: it is CLDR's catch-all and Locon's first fallback before the bare key. There is deliberately no
generic _plural suffix. See the
ECMA-402 API specification ↗
and
Unicode CLDR Russian rules ↗.
lIn() ·
createTranslator()
One language for the screen, another for the document
lIn() forces a single lookup into another locale. createTranslator() is the same resolver without
React — for a PDF, an export, an e-mail, or a background notification.
import { createTranslator, useLocon } from 'locon'
// Inside React: ignore the UI locale for one lookup.
const { lIn } = useLocon()
lIn('tr', 'Arbeitszeit') // → 'Çalışma süresi'
// Outside React: bind a translator to the output locale.
const translateReport = createTranslator({
assets: { de, tr },
locale: 'tr', // output language
defaultLocale: 'de',
projectLocale: 'de', // language of source phrases
})
translateReport('Arbeitszeit') // → 'Çalışma süresi'
The output locale is independent of the interface: a German UI can still generate a Turkish timesheet, and a Turkish UI can generate a German one.
applyRTL()Right-to-left, on purpose
isRTL on the context tells you the direction of the current locale. applyRTL() aligns React
Native's I18nManager and returns true when the direction actually changed — which means a restart
is needed before it renders. Locon never restarts your app for you.
import { applyRTL, isRtlLocale } from 'locon'
isRtlLocale('ar-EG') // true — region subtags ignored
isRtlLocale('pa-Arab') // true — an explicit script wins
const needsRestart = applyRTL('ar')
On Expo, set supportsRTL: true in the expo-localization plugin config or iOS ignores the change.
resolveLocale() ·
intlLocale()
Locale utilities without a provider
resolveLocale() answers "which locale should I show?" with the exact precedence the provider uses — explicit
choice, else device, else default. Anything running outside React should ask it rather than re-deriving the rule and
eventually disagreeing.
import { intlLocale, resolveLocale } from 'locon'
resolveLocale({ assets, currentLocale: null })
// device language, else default
intlLocale('fa', { calendar: 'persian' })
// → 'fa-u-ca-persian'
intlLocale('de-u-nu-latn', {
calendar: 'gregory',
})
// → 'de-u-nu-latn-ca-gregory'
intlLocale() builds tags for Intl. Locon has no opinion on your calendar or numbering system —
that is a product decision — but it gets the syntax right, which concatenation does not.
06 / AGENT SKILL
Give your agent
the whole checklist.
Locon ships locon-sync, a reusable workflow for auditing translations, synchronizing changed copy, and adding a
language end to end. Claude Code and Codex read the same skill and run the same bundled validation scripts.
Expose the npm skill to both tools
Run this once from the app root, then commit both links. There is still only one canonical copy inside
node_modules/locon.
mkdir -p .claude/skills .agents/skills
ln -s ../../node_modules/locon/skills/locon-sync \
.claude/skills/locon-sync
ln -s ../../node_modules/locon/skills/locon-sync \
.agents/skills/locon-sync
/locon-syncType the slash command first, followed by the task:
# Audit only
/locon-sync Audit every declared locale.
Do not change files; report every problem.
# Synchronize changed UI copy
/locon-sync Sync all existing locales,
run validation, and list open decisions.
# Add a language end to end
/locon-sync Add Polish (pl), including the
picker, fonts, Intl and native metadata.
Do not publish.
$locon-syncMention the skill with $, then describe the same task:
# Audit only
$locon-sync Audit every declared locale.
Do not change files; report every problem.
# Synchronize changed UI copy
$locon-sync Sync all existing locales,
run validation, and list open decisions.
# Add a language end to end
$locon-sync Add Polish (pl), including the
picker, fonts, Intl and native metadata.
Do not publish.
Or just describe the localization task
Both agents can load the skill automatically when your request matches its description — for example, “audit the Locon
translations” or “add Ukrainian”. Explicit invocation is best when you want to make certain the checklist is loaded. Claude
Code shows it in the
/ menu; Codex lists it through /skills or when you type $.
- Discover source locale, assets, and every language the app declares.
- Audit key parity, code lookups, placeholders, plurals, scripts, and duplicate keys.
- Integrate picker entries, fonts, RTL, Intl behavior, and native/store metadata.
- Verify with the bundled checker, project tests, and a reviewable diff.
A skill supplies instructions and scripts, not extra authority. Tell the agent whether it may edit files, commit, or only report. It will not publish merely because the skill is active.