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.

08 <LText>Guten Morgen</LText>
01 / 04

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
de.jsonproject locale
{
  "greeting": "Guten Morgen",
  "save": "Speichern",
  "cancel": "Abbrechen"
}
en.jsoncurrent locale
{
  "greeting": "Good morning",
  "save": "Save",
  "cancel": "Cancel"
}
component<LText>Guten Morgen</LText>
reverse lookup in degreeting
lookup in enGood morning

04 / 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
App.tsx01
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>
Greeting.tsx02
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.

01<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; null re-enables autodetect
autodetect
Detect device language, enabled by default
02<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>
03useLocon()

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
04autodetect

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.

05params · count

Interpolation 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 ↗.

06lIn() · 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.

07applyRTL()

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.

08resolveLocale() · 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.

01

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
Claude Code/locon-sync

Type 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.
Codex$locon-sync

Mention 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.
02

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 $.

  1. Discover source locale, assets, and every language the app declares.
  2. Audit key parity, code lookups, placeholders, plurals, scripts, and duplicate keys.
  3. Integrate picker entries, fonts, RTL, Intl behavior, and native/store metadata.
  4. 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.

Localize.
Keep it simple.