@ultimat3/i18n 21.0.0 → 22.0.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
@@ -117,6 +117,15 @@ X_CATALOG_MISSING_KEYS: catalog is incomplete
117
117
  | `X_CATALOG_MISSING_KEYS` | a shipped locale lacks a key the source uses |
118
118
  | `X_CATALOG_INVALID` | non-string leaf, bad key segment, or a dotted/nested collision |
119
119
 
120
+ ### Error classes
121
+
122
+ Every error class `src/index.ts` exports, for `instanceof` inside one process. Across a wire or
123
+ a job boundary the class is gone and the `code` is what survives — match on that.
124
+
125
+ | Class | Code | Declared in |
126
+ |---|---|---|
127
+ | `I18nError` | `X_LOCALE_UNSUPPORTED`, `X_CATALOG_MISSING_KEYS`, `X_CATALOG_INVALID`, `X_CATALOG_UNREGISTERED` | `src/errors.ts` |
128
+
120
129
  ## Why it exists
121
130
 
122
131
  Retrofitting i18n means touching every string in the app. Structuring for many locales on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/i18n",
3
- "version": "21.0.0",
3
+ "version": "22.0.0",
4
4
  "description": "Dependency-free translator, catalog flattening, locale negotiation and loud missing-key rendering",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,6 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@ultimat3/core": "21.0.0"
38
+ "@ultimat3/core": "22.0.0"
39
39
  }
40
40
  }
@@ -194,7 +194,8 @@
194
194
  "error": {
195
195
  "action-failed": "The action threw before it finished.",
196
196
  "confirmation-required": "This is destructive: echo the confirmation token first.",
197
- "invalid-input": "Some fields did not pass the entity's own schema."
197
+ "invalid-input": "Some fields did not pass the entity's own schema.",
198
+ "operation-not-offered": "This resource does not offer that operation."
198
199
  },
199
200
  "form": {
200
201
  "cancel": "Cancel",
package/src/locales.ts CHANGED
@@ -50,8 +50,18 @@ export const SUPPORTED_LOCALES: readonly Locale[] = [
50
50
  export { type Direction, directionOf, isRtl } from '@ultimat3/core';
51
51
 
52
52
  /**
53
- * Strip region, lowercase, fall back to the default.
54
- * `pt-BR` → `pt`; `ZH-Hant-TW` → `zh-hant` when that tag is supported, else `zh`.
53
+ * The registered spelling of `lower`, compared case-insensitively. A registered `pt-BR` used to
54
+ * be compared EXACTLY against a request tag this function had just lowercased, so `pt-BR` from a
55
+ * query, a cookie or a header never matched it and every such request fell back.
56
+ */
57
+ function registered(lower: string, supported: readonly Locale[]): Locale | undefined {
58
+ return supported.find((locale) => locale.toLowerCase().replace(/_/g, '-') === lower);
59
+ }
60
+
61
+ /**
62
+ * Strip region, match case-insensitively, answer the REGISTERED spelling, fall back to the default.
63
+ * `pt-BR` → `pt-BR` when that tag is registered, else `pt`; `ZH-Hant-TW` → `zh-hant` (or
64
+ * `zh-Hant`, as registered) when that tag is supported, else `zh`.
55
65
  */
56
66
  export function normalizeLocale(
57
67
  tag?: string | null,
@@ -61,17 +71,18 @@ export function normalizeLocale(
61
71
  if (!tag) return fallback;
62
72
  const lower = tag.trim().toLowerCase().replace(/_/g, '-');
63
73
  if (lower.length === 0 || lower === '*') return fallback;
64
- if (supported.includes(lower)) return lower;
74
+ const whole = registered(lower, supported);
75
+ if (whole !== undefined) return whole;
65
76
 
66
77
  const segments = lower.split('-');
67
78
  const primary = segments[0] ?? '';
68
79
  // `lang-script` is a distinct written form (zh-hant vs zh-hans) — try it before dropping to `lang`.
69
80
  const script = segments[1];
70
81
  if (script !== undefined && script.length === 4) {
71
- const withScript = `${primary}-${script}`;
72
- if (supported.includes(withScript)) return withScript;
82
+ const withScript = registered(`${primary}-${script}`, supported);
83
+ if (withScript !== undefined) return withScript;
73
84
  }
74
- return supported.includes(primary) ? primary : fallback;
85
+ return registered(primary, supported) ?? fallback;
75
86
  }
76
87
 
77
88
  /** Same as `normalizeLocale` but loud: use where an unknown locale is a caller bug. */