@urbicon-ui/i18n 6.34.0 → 6.35.0
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.
package/README.md
CHANGED
|
@@ -107,9 +107,12 @@ Each Urbicon package registers its own namespaced keys so consumers get a merged
|
|
|
107
107
|
// Inside @urbicon-ui/blocks — src/lib/i18n/index.ts
|
|
108
108
|
import { createPackageI18n } from '@urbicon-ui/i18n';
|
|
109
109
|
import en from '../translations/en';
|
|
110
|
-
import de from '../translations/de';
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
// en is the eager base; de is a lazy dynamic-import loader (see "Locale
|
|
112
|
+
// code-splitting" below), so English-only apps never bundle the de catalog.
|
|
113
|
+
export const blocksI18n = createPackageI18n('blocks', { en }, {
|
|
114
|
+
loaders: { de: () => import('../translations/de').then((m) => m.default) }
|
|
115
|
+
});
|
|
113
116
|
|
|
114
117
|
// The context-scoped hook (re-exported for components)
|
|
115
118
|
export const useBlocksI18n = blocksI18n.useTranslate;
|
|
@@ -205,7 +208,25 @@ export const blocksI18n = createPackageI18n(
|
|
|
205
208
|
);
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
Vite/Rollup splits each dynamic import into its own chunk, so only the active locale is in the initial bundle. The provider loads the active + fallback locale on mount; `setLocale` loads a target on switch. A lazy non-base initial locale renders the fallback until its chunk lands, then re-resolves reactively.
|
|
211
|
+
Vite/Rollup splits each dynamic import into its own chunk, so only the active locale is in the initial bundle. The provider loads the active + fallback locale on mount; `setLocale` loads a target on switch. A lazy non-base initial locale renders the fallback until its chunk lands, then re-resolves reactively.
|
|
212
|
+
|
|
213
|
+
> **`@urbicon-ui/blocks` ships this way** — `en` eager, `de` lazy — so an English-only app doesn't bundle the `de` catalog.
|
|
214
|
+
|
|
215
|
+
### SSR: eager-register the lazy locale for non-base apps
|
|
216
|
+
|
|
217
|
+
The provider's on-mount load runs in a **client-only** `$effect`. So under SSR a lazy non-base initial locale (e.g. a German app) renders the *fallback* (English) on the server and the first client paint, then flips to German once the chunk lands — a text flash and a possible hydration text mismatch. That is not acceptable as the default for a server-rendered app in that locale.
|
|
218
|
+
|
|
219
|
+
The fix is to register the bundle **eagerly, once at server/app start**. The registry is module-global and holds only static, request-identical translation data, so a single startup registration is SSR-safe (it carries no per-request state). Every package factory returns `registerLocale(locale, bundle)` for this; `@urbicon-ui/blocks` re-exports it as `registerBlocksLocale`:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// src/hooks.server.ts (or any module evaluated once at server start)
|
|
223
|
+
import { registerBlocksLocale } from '@urbicon-ui/blocks';
|
|
224
|
+
import de from '@urbicon-ui/blocks/i18n/de'; // the public per-locale subpath
|
|
225
|
+
|
|
226
|
+
registerBlocksLocale('de', de);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`registerLocale` is **additive** (it merges the locale in without dropping the eager base) and **write-strict** (throws on an unsupported locale or a non-object bundle). The `de` catalog stays out of English-only client bundles — you only pull it in where it is actually rendered. Worth the loader split past a handful of locales; a fully eager `createPackageI18n(name, { en, de })` is simpler when you always ship both.
|
|
209
230
|
|
|
210
231
|
## Coexisting with an app-level i18n (e.g. Paraglide)
|
|
211
232
|
|
|
@@ -244,10 +265,11 @@ configureI18n({ onError: (e) => reportToSentry(e) });
|
|
|
244
265
|
|
|
245
266
|
```ts
|
|
246
267
|
import { validatePackageTranslations } from '@urbicon-ui/i18n';
|
|
247
|
-
import
|
|
268
|
+
import en from '$lib/translations/en';
|
|
269
|
+
import de from '$lib/translations/de'; // import lazy bundles directly for the check
|
|
248
270
|
|
|
249
271
|
it('en/de key parity', () => {
|
|
250
|
-
expect(validatePackageTranslations('blocks',
|
|
272
|
+
expect(validatePackageTranslations('blocks', { en, de }).errors).toEqual([]);
|
|
251
273
|
});
|
|
252
274
|
```
|
|
253
275
|
|
|
@@ -62,6 +62,7 @@ export declare function createTypedPackage<const T extends Translations>(package
|
|
|
62
62
|
exists: (key: string) => boolean;
|
|
63
63
|
getLocales: () => Locale[];
|
|
64
64
|
register: () => void;
|
|
65
|
+
registerLocale: (locale: Locale, bundle: Translations) => void;
|
|
65
66
|
types: CreatePackageTypes<T>;
|
|
66
67
|
};
|
|
67
68
|
/**
|
|
@@ -91,6 +92,7 @@ export declare function createComponentI18n<const T extends Translations>(packag
|
|
|
91
92
|
exists: (key: string) => boolean;
|
|
92
93
|
getLocales: () => Locale[];
|
|
93
94
|
register: () => void;
|
|
95
|
+
registerLocale: (locale: Locale, bundle: Translations) => void;
|
|
94
96
|
types: CreatePackageTypes<T>;
|
|
95
97
|
};
|
|
96
98
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectDeepKeys } from '../utils/deep-keys.js';
|
|
2
2
|
import { BASE_LOCALE, useI18nState } from './context.svelte.js';
|
|
3
3
|
import { getRegistry } from './registry.svelte.js';
|
|
4
|
+
import { isLocaleSupported, SUPPORTED_LOCALES } from './types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Creates a standardized i18n integration for a package.
|
|
6
7
|
*
|
|
@@ -83,12 +84,38 @@ export function createPackageI18n(packageName, translations, options) {
|
|
|
83
84
|
// the first useTranslate()/t() (e.g. to preload a lazy locale) can call this.
|
|
84
85
|
// Previously a no-op; now it performs the idempotent first-use registration.
|
|
85
86
|
const register = () => ensureRegistered();
|
|
87
|
+
// Eager, additive registration of one locale's bundle — the SSR escape hatch for
|
|
88
|
+
// a locale that is otherwise declared as a lazy `options.loaders` entry. The
|
|
89
|
+
// provider only loads lazy chunks in a client-only `$effect`, so a lazy non-base
|
|
90
|
+
// locale renders the *fallback* (base) locale during SSR and the first client
|
|
91
|
+
// paint, then flips once the chunk lands. A server-rendered app in that locale
|
|
92
|
+
// therefore ships the wrong-language strings and risks a hydration text
|
|
93
|
+
// mismatch. Registering the imported bundle here — once at server/app start,
|
|
94
|
+
// where it is request-identical static data on the module-global registry —
|
|
95
|
+
// makes the locale present for the very first render instead. Merges (does NOT
|
|
96
|
+
// clobber the eager base bundle) via registry.registerPackageLocale.
|
|
97
|
+
//
|
|
98
|
+
// Write-strict: throws on an unsupported locale or a non-object bundle rather
|
|
99
|
+
// than silently registering garbage under a bogus key.
|
|
100
|
+
const registerLocale = (locale, bundle) => {
|
|
101
|
+
if (!isLocaleSupported(locale)) {
|
|
102
|
+
throw new Error(`[i18n] ${packageName}.registerLocale: unsupported locale "${String(locale)}". ` +
|
|
103
|
+
`Supported: ${SUPPORTED_LOCALES.join(', ')}.`);
|
|
104
|
+
}
|
|
105
|
+
if (!bundle || typeof bundle !== 'object' || Array.isArray(bundle)) {
|
|
106
|
+
const kind = bundle === null ? 'null' : Array.isArray(bundle) ? 'array' : typeof bundle;
|
|
107
|
+
throw new Error(`[i18n] ${packageName}.registerLocale("${locale}"): bundle must be a translations object, got ${kind}.`);
|
|
108
|
+
}
|
|
109
|
+
ensureRegistered();
|
|
110
|
+
getRegistry().registerPackageLocale(packageName, locale, bundle);
|
|
111
|
+
};
|
|
86
112
|
return {
|
|
87
113
|
useTranslate,
|
|
88
114
|
t,
|
|
89
115
|
exists,
|
|
90
116
|
getLocales,
|
|
91
117
|
register,
|
|
118
|
+
registerLocale,
|
|
92
119
|
types: {}
|
|
93
120
|
};
|
|
94
121
|
}
|
|
@@ -53,6 +53,24 @@ export declare class I18nRegistry {
|
|
|
53
53
|
* initial chunk until the locale is activated.
|
|
54
54
|
*/
|
|
55
55
|
registerPackageLoader(packageName: string, locale: Locale, loader: () => Promise<Translations>): void;
|
|
56
|
+
/**
|
|
57
|
+
* Additively register one locale's already-loaded bundle for a package — the
|
|
58
|
+
* synchronous, eager counterpart to {@link loadPackageLocale}.
|
|
59
|
+
*
|
|
60
|
+
* Unlike {@link registerPackage} (which `.set`s the *whole* package entry and so
|
|
61
|
+
* drops any sibling locale already registered), this **merges** the single
|
|
62
|
+
* locale into the existing entry, preserving the eager base bundle. That makes
|
|
63
|
+
* it the correct primitive for an eager-register escape hatch: a consumer that
|
|
64
|
+
* has passed a locale as a lazy `loader` can register its imported bundle up
|
|
65
|
+
* front (e.g. once at SSR/app start) so the very first server render already
|
|
66
|
+
* resolves that locale — no fallback-locale flash, no hydration text mismatch —
|
|
67
|
+
* instead of waiting for the provider's client-only on-mount chunk load.
|
|
68
|
+
*
|
|
69
|
+
* A fresh object reference is written so the reactive SvelteMap notifies
|
|
70
|
+
* `$derived` readers (an in-place mutation would not). Idempotent-friendly:
|
|
71
|
+
* re-registering the same locale simply overwrites it with identical data.
|
|
72
|
+
*/
|
|
73
|
+
registerPackageLocale(packageName: string, locale: Locale, data: Translations): void;
|
|
56
74
|
hasLoader(locale: Locale): boolean;
|
|
57
75
|
isLoaded(locale: Locale): boolean;
|
|
58
76
|
private loaderKeyLocale;
|
|
@@ -109,6 +109,28 @@ export class I18nRegistry {
|
|
|
109
109
|
registerPackageLoader(packageName, locale, loader) {
|
|
110
110
|
this.packageLoaders.set(`${packageName}::${locale}`, loader);
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Additively register one locale's already-loaded bundle for a package — the
|
|
114
|
+
* synchronous, eager counterpart to {@link loadPackageLocale}.
|
|
115
|
+
*
|
|
116
|
+
* Unlike {@link registerPackage} (which `.set`s the *whole* package entry and so
|
|
117
|
+
* drops any sibling locale already registered), this **merges** the single
|
|
118
|
+
* locale into the existing entry, preserving the eager base bundle. That makes
|
|
119
|
+
* it the correct primitive for an eager-register escape hatch: a consumer that
|
|
120
|
+
* has passed a locale as a lazy `loader` can register its imported bundle up
|
|
121
|
+
* front (e.g. once at SSR/app start) so the very first server render already
|
|
122
|
+
* resolves that locale — no fallback-locale flash, no hydration text mismatch —
|
|
123
|
+
* instead of waiting for the provider's client-only on-mount chunk load.
|
|
124
|
+
*
|
|
125
|
+
* A fresh object reference is written so the reactive SvelteMap notifies
|
|
126
|
+
* `$derived` readers (an in-place mutation would not). Idempotent-friendly:
|
|
127
|
+
* re-registering the same locale simply overwrites it with identical data.
|
|
128
|
+
*/
|
|
129
|
+
registerPackageLocale(packageName, locale, data) {
|
|
130
|
+
const existing = this.packageTranslations.get(packageName) ?? {};
|
|
131
|
+
this.packageTranslations.set(packageName, { ...existing, [locale]: data });
|
|
132
|
+
this.addTranslations(locale, { [packageName]: data });
|
|
133
|
+
}
|
|
112
134
|
hasLoader(locale) {
|
|
113
135
|
if (this.translationLoaders.has(locale))
|
|
114
136
|
return true;
|
|
@@ -190,15 +212,13 @@ export class I18nRegistry {
|
|
|
190
212
|
this.loadingPackageLocales.add(key);
|
|
191
213
|
try {
|
|
192
214
|
const data = await loader();
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// await, so two concurrent loads of different non-base locales for the same
|
|
215
|
+
// Delegate the additive merge to registerPackageLocale (same primitive the
|
|
216
|
+
// eager escape hatch uses). It reads `existing` fresh — here, AFTER the
|
|
217
|
+
// await — so two concurrent loads of different non-base locales for the same
|
|
197
218
|
// package (e.g. de + fr) each see the other's already-merged result and don't
|
|
198
|
-
// lose a write
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
this.addTranslations(locale, { [packageName]: data });
|
|
219
|
+
// lose a write, and it writes a new object reference so the SvelteMap
|
|
220
|
+
// notifies its `$derived` subscribers.
|
|
221
|
+
this.registerPackageLocale(packageName, locale, data);
|
|
202
222
|
return true;
|
|
203
223
|
}
|
|
204
224
|
catch (error) {
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -142,6 +142,16 @@ export interface PackageI18n<T extends Translations> {
|
|
|
142
142
|
exists: (key: string) => boolean;
|
|
143
143
|
getLocales: () => Locale[];
|
|
144
144
|
register: () => void;
|
|
145
|
+
/**
|
|
146
|
+
* Eagerly and **additively** register one locale's already-imported bundle for
|
|
147
|
+
* this package — the SSR escape hatch for a locale declared as a lazy
|
|
148
|
+
* `options.loaders` entry. Call once at server/app start (with e.g.
|
|
149
|
+
* `import de from '@urbicon-ui/blocks/i18n/de'`) so the first server render
|
|
150
|
+
* already resolves that locale instead of rendering the fallback until the
|
|
151
|
+
* client-only on-mount chunk load lands. Merges — it does not drop the eager
|
|
152
|
+
* base bundle. Throws on an unsupported locale or a non-object bundle.
|
|
153
|
+
*/
|
|
154
|
+
registerLocale: (locale: Locale, bundle: Translations) => void;
|
|
145
155
|
types: CreatePackageTypes<T>;
|
|
146
156
|
}
|
|
147
157
|
export interface I18nComponentProps {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/i18n",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.35.0",
|
|
4
4
|
"description": "Runes-based localization for Svelte 5 apps and the Urbicon UI design system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@sveltejs/package": "^2.5.8",
|
|
25
25
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
26
26
|
"@types/node": "^25.9.5",
|
|
27
|
-
"@urbicon-ui/shared-types": "6.
|
|
27
|
+
"@urbicon-ui/shared-types": "6.35.0",
|
|
28
28
|
"prettier": "^3.8.4",
|
|
29
29
|
"prettier-plugin-svelte": "^4.1.1",
|
|
30
30
|
"prettier-plugin-tailwindcss": "^0.8.0",
|