Skip to main content
Urbicon UI

Type Safety

Keys and their parameters flow from the en bundle straight into the hook's t — typos and missing params are compile errors, not runtime surprises.

Key inference

createPackageI18n is generic over the en bundle (declared <const T>). With as const — or a plain literal object — the key type flows straight through to the hook's t, so keys autocomplete and typos are compile errors.

const en = {
  dialog: { close: 'Close' },
  greeting: 'Hello {{name}}'
} as const;

const blocks = createPackageI18n('blocks', { en /*, de */ });
const t = blocks.useTranslate(); // inside a component

t('dialog.close');       // ✓ autocompletes
t('dialog.nonexistent'); // ✗ compile error — unknown key

Parameter inference

Parameters are extracted from the {{…}} placeholders in each string. A key with placeholders requires those params; a key without them takes none.

t('greeting', { name: 'Ada' }); // ✓ param `name` inferred from {{name}}
t('greeting');                  // ✗ compile error — missing required param
t('dialog.close');              // ✓ no params — none required

Eager vs. lazy parity

Additional eager locales are checked against the en structure, so a missing or misspelled key in de is a compile error too — key parity by construction. For lazy locales the bundle isn't visible to the type-checker, so parity becomes a runtime check; pair it with validatePackageTranslations in a test.

// Eager: de is checked against the en structure at COMPILE time.
const blocks = createPackageI18n('blocks', { en, de });
//                                                ^ a missing/misspelled key in `de`
//                                                  is a type error — parity by construction.

// Lazy: a loader's bundle isn't visible to the type-checker, so parity is a
// RUNTIME check. Pair it with validatePackageTranslations in a test.
const blocks2 = createPackageI18n(
  'blocks',
  { en },
  { loaders: { de: () => import('../translations/de').then((m) => m.default) } }
);

Deep-key utilities

The same machinery that types the keys is exported for your own tooling — the DeepKeys / DeepValue types and their runtime counterparts. Useful for building key diffs, custom validators, or typed config readers.

import {
  getDeepValue, hasDeepKey, collectDeepKeys,
  type DeepKeys, type DeepValue
} from '@urbicon-ui/i18n';

const en = { dialog: { close: 'Close' } } as const;

type Keys = DeepKeys<typeof en>;            // 'dialog' | 'dialog.close'
type Val = DeepValue<typeof en, 'dialog.close'>; // 'Close'

hasDeepKey(en, 'dialog.close');   // true
getDeepValue(en, 'dialog.close'); // 'Close'
collectDeepKeys(en);              // ['dialog.close'] — leaf paths, for diffing

Deprecations

createTypedPackage is deprecatedcreatePackageI18n gives the same type safety while also registering the bundles, so there's no reason to use the older two-step form.

// ✗ deprecated — same type safety, more ceremony
const pkg = createTypedPackage('blocks', { en, de });

// ✓ current — createPackageI18n registers AND types directly
const pkg2 = createPackageI18n('blocks', { en, de });