Skip to main content
The @ooneex/translation component is an internationalization layer for multi-language applications. You extend the Translation base class, point it at a dictionary, and resolve localized strings with trans(key, options) — dot-notation keys, {{ param }} interpolation, and count-driven pluralization are built in. The dictionary is keyed by locale, with en as the always-present fallback, across 31 supported locale codes.

Why this component

  • One dictionary, every locale. Each leaf carries its translations keyed by locale code (en, fr, es, …); en is the guaranteed fallback when a target locale is missing.
  • Dot-notation keys. Access nested entries like trans("user.profile.name") regardless of how deep the dictionary nests.
  • Interpolation built in. Fill {{ param }} placeholders through params without string concatenation.
  • Pluralization by count. Sibling keys (<key>, <key>_plural, <key>_zero) are selected automatically from the count you pass.
  • Container-managed. Register a translation class with a decorator and resolve it from the container.

How it works

You extend Translation and implement two methods: getName() returns a stable identifier for the domain, and getDict() returns the dictionary. Lookups go through has() and trans(), which resolve the requested locale, fall back to en, interpolate, and pick the right plural form.
MemberPurpose
trans(key, options?)Resolve a localized string for key; interpolates params and selects the plural form from count.
has(key)Whether key exists in the dictionary.
getName()Abstract — a stable identifier for the translation domain.
getDict()Abstract — returns the TranslationDictType dictionary.
trans() accepts a TransOptionsType:
OptionTypePurpose
langLocaleTypeTarget locale. Defaults to en.
paramsTransParamsTypeValues for {{ param }} placeholders.
countnumberSelects the pluralization sibling (singular / _plural / _zero).
Resolution order: the requested lang is tried first, then the en fallback. A missing key throws with KEY_NOT_FOUND; a key present but absent in both the locale and the en fallback throws with LOCALE_NOT_FOUND. Pluralization picks <key> when count === 1, <key>_plural when count > 1 or count < 0, and <key>_zero when count === 0 (falling back to _plural if _zero is absent). The supported locale codes are exported as locales:
ar bg cs da de el en eo es et eu fi fr hu hy it ja ko lt nl no pl pt ro ru sk sv th uk zh zh-tw

Usage

Extend Translation, load a dictionary, and resolve keys through trans().
import type { TranslationDictType } from "@ooneex/translation";
import { decorator, Translation } from "@ooneex/translation";
import dict from "./translations.yml";

@decorator.translation()
export class CheckoutTranslation extends Translation {
  public getName = (): string => "checkout";

  public getDict = (): TranslationDictType => dict as TranslationDictType;
}
Resolve it from the container and translate keys:
import { container } from "@ooneex/container";

const t = container.get(CheckoutTranslation);

// Simple lookup with fallback to "en"
t.trans("user.profile.name", { lang: "fr" }); // "Nom complet"

// Interpolation with params
t.trans("coach.session.welcome", { lang: "fr", params: { name: "Marie" } });
// "Bon retour, Marie !"

// Pluralization driven by count
t.trans("cart.items", { count: 0 }); // "No items"   (items_zero)
t.trans("cart.items", { count: 1 }); // "1 item"     (items)
t.trans("cart.items", { count: 5 }); // "5 items"    (items_plural)

// Existence check
t.has("user.profile.email"); // true
The dictionary is a tree of nested keys; each leaf is an object keyed by locale code. Interpolation uses {{ param }} placeholders, and pluralization uses sibling keys selected by count:
# translations.yml
user:
  profile:
    name:
      en: "Full name"
      fr: "Nom complet"

coach:
  session:
    welcome:
      en: "Welcome back, {{ name }}!"
      fr: "Bon retour, {{ name }} !"

cart:
  items:
    en: "{{ count }} item"
    fr: "{{ count }} article"
  items_plural:
    en: "{{ count }} items"
    fr: "{{ count }} articles"
  items_zero:
    en: "No items"
    fr: "Aucun article"

Exceptions

The component throws TranslationException when a key cannot be resolved. It carries a machine-readable key, a human-readable message, and a data object (with the lookup key and lang).
KeyWhen
KEY_NOT_FOUNDThe requested key does not exist in the dictionary.
LOCALE_NOT_FOUNDThe key exists but has no value for the requested locale or the en fallback.
import { TranslationException } from "@ooneex/translation";

try {
  return t.trans("user.profile.name", { lang: "fr" });
} catch (error) {
  if (error instanceof TranslationException) {
    logger.error(`Translation error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • Always provide an en value. It is the fallback for every locale; a key without en throws LOCALE_NOT_FOUND whenever the target locale is missing.
  • Group keys by domain. Use stable dot-notation paths like user.profile.name so dictionaries stay navigable and getName() maps cleanly to a domain.
  • Keep placeholders verbatim. {{ name }} and {{ count }} must appear identically across locales; only the surrounding words change.
  • Always pass count for pluralized keys. Provide <key>, <key>_plural, and (optionally) <key>_zero siblings so the correct form is selected.
  • Translate meaning, not words. Phrase each string the way a native speaker writing the product UI would, matching the locale’s capitalization and punctuation conventions.
  • Never overwrite correct translations. When completing locales, fill blanks only — keep keys stable and existing entries intact.

CLI command

Scaffold a translation class, its test file, and a sibling translations.yml dictionary with the generator. It writes the class under modules/<module>/src/translations/<Name>Translation.ts and installs @ooneex/translation if it is missing.
# Interactive: prompts for the name
ooneex translation:create

# Provide the name
ooneex translation:create --name=Checkout

# Target a module and overwrite
ooneex translation:create --name=Checkout --module=shop --override
OptionDescriptionDefault
--nameTranslation class name. The Translation suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class extends Translation and loads the sibling translations.yml as its dictionary, ready for you to fill the keys:
import type { TranslationDictType } from "@ooneex/translation";
import { decorator, Translation } from "@ooneex/translation";
import dict from "./translations.yml";

@decorator.translation()
export class CheckoutTranslation extends Translation {
  public getName = (): string => "checkout";

  public getDict = (): TranslationDictType => dict as TranslationDictType;
}
The sibling translations.yml is written once per folder, so translation classes in the same translations/ directory share one dictionary. See translation:create for the full command reference.

Use with Claude and Codex

The generator ships matching translation:create and translation:translate skills. The first runs the scaffold and guides your AI agent through filling the dictionary; the second translates existing dictionaries meaning-for-meaning, completing every target locale from the en source. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the translations:
Prompt
Create translations for the checkout page in English and French.
For example, the prompt above maps to translation:create --name=Checkout, then fills the translations.yml dictionary with the en and fr entries for each key.