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 keyParameter 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 requiredEager 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 diffingDeprecations
createTypedPackage is deprecated — createPackageI18n 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 });