@urbicon-ui/i18n 6.49.0 → 6.50.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
@@ -10,13 +10,11 @@ The locale lives in **context**, not a module-global singleton — so concurrent
10
10
 
11
11
  ## Installation
12
12
 
13
- This package ships inside the Urbicon UI monorepo. Install from repo root:
14
-
15
13
  ```bash
16
- bun install
14
+ bun add @urbicon-ui/i18n
17
15
  ```
18
16
 
19
- Peer dependencies: `svelte` (^5.40 — uses runes + `createContext`-era context), `@sveltejs/kit`.
17
+ Peer dependencies: `svelte` (^5.40 — uses runes + `createContext`-era context). No SvelteKit needed: the package imports neither `$app/*` nor `@sveltejs/kit`, so it works in any Svelte 5 project — the request-scoped locale below is what keeps it SSR-correct wherever you render.
20
18
 
21
19
  ## Quick Start
22
20
 
@@ -191,6 +189,23 @@ resolveLocale(request, {
191
189
 
192
190
  > Fully **prerendered** (static) sites have no per-request server, so resolve the locale on the client after mount instead (read a cookie/`localStorage`, then `setLocale`). The provider's base-locale-first render keeps hydration stable.
193
191
 
192
+ ## Formatting with `Intl` — `resolveDateLocale`
193
+
194
+ Building your own date or number component? Never hand `Intl` an `undefined` locale: it follows the *runtime*, which is your server process during SSR and the user's browser after hydration, so the same value renders two ways across the boundary. `resolveDateLocale` is the chain the library's own components use:
195
+
196
+ ```ts
197
+ import { resolveDateLocale, useI18n } from '@urbicon-ui/i18n';
198
+
199
+ const i18n = useI18n();
200
+ // `explicit prop → provider locale → BASE_LOCALE`
201
+ const locale = $derived(resolveDateLocale(localeProp, i18n.locale));
202
+ new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(value);
203
+ ```
204
+
205
+ The two rungs are treated differently on purpose. An explicit prop is **trusted** — any BCP 47 tag is fair game, including ones this package ships no translations for (`'ja-JP'`), and a malformed one throws from `Intl` at the call site that caused it. A provider value is **verified**, because `<I18nProvider locale={x}>` does not validate `x`: `'de_DE'` and `''` would throw at render time (an SSR 500 in a component that never saw the value), while `'xx'` and `'english'` do not throw at all — `Intl` silently resolves them to the runtime default, reintroducing the divergence through the back door. Unsupported values therefore fall back to `BASE_LOCALE` and say so in DEV.
206
+
207
+ Pass `'auto'` (or `undefined`) as the prop to mean "no explicit choice".
208
+
194
209
  ## Locale code-splitting (opt-in)
195
210
 
196
211
  By default a package registers all its locale bundles eagerly. To keep non-base locales out of the initial bundle, register them as dynamic-import loaders — the base/fallback locale stays eager, the rest load on activation:
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Resolve the BCP 47 tag a component formats with (`Intl.DateTimeFormat`,
3
+ * `Intl.NumberFormat`, `Intl.RelativeTimeFormat`).
4
+ *
5
+ * The chain is `explicit prop → i18n provider → base locale`, and the two rungs
6
+ * are treated differently on purpose.
7
+ *
8
+ * **The prop is trusted.** A consumer writing `locale="ja-JP"` means it, and any
9
+ * valid BCP 47 tag is fair game — the library has no business restricting it to
10
+ * the six locales it ships translations for. An invalid one throws from `Intl`,
11
+ * loudly, at the call site that caused it. That is the house rule: explicit
12
+ * input fails loudly rather than being silently repaired.
13
+ *
14
+ * **The context value is verified**, because a bad one is a very different
15
+ * failure. `I18nState`'s constructor does not validate its argument (only
16
+ * `setLocale` does — `context.svelte.ts`), so `<I18nProvider locale={x}>` puts
17
+ * whatever `x` is behind the `Locale` type. Before formatting read that value,
18
+ * a bogus one merely made translation lookups fall back. Now it reaches `Intl`,
19
+ * where it fails in two ways, both measured:
20
+ *
21
+ * - `'de_DE'` (underscore) and `''` throw `RangeError` — a render-time throw,
22
+ * i.e. an SSR 500, from a component that never saw the offending value;
23
+ * - `'xx'`, `'zz-ZZ'`, `'english'` do NOT throw. `Intl` resolves them to the
24
+ * *runtime* default — `en-US` under Bun, whatever `LANG` says under Node.
25
+ * That is exactly the server/client divergence the `'auto'` default exists
26
+ * to prevent, reintroduced through the back door.
27
+ *
28
+ * So an unsupported context value falls back to the base locale, which is what
29
+ * the rest of the i18n system does with it anyway, and says so in DEV. The
30
+ * component stays renderable and the diagnosis points at the provider.
31
+ *
32
+ * Lives here rather than in `blocks` (where it was written, 2026-07-31) because
33
+ * `table` needs the same chain for its own cells and the two packages share no
34
+ * code but this one — both peer-depend on this package, and both `BASE_LOCALE`
35
+ * and `isLocaleSupported` already live next door. A second hand-aligned copy is
36
+ * how the four drifted `toSlug` implementations happened (#43).
37
+ */
38
+ export declare function resolveDateLocale(prop: string | undefined, contextLocale: string): string;
@@ -0,0 +1,59 @@
1
+ import { BASE_LOCALE } from './context.svelte.js';
2
+ import { isLocaleSupported } from './types.js';
3
+ /**
4
+ * Resolve the BCP 47 tag a component formats with (`Intl.DateTimeFormat`,
5
+ * `Intl.NumberFormat`, `Intl.RelativeTimeFormat`).
6
+ *
7
+ * The chain is `explicit prop → i18n provider → base locale`, and the two rungs
8
+ * are treated differently on purpose.
9
+ *
10
+ * **The prop is trusted.** A consumer writing `locale="ja-JP"` means it, and any
11
+ * valid BCP 47 tag is fair game — the library has no business restricting it to
12
+ * the six locales it ships translations for. An invalid one throws from `Intl`,
13
+ * loudly, at the call site that caused it. That is the house rule: explicit
14
+ * input fails loudly rather than being silently repaired.
15
+ *
16
+ * **The context value is verified**, because a bad one is a very different
17
+ * failure. `I18nState`'s constructor does not validate its argument (only
18
+ * `setLocale` does — `context.svelte.ts`), so `<I18nProvider locale={x}>` puts
19
+ * whatever `x` is behind the `Locale` type. Before formatting read that value,
20
+ * a bogus one merely made translation lookups fall back. Now it reaches `Intl`,
21
+ * where it fails in two ways, both measured:
22
+ *
23
+ * - `'de_DE'` (underscore) and `''` throw `RangeError` — a render-time throw,
24
+ * i.e. an SSR 500, from a component that never saw the offending value;
25
+ * - `'xx'`, `'zz-ZZ'`, `'english'` do NOT throw. `Intl` resolves them to the
26
+ * *runtime* default — `en-US` under Bun, whatever `LANG` says under Node.
27
+ * That is exactly the server/client divergence the `'auto'` default exists
28
+ * to prevent, reintroduced through the back door.
29
+ *
30
+ * So an unsupported context value falls back to the base locale, which is what
31
+ * the rest of the i18n system does with it anyway, and says so in DEV. The
32
+ * component stays renderable and the diagnosis points at the provider.
33
+ *
34
+ * Lives here rather than in `blocks` (where it was written, 2026-07-31) because
35
+ * `table` needs the same chain for its own cells and the two packages share no
36
+ * code but this one — both peer-depend on this package, and both `BASE_LOCALE`
37
+ * and `isLocaleSupported` already live next door. A second hand-aligned copy is
38
+ * how the four drifted `toSlug` implementations happened (#43).
39
+ */
40
+ export function resolveDateLocale(prop, contextLocale) {
41
+ if (prop !== undefined && prop !== 'auto')
42
+ return prop;
43
+ if (isLocaleSupported(contextLocale))
44
+ return contextLocale;
45
+ // First `import.meta.env` in this package, so the `@sveltejs/package` build
46
+ // advisory ("Avoid usage of `import.meta.env`") now fires here too. Same
47
+ // deliberate trade as in `blocks`: optional-chained so a non-Vite consumer
48
+ // gets `undefined` rather than a throw, and `esm-env` is not an option
49
+ // because it would be a runtime dependency in the published dist. The
50
+ // advisory is a plain string match, so `?.` does not quiet it. See the
51
+ // zero-dependency note in AGENTS.md — do not "fix" it by adding a dep.
52
+ if (import.meta.env?.DEV) {
53
+ console.warn(`[i18n] <I18nProvider locale="${contextLocale}"> is not a supported locale, so date and ` +
54
+ `number formatting falls back to "${BASE_LOCALE}". Supported: en, de, fr, es, it, nl. ` +
55
+ `To format in a locale the library ships no translations for, pass it to the ` +
56
+ `component directly (e.g. locale="ja-JP") instead of through the provider.`);
57
+ }
58
+ return BASE_LOCALE;
59
+ }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export type { I18nApi, I18nConfigureOptions } from './i18n/context.svelte.js';
7
7
  export { BASE_LOCALE, configureI18n, provideI18n, useI18n } from './i18n/context.svelte.js';
8
8
  export type { CreatePackageI18nOptions } from './i18n/package-integration.js';
9
9
  export { createComponentI18n, createPackageI18n, createPackageTranslations, createTypedPackage, registerPackages, registerTranslationLoaders, validatePackageTranslations } from './i18n/package-integration.js';
10
+ export { resolveDateLocale } from './i18n/resolve-date-locale.js';
10
11
  export type { LocaleSource, ResolveLocaleOptions } from './i18n/resolve-locale.js';
11
12
  export { resolveLocale } from './i18n/resolve-locale.js';
12
13
  export type { CreatePackageTypes, I18nComponentProps, I18nConfig, I18nError, I18nMissingKey, I18nStore, Locale, PackageI18n, PackageTranslations, PluralParams, PluralRules, TranslationFunction, TranslationLoader, TranslationOptions, TranslationParams, Translations, TypedTranslationFunction } from './i18n/types.js';
package/dist/index.js CHANGED
@@ -14,6 +14,10 @@ export { I18nProvider, T } from './components/index.js';
14
14
  export { BASE_LOCALE, configureI18n, provideI18n, useI18n } from './i18n/context.svelte.js';
15
15
  // Package integration utilities
16
16
  export { createComponentI18n, createPackageI18n, createPackageTranslations, createTypedPackage, registerPackages, registerTranslationLoaders, validatePackageTranslations } from './i18n/package-integration.js';
17
+ // The `explicit prop → provider → base locale` chain a component resolves an
18
+ // `Intl` tag through. Distinct from `resolveLocale` below: this one answers
19
+ // "what do I format with", not "what locale is this request in".
20
+ export { resolveDateLocale } from './i18n/resolve-date-locale.js';
17
21
  // Server-side initial-locale resolution (cookie + Accept-Language) for the
18
22
  // provider's `locale` prop. SSR/hydration-stable.
19
23
  export { resolveLocale } from './i18n/resolve-locale.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/i18n",
3
- "version": "6.49.0",
3
+ "version": "6.50.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": "^26.1.2",
27
- "@urbicon-ui/shared-types": "6.49.0",
27
+ "@urbicon-ui/shared-types": "6.50.0",
28
28
  "prettier": "^3.9.6",
29
29
  "prettier-plugin-svelte": "^4.1.1",
30
30
  "prettier-plugin-tailwindcss": "^0.8.1",
@@ -56,7 +56,6 @@
56
56
  "main": "dist/index.js",
57
57
  "sideEffects": false,
58
58
  "peerDependencies": {
59
- "@sveltejs/kit": "^2.70.2",
60
59
  "svelte": "^5.56.8",
61
60
  "typescript": "^6.0.3",
62
61
  "@urbicon-ui/shared-types": "^6.0.0"