{
"greeting": "Guten Morgen",
"save": "Speichern",
"cancel": "Abbrechen"
}
1.6.1 React Native · React · Node
Write text.
Ship anywhere.
Write plain text in React Native. Load languages on demand in React. Translate on the server with Node. One small library, flat JSON files.
<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>greetingNEW IN 1.6 / THREE ENTRIES
One package.
Your runtime.
Use the entry that fits your app. Browser and server imports keep React Native out of the dependency path.
React Native
from 'locon'The provider, hook and drop-in <LText> you already use. Source-phrase lookup, native locale detection and RTL helpers stay available.
React
from 'locon/react'A provider, hook and wrapper-free <L> tag for React 19+. Fetch the selected language, cache it, and load another only when it is chosen.
Node
from 'locon/core'Translate emails, exports and server output with the same resources. No React or React Native import. ESM, CommonJS and TypeScript types included.
Node quick start ↓Write source-language text with <L> or l(). Using source phrases as JSON keys preserves lazy loading without preloading the source locale.
04 / QUICK START
Pick a runtime.
Start small.
Install Locon once, then choose your entry below. Native keeps its existing API. React web gets async resource loaders. Node uses the standalone core, without installing either framework. The core entries require Locon 1.6.0+. The React <L> component requires 1.6.1+.
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>
}
Your app supplies React 19+ and React Native. Device detection adapters remain optional; see the native docs below.
import { createRoot } from 'react-dom/client'
import { createLocon, LoconProvider } from 'locon/react'
import { Greeting } from './Greeting'
const i18n = createLocon({
defaultLocale: 'en',
languages: navigator.languages,
loaders: {
en: () => import('./i18n/en.json'),
de: () => import('./i18n/de.json'),
},
})
async function mount() {
await i18n.setLocale(null) // just the browser language
createRoot(document.getElementById('root')!).render(
<LoconProvider instance={i18n}>
<Greeting />
</LoconProvider>,
)
}
mount().catch(() => {
document.getElementById('root')!.textContent =
'Could not load translations. Please reload.'
})import { L, useLocon } from 'locon/react'
export function Greeting() {
const { currentLocale, setLocale, loading, error } = useLocon()
const next = currentLocale === 'en' ? 'de' : 'en'
return <>
<h1><L>Good morning</L></h1>
<button
disabled={loading}
onClick={() => { void setLocale(next).catch(() => {}) }}
>
{next === 'de' ? 'Deutsch' : 'English'}
</button>
{error && <p role="alert"><L>Could not load language. Try again.</L></p>}
</>
}For this lazy web example, use source phrases as keys: en.json: {"Good morning":"Good morning","Could not load language. Try again.":"Could not load language. Try again."}; de.json: {"Good morning":"Guten Morgen","Could not load language. Try again.":"Sprache konnte nicht geladen werden. Bitte erneut versuchen."}. Each dynamic import becomes a separate resource chunk in bundlers such as Vite. Browser preferences are passed explicitly; persisting the selected locale and setting document language remain app decisions.
import { createTranslator } from 'locon/core'
import de from './i18n/de.json' with { type: 'json' }
import en from './i18n/en.json' with { type: 'json' }
const l = createTranslator({
assets: { de, en },
locale: 'de',
defaultLocale: 'en',
})
console.log(l('Good morning')) // Guten Morgen
// Bind a translator to each request's output locale.
// A server needs neither React nor React Native.For async server resources, createLocon is also available from locon/core. Create one store per request and await its selected locale before rendering.
05 / DOCUMENTATION
Small API.
Complete picture.
Start with the entry for your runtime. The native reference follows the new React and Node APIs below; the repository README has the complete details.
createLocon()Load a language when it is needed
Pass one async loader per locale. Creating a store performs no fetch. Await setLocale(null) to load the first supported preference from languages, or defaultLocale if none match.
import { createLocon } from 'locon/core'
const i18n = createLocon({
defaultLocale: 'en',
languages: ['de-AT', 'en'],
loaders: {
en: () => import('./i18n/en.json'),
de: () => import('./i18n/de.json'),
},
})
await i18n.setLocale(null) // loads de only
await i18n.setLocale('en') // loads en once
await i18n.setLocale('de') // uses cached de
i18n.l('Good morning') // 'Guten Morgen' (phrase-indexed resources)
Successful loads are cached and parallel loads are deduplicated. The last selection wins. Failed loads reject, expose an error and keep the displayed language; catch the rejection and allow retry. A superseded selection resolves false.
<L>useLocon()Connect the store to React
Pass a loaded instance to LoconProvider. The hook subscribes to locale, loading and error changes. This adapter requires React 19+ and never imports React Native.
import { L, LoconProvider, useLocon } from 'locon/react'
// Bootstrap before mounting:
await i18n.setLocale(null)
// Inside your React tree:
<LoconProvider instance={i18n}>
<App />
</LoconProvider>
// Inside a child component:
const { l, currentLocale, loading, error, setLocale } = useLocon()
l('Good morning') // string props and metadata
<h1><L>Good morning</L></h1>
<h2><L params={{ name }}>{'Hello {name}'}</L></h2>
L returns text without a wrapping HTML element, including inside SVG text. It supports params, count, locale and fallback. Keep l() for string props. Use source phrases as resource keys for lazy apps; reverse lookup against separate keys needs the source resource loaded. On the server, keep each mutable store scoped to one request.
<Locon />React Native: 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()React Native: 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
autodetectReact Native: follow 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 } from 'locon/core'
import { 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.
// Native provider helpers; Node uses matchLocale from locon/core.
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.