@urbicon-ui/i18n 6.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,107 @@
1
+ import type { I18nError, Locale, PluralParams, TranslationOptions, TranslationParams } from './types';
2
+ /**
3
+ * The constant base locale used for read-tolerant resolution when no
4
+ * `<I18nProvider>` is mounted. A constant — never global mutable state — so a
5
+ * provider-less render is SSR-safe and identical on server and client (no
6
+ * hydration mismatch), and reads never leak across requests. Consumers who need
7
+ * a different or switchable locale mount a provider.
8
+ */
9
+ export declare const BASE_LOCALE: Locale;
10
+ /**
11
+ * Request-scoped reactive locale state. The *only* mutable per-request i18n
12
+ * value — created by `<I18nProvider>` and read through the context. Encapsulated:
13
+ * `locale` is exposed read-only; switching goes through `setLocale`.
14
+ */
15
+ export declare class I18nState {
16
+ #private;
17
+ /** Fallback locale used when a key is missing in the active locale. */
18
+ readonly fallbackLocale: Locale;
19
+ constructor(locale?: Locale, fallbackLocale?: Locale);
20
+ get locale(): Locale;
21
+ /**
22
+ * Switch the active locale in place (reactive — no reload). Returns `false`
23
+ * (and reports `unsupported-locale`) for an unsupported locale, without
24
+ * switching. For a supported locale the switch is applied immediately and
25
+ * `true` is returned ("switch initiated"): if a lazy loader is registered and
26
+ * the data isn't present yet, the load is triggered (not awaited) so the
27
+ * `$derived` reads re-resolve once the chunk lands. Use `registry.loadLocale`
28
+ * directly when you need to await.
29
+ *
30
+ * The async failure is not silent: if that load rejects AND no data exists for
31
+ * the locale (so reads can't even fall back to it), `load-failed-no-fallback`
32
+ * is reported to the error sink — the loud signal that "the language you
33
+ * switched to can't be rendered". (No-op today, where all bundles are eager.)
34
+ */
35
+ setLocale(locale: Locale): boolean;
36
+ }
37
+ /** Set the request-scoped locale state. Called by `<I18nProvider>`. */
38
+ export declare function provideI18nState(state: I18nState): I18nState;
39
+ /**
40
+ * Read the request-scoped locale state, or `undefined` when no `<I18nProvider>`
41
+ * is mounted above (read-tolerant). Must be called during component init.
42
+ */
43
+ export declare function useI18nState(): I18nState | undefined;
44
+ /**
45
+ * Provide the request-scoped locale state from a component's own init, and return
46
+ * it. This is the primitive behind `<I18nProvider>`; reach for it directly when
47
+ * the **same** component both provides i18n and renders translated content —
48
+ * e.g. a root `+layout.svelte` whose own chrome uses `t`. (A child
49
+ * `<I18nProvider>` cannot serve the parent that mounts it, because context only
50
+ * flows downward; calling `provideI18n` in the parent's script puts the state on
51
+ * that component's own context map, which its own `useI18n()`/`use<Pkg>I18n()`
52
+ * then reads.)
53
+ *
54
+ * Pass `locale` as a reactive getter (`() => data.locale`) to keep it controlled:
55
+ * a prop/load change is synced into the state, while an in-place `setLocale`
56
+ * switch (which doesn't change the getter's value) is never clobbered.
57
+ *
58
+ * Must be called during component initialisation.
59
+ */
60
+ export declare function provideI18n(locale: Locale | (() => Locale), fallbackLocale?: Locale): I18nState;
61
+ /**
62
+ * General i18n hook for locale control and locale-aware formatting/resolution.
63
+ *
64
+ * Read paths (`locale`, `t`, formatters) are tolerant: without a provider they
65
+ * resolve against {@link BASE_LOCALE}. The write path (`setLocale`) is strict:
66
+ * without a provider it throws, because there is no request-scoped state to
67
+ * mutate — "you asked to switch the language but never mounted an
68
+ * `<I18nProvider>`". Call during component initialisation.
69
+ */
70
+ export interface I18nApi {
71
+ /** Active locale (reactive). Falls back to {@link BASE_LOCALE} without a provider. */
72
+ readonly locale: Locale;
73
+ /** Locales with registered data or a loader (reactive). */
74
+ readonly availableLocales: Locale[];
75
+ /** Whether a lazy locale load is in flight (reactive). */
76
+ readonly isLoading: boolean;
77
+ /** Switch the active locale. Throws without a provider. */
78
+ setLocale(locale: Locale): boolean;
79
+ t(key: string, params?: TranslationParams, options?: TranslationOptions | string): string;
80
+ plural(key: string, params: PluralParams, options?: TranslationOptions): string;
81
+ exists(key: string, packageName?: string): boolean;
82
+ formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
83
+ formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string;
84
+ formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit): string;
85
+ formatTimeAgo(date: Date): string;
86
+ }
87
+ export declare function useI18n(): I18nApi;
88
+ export interface I18nConfigureOptions {
89
+ /**
90
+ * Routes i18n errors — a lazy locale load that rejects (`load-failed`,
91
+ * `load-failed-no-fallback`), or a `setLocale` with an unsupported code
92
+ * (`unsupported-locale`) — to your handler instead of the default
93
+ * `console.warn`. The hook for telemetry (Sentry, structured logging).
94
+ */
95
+ onError?: (error: I18nError) => void;
96
+ }
97
+ /**
98
+ * App-global i18n configuration. Call **once at startup** (module scope or root
99
+ * setup), not per-request: the handler lives on the process-wide registry, so a
100
+ * per-request assignment under concurrent SSR would race. Without it, errors fall
101
+ * back to `console.warn`.
102
+ *
103
+ * ```ts
104
+ * configureI18n({ onError: (e) => reportToSentry(e) });
105
+ * ```
106
+ */
107
+ export declare function configureI18n(options: I18nConfigureOptions): void;
@@ -0,0 +1,167 @@
1
+ import { getContext, hasContext, setContext, untrack } from 'svelte';
2
+ import { getRegistry } from './registry.svelte';
3
+ import { isLocaleSupported } from './types';
4
+ /**
5
+ * The constant base locale used for read-tolerant resolution when no
6
+ * `<I18nProvider>` is mounted. A constant — never global mutable state — so a
7
+ * provider-less render is SSR-safe and identical on server and client (no
8
+ * hydration mismatch), and reads never leak across requests. Consumers who need
9
+ * a different or switchable locale mount a provider.
10
+ */
11
+ export const BASE_LOCALE = 'en';
12
+ /**
13
+ * Request-scoped reactive locale state. The *only* mutable per-request i18n
14
+ * value — created by `<I18nProvider>` and read through the context. Encapsulated:
15
+ * `locale` is exposed read-only; switching goes through `setLocale`.
16
+ */
17
+ export class I18nState {
18
+ // Private $state field: external code can read `.locale` but must go through
19
+ // `setLocale` to change it (validation + lazy-load trigger live there).
20
+ #locale = $state(BASE_LOCALE);
21
+ /** Fallback locale used when a key is missing in the active locale. */
22
+ fallbackLocale;
23
+ constructor(locale = BASE_LOCALE, fallbackLocale = BASE_LOCALE) {
24
+ this.#locale = locale;
25
+ this.fallbackLocale = fallbackLocale;
26
+ }
27
+ get locale() {
28
+ return this.#locale;
29
+ }
30
+ /**
31
+ * Switch the active locale in place (reactive — no reload). Returns `false`
32
+ * (and reports `unsupported-locale`) for an unsupported locale, without
33
+ * switching. For a supported locale the switch is applied immediately and
34
+ * `true` is returned ("switch initiated"): if a lazy loader is registered and
35
+ * the data isn't present yet, the load is triggered (not awaited) so the
36
+ * `$derived` reads re-resolve once the chunk lands. Use `registry.loadLocale`
37
+ * directly when you need to await.
38
+ *
39
+ * The async failure is not silent: if that load rejects AND no data exists for
40
+ * the locale (so reads can't even fall back to it), `load-failed-no-fallback`
41
+ * is reported to the error sink — the loud signal that "the language you
42
+ * switched to can't be rendered". (No-op today, where all bundles are eager.)
43
+ */
44
+ setLocale(locale) {
45
+ const registry = getRegistry();
46
+ if (!isLocaleSupported(locale)) {
47
+ registry.reportLoadError({ type: 'unsupported-locale', locale });
48
+ return false;
49
+ }
50
+ if (registry.hasLoader(locale) && !registry.isLoaded(locale)) {
51
+ // loadLocale never rejects (it reports `load-failed` internally); inspect
52
+ // its boolean result to surface the harder "switched but unrenderable" case.
53
+ registry.loadLocale(locale).then((ok) => {
54
+ if (!ok && !registry.hasTranslations(locale)) {
55
+ registry.reportLoadError({ type: 'load-failed-no-fallback', locale });
56
+ }
57
+ });
58
+ }
59
+ this.#locale = locale;
60
+ return true;
61
+ }
62
+ }
63
+ // Symbol key + explicit typed accessors rather than svelte's `createContext`:
64
+ // its generated getter calls `e.missing_context()` (throws) when no provider is
65
+ // mounted, which is incompatible with the read-tolerant contract below (a
66
+ // provider-less <Button> must still render its ARIA strings in baseLocale). A
67
+ // Symbol is collision-free and type-safe — the convention's intent ("no string
68
+ // keys") holds; only the throw-on-missing semantics are traded for tolerance.
69
+ const I18N_CONTEXT_KEY = Symbol('urbicon-ui-i18n');
70
+ /** Set the request-scoped locale state. Called by `<I18nProvider>`. */
71
+ export function provideI18nState(state) {
72
+ return setContext(I18N_CONTEXT_KEY, state);
73
+ }
74
+ /**
75
+ * Read the request-scoped locale state, or `undefined` when no `<I18nProvider>`
76
+ * is mounted above (read-tolerant). Must be called during component init.
77
+ */
78
+ export function useI18nState() {
79
+ return hasContext(I18N_CONTEXT_KEY) ? getContext(I18N_CONTEXT_KEY) : undefined;
80
+ }
81
+ /**
82
+ * Provide the request-scoped locale state from a component's own init, and return
83
+ * it. This is the primitive behind `<I18nProvider>`; reach for it directly when
84
+ * the **same** component both provides i18n and renders translated content —
85
+ * e.g. a root `+layout.svelte` whose own chrome uses `t`. (A child
86
+ * `<I18nProvider>` cannot serve the parent that mounts it, because context only
87
+ * flows downward; calling `provideI18n` in the parent's script puts the state on
88
+ * that component's own context map, which its own `useI18n()`/`use<Pkg>I18n()`
89
+ * then reads.)
90
+ *
91
+ * Pass `locale` as a reactive getter (`() => data.locale`) to keep it controlled:
92
+ * a prop/load change is synced into the state, while an in-place `setLocale`
93
+ * switch (which doesn't change the getter's value) is never clobbered.
94
+ *
95
+ * Must be called during component initialisation.
96
+ */
97
+ export function provideI18n(locale, fallbackLocale = BASE_LOCALE) {
98
+ const getLocale = typeof locale === 'function' ? locale : () => locale;
99
+ const state = new I18nState(untrack(getLocale), fallbackLocale);
100
+ provideI18nState(state);
101
+ if (typeof locale === 'function') {
102
+ // Controlled sync: depend on the getter only (untrack the state read) so a
103
+ // prop change flows in but setLocale() is not reverted.
104
+ $effect(() => {
105
+ const next = getLocale();
106
+ if (next !== untrack(() => state.locale)) {
107
+ state.setLocale(next);
108
+ }
109
+ });
110
+ }
111
+ // Ensure the initial active + fallback locales' lazy bundles are loaded (WP4) —
112
+ // a no-op when everything is eager. Client-only (effects don't run during SSR),
113
+ // so a lazy non-base initial locale renders the fallback on the server and the
114
+ // first client paint, then re-resolves once the chunk lands. Subsequent switches
115
+ // load via setLocale. Read untracked → runs once on mount, not on every switch.
116
+ $effect(() => {
117
+ const registry = getRegistry();
118
+ void registry.loadLocale(untrack(() => state.locale));
119
+ void registry.loadLocale(state.fallbackLocale);
120
+ });
121
+ return state;
122
+ }
123
+ export function useI18n() {
124
+ const state = useI18nState();
125
+ const registry = getRegistry();
126
+ const localeOf = () => state?.locale ?? BASE_LOCALE;
127
+ const fallbackOf = () => state?.fallbackLocale ?? BASE_LOCALE;
128
+ return {
129
+ get locale() {
130
+ return localeOf();
131
+ },
132
+ get availableLocales() {
133
+ return registry.getAvailableLocales();
134
+ },
135
+ get isLoading() {
136
+ return registry.isLoading;
137
+ },
138
+ setLocale(locale) {
139
+ if (!state) {
140
+ throw new Error('[i18n] setLocale() requires an <I18nProvider>. Without a provider the locale ' +
141
+ 'is the constant base locale and cannot change. Wrap your app root in ' +
142
+ '<I18nProvider locale={…}> to enable locale switching.');
143
+ }
144
+ return state.setLocale(locale);
145
+ },
146
+ t: (key, params, options) => registry.translate(key, localeOf(), fallbackOf(), params, options),
147
+ plural: (key, params, options) => registry.pluralize(key, params, localeOf(), fallbackOf(), options),
148
+ exists: (key, packageName) => registry.exists(key, localeOf(), packageName),
149
+ formatNumber: (value, options) => registry.formatNumber(value, localeOf(), options),
150
+ formatDate: (date, options) => registry.formatDate(date, localeOf(), options),
151
+ formatRelativeTime: (value, unit) => registry.formatRelativeTime(value, unit, localeOf()),
152
+ formatTimeAgo: (date) => registry.formatTimeAgo(date, localeOf(), fallbackOf())
153
+ };
154
+ }
155
+ /**
156
+ * App-global i18n configuration. Call **once at startup** (module scope or root
157
+ * setup), not per-request: the handler lives on the process-wide registry, so a
158
+ * per-request assignment under concurrent SSR would race. Without it, errors fall
159
+ * back to `console.warn`.
160
+ *
161
+ * ```ts
162
+ * configureI18n({ onError: (e) => reportToSentry(e) });
163
+ * ```
164
+ */
165
+ export function configureI18n(options) {
166
+ getRegistry().onError = options.onError;
167
+ }
@@ -0,0 +1,118 @@
1
+ import type { CreatePackageTypes, Locale, PackageI18n, PackageTranslations, TranslationParams, TranslationSchema, Translations, TypedTranslationFunction } from './types';
2
+ export interface CreatePackageI18nOptions {
3
+ /**
4
+ * Per-locale lazy loaders (WP4 code-splitting). Each returns the package's
5
+ * bundle for that locale as a dynamic-import chunk, e.g.
6
+ * `() => import('./translations/de').then((m) => m.default)`. Listed locales
7
+ * stay out of the initial bundle until activated by the provider / `setLocale`;
8
+ * the eager bundle passed in `translations` (typically `en`) is the base/
9
+ * fallback. Compile-time key parity is NOT checked for lazy locales — pair with
10
+ * `validatePackageTranslations` in a test to guard parity at runtime/CI.
11
+ */
12
+ loaders?: Partial<Record<Locale, () => Promise<Translations>>>;
13
+ }
14
+ /**
15
+ * Creates a standardized i18n integration for a package.
16
+ *
17
+ * Generic over the `en` bundle: with `<const T>` the literal key/param types of
18
+ * `en` flow through `PackageI18n<T>` into a fully typed `t` — real `DeepKeys<T>`
19
+ * plus `ExtractParams`, so `t('dialog.close')` autocompletes and
20
+ * `t('dialog.nope')` is a compile error. Other locales are checked against
21
+ * `TranslationSchema<T>` (T's structure with its string values widened), which
22
+ * enforces key parity at compile time while allowing locale-specific strings.
23
+ *
24
+ * Opt-in code-splitting (WP4): pass `options.loaders` to keep non-base locales
25
+ * out of the initial bundle as dynamic-import chunks, loaded only when activated.
26
+ */
27
+ export declare function createPackageI18n<const T extends Translations>(packageName: string, translations: {
28
+ en: T;
29
+ } & Partial<Record<Locale, TranslationSchema<T>>>, options?: CreatePackageI18nOptions): PackageI18n<T>;
30
+ /**
31
+ * Creates a package-translations descriptor (data only — does NOT register).
32
+ *
33
+ * @deprecated Superseded by {@link createPackageI18n}, which registers and
34
+ * returns a typed `t` in one step. Retained for back-compat; the `types` field
35
+ * is a non-functional placeholder (`CreatePackageTypes<Translations>` degenerates
36
+ * to `string` keys, predating the generic factory).
37
+ */
38
+ export declare function createPackageTranslations(packageName: string, translations: Partial<Record<Locale, Translations>>): {
39
+ packageName: string;
40
+ translations: Partial<Record<"en" | "de" | "fr" | "es" | "it" | "nl", Translations>>;
41
+ types: CreatePackageTypes<Translations>;
42
+ };
43
+ /**
44
+ * Creates a typed translation package with auto-registration.
45
+ *
46
+ * @deprecated Use {@link createPackageI18n} directly. Since that factory became
47
+ * generic (`<const T>`) it already infers literal keys and returns a fully typed
48
+ * `t`; this wrapper adds only a redundant `tt` alias and a `packageName`/
49
+ * `translations` passthrough. Kept as a thin, typed shim for back-compat — it
50
+ * will be removed in a future major.
51
+ */
52
+ export declare function createTypedPackage<const T extends Translations>(packageName: string, translations: {
53
+ en: T;
54
+ } & Partial<Record<Locale, TranslationSchema<T>>>): {
55
+ packageName: string;
56
+ translations: {
57
+ en: T;
58
+ } & Partial<Record<"en" | "de" | "fr" | "es" | "it" | "nl", TranslationSchema<T>>>;
59
+ tt: TypedTranslationFunction<T>;
60
+ useTranslate: () => TypedTranslationFunction<T>;
61
+ t: TypedTranslationFunction<T>;
62
+ exists: (key: string) => boolean;
63
+ getLocales: () => Locale[];
64
+ register: () => void;
65
+ types: CreatePackageTypes<T>;
66
+ };
67
+ /**
68
+ * Register translation loaders for lazy loading
69
+ * Useful for larger packages with many translations
70
+ */
71
+ export declare function registerTranslationLoaders(loaders: Record<Locale, () => Promise<Translations>>): void;
72
+ /**
73
+ * Smart component integration helper
74
+ * Provides common patterns for component i18n integration
75
+ */
76
+ export declare function createComponentI18n<const T extends Translations>(packageName: string, translations: {
77
+ en: T;
78
+ } & Partial<Record<Locale, TranslationSchema<T>>>, defaultOptions?: {
79
+ useI18n?: boolean;
80
+ fallbackToGlobal?: boolean;
81
+ }): {
82
+ getText: (key: string, customText?: string, params?: TranslationParams, options?: {
83
+ useI18n?: boolean;
84
+ }) => string;
85
+ maybeT: (key: string, params?: TranslationParams, options?: {
86
+ useI18n?: boolean;
87
+ fallback?: string;
88
+ }) => string;
89
+ useTranslate: () => TypedTranslationFunction<T>;
90
+ t: TypedTranslationFunction<T>;
91
+ exists: (key: string) => boolean;
92
+ getLocales: () => Locale[];
93
+ register: () => void;
94
+ types: CreatePackageTypes<T>;
95
+ };
96
+ /**
97
+ * Batch register multiple packages
98
+ * Useful for apps that use many packages
99
+ */
100
+ export declare function registerPackages(packages: Array<{
101
+ name: string;
102
+ translations: PackageTranslations;
103
+ }>): void;
104
+ /**
105
+ * Validates deep-key parity across a package's locale bundles.
106
+ *
107
+ * Compares the full recursive leaf-key set (not just top-level keys) of every
108
+ * locale against the base locale (`en` first by convention). A missing nested
109
+ * key is an error; an extra nested key is a warning. Pair with a per-package
110
+ * vitest assertion (`expect(errors).toEqual([])`) to fail CI on drift —
111
+ * complementing the compile-time parity `satisfies`/generic factory enforce for
112
+ * statically-typed bundles, and covering dynamically/lazily loaded ones.
113
+ */
114
+ export declare function validatePackageTranslations(packageName: string, translations: Partial<Record<Locale, Translations>>): {
115
+ isValid: boolean;
116
+ errors: string[];
117
+ warnings: string[];
118
+ };
@@ -0,0 +1,211 @@
1
+ import { collectDeepKeys } from '../utils/deep-keys';
2
+ import { BASE_LOCALE, useI18nState } from './context.svelte';
3
+ import { getRegistry } from './registry.svelte';
4
+ /**
5
+ * Creates a standardized i18n integration for a package.
6
+ *
7
+ * Generic over the `en` bundle: with `<const T>` the literal key/param types of
8
+ * `en` flow through `PackageI18n<T>` into a fully typed `t` — real `DeepKeys<T>`
9
+ * plus `ExtractParams`, so `t('dialog.close')` autocompletes and
10
+ * `t('dialog.nope')` is a compile error. Other locales are checked against
11
+ * `TranslationSchema<T>` (T's structure with its string values widened), which
12
+ * enforces key parity at compile time while allowing locale-specific strings.
13
+ *
14
+ * Opt-in code-splitting (WP4): pass `options.loaders` to keep non-base locales
15
+ * out of the initial bundle as dynamic-import chunks, loaded only when activated.
16
+ */
17
+ export function createPackageI18n(packageName, translations, options) {
18
+ // Eager registration at module-init time. The previous lazy variant
19
+ // (queueMicrotask inside t()) returned the raw key on first call and
20
+ // never re-triggered the reactive expression that read it, so consumers
21
+ // saw `filter.button.add` instead of the translated string.
22
+ //
23
+ // Running synchronously here is safe: `createPackageI18n` is invoked at
24
+ // module top-level (`export const tableI18n = createPackageI18n(...)`),
25
+ // which is outside any $derived/$effect — so the SvelteMap mutation in
26
+ // `registerPackage` cannot trip the `state_unsafe_mutation` rule.
27
+ //
28
+ // Goes through `getRegistry()` (a hoisted function), NOT a module-const binding:
29
+ // this call fires at consumer module-eval time, and under Vite 8 / Rolldown a
30
+ // reordered chunk can run it before the registry module's body ran. A hoisted
31
+ // function binding survives that; the lazy getter then builds the registry on
32
+ // first touch, in whatever order the chunks happen to fire.
33
+ getRegistry().registerPackage(packageName, translations);
34
+ // Opt-in lazy locales (WP4): register dynamic-import loaders. The eager bundle
35
+ // above is the base; these cover the rest, loaded on demand by the provider /
36
+ // setLocale. Parity for lazy bundles is a runtime concern (validatePackageTranslations).
37
+ if (options?.loaders) {
38
+ const registry = getRegistry();
39
+ for (const [locale, loader] of Object.entries(options.loaders)) {
40
+ if (loader)
41
+ registry.registerPackageLoader(packageName, locale, loader);
42
+ }
43
+ }
44
+ // Context-scoped hook — the SSR-correct, reactive accessor. Captures the
45
+ // request-scoped locale state at component init (or `undefined` without a
46
+ // provider → base locale), then resolves against the static registry. Reading
47
+ // `state.locale` inside the returned closure (called from a `$derived`) makes
48
+ // call-sites re-render on locale change; reading the registry's SvelteMap makes
49
+ // them re-render when a package registers more translations.
50
+ const useTranslate = () => {
51
+ const state = useI18nState();
52
+ const registry = getRegistry();
53
+ return ((key, params, options) => registry.translate(key, state?.locale ?? BASE_LOCALE, state?.fallbackLocale ?? BASE_LOCALE, params, { packageName, ...options }));
54
+ };
55
+ // Non-hook `t` for non-component use (tests, server utilities). Resolves
56
+ // against the base locale — there is no request-scoped state outside a
57
+ // component. Components use `useTranslate` for the reactive, provider-scoped
58
+ // locale.
59
+ const t = ((key, params, options) => getRegistry().translate(key, BASE_LOCALE, BASE_LOCALE, params, {
60
+ packageName,
61
+ ...options
62
+ }));
63
+ const exists = (key) => getRegistry().exists(key, BASE_LOCALE, packageName);
64
+ const getLocales = () => getRegistry().getPackageLocales(packageName);
65
+ // No-op kept for API back-compat with callers that used to invoke `register()`
66
+ // before reading translations. Registration now happens eagerly above.
67
+ const register = () => { };
68
+ return {
69
+ useTranslate,
70
+ t,
71
+ exists,
72
+ getLocales,
73
+ register,
74
+ types: {}
75
+ };
76
+ }
77
+ /**
78
+ * Creates a package-translations descriptor (data only — does NOT register).
79
+ *
80
+ * @deprecated Superseded by {@link createPackageI18n}, which registers and
81
+ * returns a typed `t` in one step. Retained for back-compat; the `types` field
82
+ * is a non-functional placeholder (`CreatePackageTypes<Translations>` degenerates
83
+ * to `string` keys, predating the generic factory).
84
+ */
85
+ export function createPackageTranslations(packageName, translations) {
86
+ return {
87
+ packageName,
88
+ translations,
89
+ types: {}
90
+ };
91
+ }
92
+ /**
93
+ * Creates a typed translation package with auto-registration.
94
+ *
95
+ * @deprecated Use {@link createPackageI18n} directly. Since that factory became
96
+ * generic (`<const T>`) it already infers literal keys and returns a fully typed
97
+ * `t`; this wrapper adds only a redundant `tt` alias and a `packageName`/
98
+ * `translations` passthrough. Kept as a thin, typed shim for back-compat — it
99
+ * will be removed in a future major.
100
+ */
101
+ export function createTypedPackage(packageName, translations) {
102
+ const packageI18n = createPackageI18n(packageName, translations);
103
+ return {
104
+ ...packageI18n,
105
+ packageName,
106
+ translations,
107
+ // Convenient alias
108
+ tt: packageI18n.t
109
+ };
110
+ }
111
+ /**
112
+ * Register translation loaders for lazy loading
113
+ * Useful for larger packages with many translations
114
+ */
115
+ export function registerTranslationLoaders(loaders) {
116
+ Object.entries(loaders).forEach(([locale, loader]) => {
117
+ getRegistry().registerTranslationLoader(locale, loader);
118
+ });
119
+ }
120
+ /**
121
+ * Smart component integration helper
122
+ * Provides common patterns for component i18n integration
123
+ */
124
+ export function createComponentI18n(packageName, translations, defaultOptions) {
125
+ const packageI18n = createPackageI18n(packageName, translations);
126
+ return {
127
+ ...packageI18n,
128
+ // Smart text resolution for components
129
+ getText: (key, customText, params, options) => {
130
+ // If custom text provided and i18n disabled, use custom text
131
+ if (customText && options?.useI18n === false) {
132
+ return customText;
133
+ }
134
+ const loose = packageI18n.t;
135
+ return loose(key, params, {
136
+ fallbackToGlobal: defaultOptions?.fallbackToGlobal ?? true
137
+ });
138
+ },
139
+ // Conditional translation (useful for optional i18n)
140
+ maybeT: (key, params, options) => {
141
+ if (options?.useI18n === false) {
142
+ return options.fallback || key;
143
+ }
144
+ const loose = packageI18n.t;
145
+ return loose(key, params);
146
+ }
147
+ };
148
+ }
149
+ /**
150
+ * Batch register multiple packages
151
+ * Useful for apps that use many packages
152
+ */
153
+ export function registerPackages(packages) {
154
+ packages.forEach(({ name, translations }) => {
155
+ getRegistry().registerPackage(name, translations);
156
+ });
157
+ }
158
+ /**
159
+ * Validates deep-key parity across a package's locale bundles.
160
+ *
161
+ * Compares the full recursive leaf-key set (not just top-level keys) of every
162
+ * locale against the base locale (`en` first by convention). A missing nested
163
+ * key is an error; an extra nested key is a warning. Pair with a per-package
164
+ * vitest assertion (`expect(errors).toEqual([])`) to fail CI on drift —
165
+ * complementing the compile-time parity `satisfies`/generic factory enforce for
166
+ * statically-typed bundles, and covering dynamically/lazily loaded ones.
167
+ */
168
+ // Non-generic on purpose: this is a runtime structural check. A generic
169
+ // `<T>(…: Partial<Record<Locale, T>>)` would bind every locale to the SAME `T`
170
+ // inferred from `en`, so a `de` bundle with different string literals (the
171
+ // normal case under `as const`) fails to assign. `Translations` widens the
172
+ // values, decoupling the locales — parity is verified at runtime via keys.
173
+ export function validatePackageTranslations(packageName, translations) {
174
+ const errors = [];
175
+ const warnings = [];
176
+ const locales = Object.keys(translations);
177
+ if (locales.length === 0) {
178
+ errors.push(`[${packageName}] No translations provided`);
179
+ return { isValid: false, errors, warnings };
180
+ }
181
+ // Prefer `en` as the base: the error (missing) vs warning (extra) asymmetry
182
+ // is only meaningful against the canonical source locale, not whichever key
183
+ // Object.keys happens to return first.
184
+ const baseLocale = locales.includes('en') ? 'en' : locales[0];
185
+ const baseBundle = translations[baseLocale];
186
+ if (!baseBundle) {
187
+ errors.push(`[${packageName}] Base locale ${baseLocale} has no translations`);
188
+ return { isValid: false, errors, warnings };
189
+ }
190
+ const baseKeys = collectDeepKeys(baseBundle);
191
+ const baseKeySet = new Set(baseKeys);
192
+ locales.slice(1).forEach((locale) => {
193
+ const bundle = translations[locale];
194
+ if (!bundle)
195
+ return;
196
+ const localeKeySet = new Set(collectDeepKeys(bundle));
197
+ const missingKeys = baseKeys.filter((key) => !localeKeySet.has(key));
198
+ const extraKeys = [...localeKeySet].filter((key) => !baseKeySet.has(key));
199
+ if (missingKeys.length > 0) {
200
+ errors.push(`[${packageName}] Locale ${locale} missing keys: ${missingKeys.join(', ')}`);
201
+ }
202
+ if (extraKeys.length > 0) {
203
+ warnings.push(`[${packageName}] Locale ${locale} has extra keys: ${extraKeys.join(', ')}`);
204
+ }
205
+ });
206
+ return {
207
+ isValid: errors.length === 0,
208
+ errors,
209
+ warnings
210
+ };
211
+ }