Language packs

DataMind Oda resolves all of its interface text through one language registry. The interface runs in English today and there is no Language row in Settings; a language pack adds a language to the registry and registers the text the interface shows in that language. This page covers the calls a pack makes, the rules they must pass and the messages they return. Language in Oda covers what a user can change today.

What the registry holds

Add a language pack

  1. Define the language. Pass an id, a label and a fallback. The id is a BCP 47-style tag, the fallback must already be registered, and its chain must reach en. The call returns a disposer that removes the definition.
  2. Register the text. Register one namespace at a time, giving the language id in each call. Every call returns a disposer that removes exactly what it added.
  3. Keep the disposers. They are idempotent, so the pack unloads cleanly. Unloading a language the interface is running returns it to English, and the saved choice is not cleared.

The two calls may run in either order: definitions and dictionaries are independent contributions, and a saved choice for a language that is not registered yet waits until its definition arrives.

js
export const inject = ['locale']

export function apply(ctx) {
  ctx.effect(
    () => ctx.locale.addLanguage({ id: 'ja', label: '日本語', fallback: 'en' }),
    'language pack: language',
  )
  ctx.effect(
    () => ctx.locale.register('common', 'ja', { cancel: 'キャンセル', close: '閉じる' }),
    'language pack: dictionary',
  )
}

Rules a definition must pass

Text, lookup and fallbacks

Switching and saving

Questions, answered

What does a language pack have to provide?

A definition and text. The definition carries an id, a label and a fallback; the text arrives as dictionaries, registered one namespace and one language at a time. Both calls return disposers, so the pack unloads cleanly.

Why is my language definition refused?

The definition must pass every rule above: a tag-shaped id, a tag Oda offers, a non-blank label, a registered fallback whose chain reaches en without a loop, and an id that is not already registered. The refusal message names the rule, and a refused definition leaves the registry unchanged.

In which order do I define the language and register its text?

Either order works: definitions and dictionaries are independent contributions. A saved choice for a language that is not registered yet waits unchanged and is adopted as soon as its definition registers.

Can my pack translate the shared vocabulary?

Yes. Register the common namespace for your language, as the example above does, and the keys you supply resolve to your text; keys you leave out fall through to English.

Which text shows when a key has no translation?

The lookup walks the running language's fallback chain in the namespace you registered, then repeats that walk in common, and, when nothing else matches, shows the key text itself. Nothing breaks when a translation is missing; the fallback answers.

Why does some text keep the language it was registered in?

Dictionary lookups read the running language every time, but copy captured while registering, such as a command's description, keeps the language it was registered under until that part registers it again. Slot-rendered copy updates without a reload.

How do placeholders work in my text?

A template can carry {name} placeholders; each one is replaced by the parameter you pass under that name, and a placeholder you leave unfilled stays in the text as written.

Is the translate function safe to keep?

Yes. bind returns the same function for a namespace on every call, so it is safe to keep and reuse, and it reads the running language when it is called, so it follows switches.

What happens if I register a namespace twice for the same language?

The second call is refused with locale namespace "<ns>" already has locale "<locale>": a namespace keeps one owner per language. To replace it, unload the first registration with its disposer and register again.

Can I unload a language pack?

Yes. Every call returns a disposer and disposing is idempotent. Unloading a language the interface is running returns it to English; a saved choice for that language is kept, so registering the definition again brings it back.

Where is a language choice saved?

On the machine that runs Oda, the choice is kept in Oda's settings; on an interface reached from another machine it lasts only for that page's session. setLocale is the only call that writes: it saves even when it names the language already running, and a saved value must look like a language tag (en and pt-BR pass; bad locale and 123 are refused).