@urbicon-ui/i18n 6.3.9 → 6.3.11
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 +35 -0
- package/dist/i18n/package-integration.js +50 -32
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -302,6 +302,41 @@ import type {
|
|
|
302
302
|
|
|
303
303
|
`en`, `de` ship data. `fr`, `es`, `it`, `nl` are declared target locales (in the `Locale` union / `SUPPORTED_LOCALES`) — register your own bundles for them via `createPackageI18n`.
|
|
304
304
|
|
|
305
|
+
## Translation Auditing
|
|
306
|
+
|
|
307
|
+
Three layers catch i18n problems — untranslated strings, unused keys, and copy that bypassed i18n entirely. The data-level audit and the runtime sink ship from the main entry (dependency-free, usable in a Vitest test); the source scanner lives on the dev-only `@urbicon-ui/i18n/audit` subpath; the `urbicon i18n` CLI (`@urbicon-ui/design`) is the filesystem front end over all three.
|
|
308
|
+
|
|
309
|
+
**1. Data-level parity & quality** — `auditTranslations(packageName, bundles)` diffs locale bundles for missing/extra keys, empty values, interpolation-param drift (`{{name}}` in one locale but not another), value-equals-key placeholders, and malformed / CLDR-incomplete `_plural` objects. Pure and deterministic — run it as a test (the richer successor to `validatePackageTranslations`, kept for back-compat):
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { auditTranslations } from '@urbicon-ui/i18n';
|
|
313
|
+
import { appTranslations } from '$lib/i18n';
|
|
314
|
+
|
|
315
|
+
it('translations are in parity', () => {
|
|
316
|
+
expect(auditTranslations('app', appTranslations).ok).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**2. Runtime missing-key sink** — `onMissingKey` (via `configureI18n`) fires when a key resolves nowhere and would render as its raw string. `createMissingKeyCollector()` packages it for tests/E2E — assert that nothing rendered a raw key, including dynamically-built keys a static scan can't see:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { configureI18n, createMissingKeyCollector } from '@urbicon-ui/i18n';
|
|
324
|
+
|
|
325
|
+
const misses = createMissingKeyCollector();
|
|
326
|
+
configureI18n({ onMissingKey: misses.onMissingKey });
|
|
327
|
+
// … render / exercise the app …
|
|
328
|
+
expect(misses.isClean()).toBe(true);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**3. Source scan & CLI** — `urbicon i18n` scans your sources for **unused** keys (defined but referenced nowhere), **used-but-undefined** keys (a typo that renders raw), and **hardcoded** UI strings. Run it under Bun (it loads `.ts` locale bundles):
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
urbicon i18n audit src/ --translations src/lib/translations # parity + unused + hardcoded
|
|
335
|
+
urbicon i18n unused --dynamic-keys 'errors.*' --json # just the scan, allowlisting dynamic key families
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
It gates (exit 1) on parity errors + used-but-undefined; unused keys and hardcoded strings are advisory (`--strict` gates them too). The pure scanner core — `scanSources`, `findUnusedKeys`, `findHardcodedStrings` — is on the `@urbicon-ui/i18n/audit` subpath for programmatic use, with `typescript` + `svelte` as optional peers it lazily imports. See the [CI gate template](../design/templates/ci-github.yml).
|
|
339
|
+
|
|
305
340
|
## Development
|
|
306
341
|
|
|
307
342
|
```bash
|
|
@@ -15,32 +15,39 @@ import { getRegistry } from './registry.svelte.js';
|
|
|
15
15
|
* out of the initial bundle as dynamic-import chunks, loaded only when activated.
|
|
16
16
|
*/
|
|
17
17
|
export function createPackageI18n(packageName, translations, options) {
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// Lazy, first-use registration — deliberately NOT at module-eval (Codeberg #22).
|
|
19
|
+
// A consumer's top-level `export const x = createPackageI18n(...)` must not call
|
|
20
|
+
// getRegistry() during module initialisation: in a reordered *production* chunk
|
|
21
|
+
// (Rollup, `sideEffects: false`) that call can run before the registry module's
|
|
22
|
+
// `class I18nRegistry` statement, hitting the class temporal dead zone →
|
|
23
|
+
// `new (undefined)()` → "is not a constructor" → blank page on hydration. The
|
|
24
|
+
// hoisted getRegistry() keeps the *getter* reachable, but the *class* it
|
|
25
|
+
// constructs is still a TDZ binding, so the hoist guard alone was insufficient.
|
|
22
26
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// setLocale. Parity for lazy bundles is a runtime concern (validatePackageTranslations).
|
|
37
|
-
if (options?.loaders) {
|
|
27
|
+
// Deferring registration to the first useTranslate()/t()/exists() call removes
|
|
28
|
+
// the ordering dependency at the source: by render / first-call time every module
|
|
29
|
+
// is fully evaluated, so `new I18nRegistry()` is always safe. Registration is
|
|
30
|
+
// synchronous (it runs before the translate read), so the first call resolves the
|
|
31
|
+
// real string — unlike the old queueMicrotask variant that returned the raw key.
|
|
32
|
+
// registerPackage mutates the reactive SvelteMap, but first touch is a component
|
|
33
|
+
// init (`useTranslate` body) or a non-reactive call (`t`/tests), never inside a
|
|
34
|
+
// `$derived`, so it cannot trip `state_unsafe_mutation`.
|
|
35
|
+
let registered = false;
|
|
36
|
+
const ensureRegistered = () => {
|
|
37
|
+
if (registered)
|
|
38
|
+
return;
|
|
39
|
+
registered = true;
|
|
38
40
|
const registry = getRegistry();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
registry.registerPackage(packageName, translations);
|
|
42
|
+
// Opt-in lazy locales (WP4): dynamic-import loaders alongside the eager base
|
|
43
|
+
// bundle, loaded on demand by the provider / setLocale.
|
|
44
|
+
if (options?.loaders) {
|
|
45
|
+
for (const [locale, loader] of Object.entries(options.loaders)) {
|
|
46
|
+
if (loader)
|
|
47
|
+
registry.registerPackageLoader(packageName, locale, loader);
|
|
48
|
+
}
|
|
42
49
|
}
|
|
43
|
-
}
|
|
50
|
+
};
|
|
44
51
|
// Context-scoped hook — the SSR-correct, reactive accessor. Captures the
|
|
45
52
|
// request-scoped locale state at component init (or `undefined` without a
|
|
46
53
|
// provider → base locale), then resolves against the static registry. Reading
|
|
@@ -48,6 +55,7 @@ export function createPackageI18n(packageName, translations, options) {
|
|
|
48
55
|
// call-sites re-render on locale change; reading the registry's SvelteMap makes
|
|
49
56
|
// them re-render when a package registers more translations.
|
|
50
57
|
const useTranslate = () => {
|
|
58
|
+
ensureRegistered();
|
|
51
59
|
const state = useI18nState();
|
|
52
60
|
const registry = getRegistry();
|
|
53
61
|
return ((key, params, options) => registry.translate(key, state?.locale ?? BASE_LOCALE, state?.fallbackLocale ?? BASE_LOCALE, params, { packageName, ...options }));
|
|
@@ -56,15 +64,25 @@ export function createPackageI18n(packageName, translations, options) {
|
|
|
56
64
|
// against the base locale — there is no request-scoped state outside a
|
|
57
65
|
// component. Components use `useTranslate` for the reactive, provider-scoped
|
|
58
66
|
// locale.
|
|
59
|
-
const t = ((key, params, options) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const t = ((key, params, options) => {
|
|
68
|
+
ensureRegistered();
|
|
69
|
+
return getRegistry().translate(key, BASE_LOCALE, BASE_LOCALE, params, {
|
|
70
|
+
packageName,
|
|
71
|
+
...options
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const exists = (key) => {
|
|
75
|
+
ensureRegistered();
|
|
76
|
+
return getRegistry().exists(key, BASE_LOCALE, packageName);
|
|
77
|
+
};
|
|
78
|
+
const getLocales = () => {
|
|
79
|
+
ensureRegistered();
|
|
80
|
+
return getRegistry().getPackageLocales(packageName);
|
|
81
|
+
};
|
|
82
|
+
// Eager-register escape hatch: a consumer that wants the package present before
|
|
83
|
+
// the first useTranslate()/t() (e.g. to preload a lazy locale) can call this.
|
|
84
|
+
// Previously a no-op; now it performs the idempotent first-use registration.
|
|
85
|
+
const register = () => ensureRegistered();
|
|
68
86
|
return {
|
|
69
87
|
useTranslate,
|
|
70
88
|
t,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/i18n",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.11",
|
|
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.4",
|
|
27
|
-
"@urbicon-ui/shared-types": "6.3.
|
|
27
|
+
"@urbicon-ui/shared-types": "6.3.11",
|
|
28
28
|
"prettier": "^3.8.4",
|
|
29
29
|
"prettier-plugin-svelte": "^4.1.1",
|
|
30
30
|
"prettier-plugin-tailwindcss": "^0.8.0",
|