@ultimat3/i18n 1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/package.json +35 -0
  4. package/src/catalog.d.ts +28 -0
  5. package/src/catalog.d.ts.map +1 -0
  6. package/src/catalog.js +81 -0
  7. package/src/catalog.js.map +1 -0
  8. package/src/catalog.ts +132 -0
  9. package/src/catalogs/en.json +212 -0
  10. package/src/context.d.ts +64 -0
  11. package/src/context.d.ts.map +1 -0
  12. package/src/context.js +132 -0
  13. package/src/context.js.map +1 -0
  14. package/src/context.ts +195 -0
  15. package/src/define-catalogs.ts +56 -0
  16. package/src/errors.d.ts +22 -0
  17. package/src/errors.d.ts.map +1 -0
  18. package/src/errors.js +52 -0
  19. package/src/errors.js.map +1 -0
  20. package/src/errors.ts +73 -0
  21. package/src/extract.d.ts +67 -0
  22. package/src/extract.d.ts.map +1 -0
  23. package/src/extract.js +106 -0
  24. package/src/extract.js.map +1 -0
  25. package/src/extract.ts +184 -0
  26. package/src/framework.d.ts +12 -0
  27. package/src/framework.d.ts.map +1 -0
  28. package/src/framework.js +16 -0
  29. package/src/framework.js.map +1 -0
  30. package/src/framework.ts +19 -0
  31. package/src/index.d.ts +10 -0
  32. package/src/index.d.ts.map +1 -0
  33. package/src/index.js +10 -0
  34. package/src/index.js.map +1 -0
  35. package/src/index.ts +101 -0
  36. package/src/interpolate.d.ts +36 -0
  37. package/src/interpolate.d.ts.map +1 -0
  38. package/src/interpolate.js +96 -0
  39. package/src/interpolate.js.map +1 -0
  40. package/src/interpolate.ts +103 -0
  41. package/src/locales.d.ts +36 -0
  42. package/src/locales.d.ts.map +1 -0
  43. package/src/locales.js +141 -0
  44. package/src/locales.js.map +1 -0
  45. package/src/locales.ts +166 -0
  46. package/src/translator.d.ts +25 -0
  47. package/src/translator.d.ts.map +1 -0
  48. package/src/translator.js +37 -0
  49. package/src/translator.js.map +1 -0
  50. package/src/translator.ts +87 -0
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Static extraction: which keys the source calls, which are missing per locale, which
3
+ * are defined and never used. `x verify` fails on a missing key in a shipped locale —
4
+ * a translation gap is a build error, not a ticket.
5
+ */
6
+ import { type Catalog } from './catalog';
7
+ import type { Locale } from './locales';
8
+ export interface KeyUsage {
9
+ key: string;
10
+ file: string;
11
+ line: number;
12
+ column: number;
13
+ }
14
+ /** `t(key)` where `key` is a variable — the extractor cannot verify it, so it says so. */
15
+ export interface DynamicUsage {
16
+ expression: string;
17
+ file: string;
18
+ line: number;
19
+ column: number;
20
+ }
21
+ export interface Extraction {
22
+ usages: KeyUsage[];
23
+ dynamic: DynamicUsage[];
24
+ }
25
+ export interface LocaleAudit {
26
+ locale: Locale;
27
+ missing: string[];
28
+ unused: string[];
29
+ }
30
+ export interface ExtractReport {
31
+ /** Distinct static keys, sorted. */
32
+ used: string[];
33
+ usages: KeyUsage[];
34
+ dynamic: DynamicUsage[];
35
+ locales: LocaleAudit[];
36
+ ok: boolean;
37
+ }
38
+ export interface ExtractOptions {
39
+ /** Call names treated as translator calls. */
40
+ callees?: readonly string[];
41
+ }
42
+ /**
43
+ * Scan one source file. Deliberately a lexer-free scan: a translator call is a
44
+ * recognisable shape, and a regex that runs in milliseconds over a whole repo beats a
45
+ * parser that needs a tsconfig to start.
46
+ */
47
+ export declare function extractKeys(source: string, file?: string, options?: ExtractOptions): Extraction;
48
+ export interface AuditInput {
49
+ extraction: Extraction;
50
+ /** Every locale that ships. A locale absent here is not audited. */
51
+ catalogs: Readonly<Record<Locale, Catalog>>;
52
+ /** Keys resolved at runtime (e.g. `time.cron.*`) — never reported unused. */
53
+ ignoreUnused?: readonly string[];
54
+ }
55
+ /**
56
+ * A used key counts as present when the locale defines it *or* any of its plural
57
+ * variants: `pl` legitimately defines `items_one`/`items_few`/`items_many` and no bare
58
+ * `items`, and that must not read as a missing key.
59
+ */
60
+ export declare function auditCatalogs(input: AuditInput): ExtractReport;
61
+ /** The `x verify` gate. Throws on the first locale with a gap, naming file and keys. */
62
+ export declare function assertCatalogsComplete(report: ExtractReport): void;
63
+ /** Merge per-file extractions before auditing. */
64
+ export declare function mergeExtractions(...extractions: readonly Extraction[]): Extraction;
65
+ /** Read + scan real files. Bun-only by design; the CLI passes the glob results. */
66
+ export declare function extractFromFiles(paths: readonly string[], options?: ExtractOptions): Promise<Extraction>;
67
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["extract.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAe,MAAM,WAAW,CAAC;AAGtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0FAA0F;AAC1F,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,EAAE,EAAE,OAAO,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAKD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,IAAI,SAAa,EACjB,OAAO,GAAE,cAAmB,GAC3B,UAAU,CA0BZ;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,oEAAoE;IACpE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC5C,6EAA6E;IAC7E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAuB9D;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAIlE;AAED,kDAAkD;AAClD,wBAAgB,gBAAgB,CAAC,GAAG,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,CAKlF;AAED,mFAAmF;AACnF,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAKrB"}
package/src/extract.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Static extraction: which keys the source calls, which are missing per locale, which
3
+ * are defined and never used. `x verify` fails on a missing key in a shipped locale —
4
+ * a translation gap is a build error, not a ticket.
5
+ */
6
+ import { catalogKeys } from './catalog';
7
+ import { catalogMissingKeys } from './errors';
8
+ import { PLURAL_CATEGORIES } from './interpolate';
9
+ const DEFAULT_CALLEES = ['t', '$t', 'translate'];
10
+ const STRING_LITERAL = /^(['"`])((?:[^\\]|\\.)*?)\1$/s;
11
+ /**
12
+ * Scan one source file. Deliberately a lexer-free scan: a translator call is a
13
+ * recognisable shape, and a regex that runs in milliseconds over a whole repo beats a
14
+ * parser that needs a tsconfig to start.
15
+ */
16
+ export function extractKeys(source, file = '<memory>', options = {}) {
17
+ const callees = options.callees ?? DEFAULT_CALLEES;
18
+ const pattern = new RegExp(
19
+ // not preceded by an identifier char or `.`, so `ctx.t(` and `format(` don't match
20
+ String.raw `(?<![\w$.])(?:${callees.map(escapeRegExp).join('|')})(?:\.has)?\s*\(\s*([^,)]*)`, 'g');
21
+ const usages = [];
22
+ const dynamic = [];
23
+ for (const match of source.matchAll(pattern)) {
24
+ const argument = (match[1] ?? '').trim();
25
+ const index = match.index ?? 0;
26
+ const position = positionOf(source, index);
27
+ if (argument === '')
28
+ continue;
29
+ const literal = STRING_LITERAL.exec(argument);
30
+ const key = literal?.[2];
31
+ if (key === undefined || key.includes('${')) {
32
+ dynamic.push({ expression: argument, file, ...position });
33
+ continue;
34
+ }
35
+ usages.push({ key, file, ...position });
36
+ }
37
+ return { usages, dynamic };
38
+ }
39
+ /**
40
+ * A used key counts as present when the locale defines it *or* any of its plural
41
+ * variants: `pl` legitimately defines `items_one`/`items_few`/`items_many` and no bare
42
+ * `items`, and that must not read as a missing key.
43
+ */
44
+ export function auditCatalogs(input) {
45
+ const used = [...new Set(input.extraction.usages.map((usage) => usage.key))].sort();
46
+ const ignore = input.ignoreUnused ?? [];
47
+ const locales = Object.keys(input.catalogs)
48
+ .sort()
49
+ .map((locale) => {
50
+ const catalog = input.catalogs[locale] ?? {};
51
+ const missing = used.filter((key) => !definesKey(catalog, key));
52
+ const usedWithVariants = new Set(used.flatMap(variantsOf));
53
+ const unused = catalogKeys(catalog).filter((key) => !usedWithVariants.has(key) && !isIgnored(key, ignore));
54
+ return { locale, missing, unused };
55
+ });
56
+ return {
57
+ used,
58
+ usages: input.extraction.usages,
59
+ dynamic: input.extraction.dynamic,
60
+ locales,
61
+ ok: locales.every((audit) => audit.missing.length === 0),
62
+ };
63
+ }
64
+ /** The `x verify` gate. Throws on the first locale with a gap, naming file and keys. */
65
+ export function assertCatalogsComplete(report) {
66
+ for (const audit of report.locales) {
67
+ if (audit.missing.length > 0)
68
+ throw catalogMissingKeys(audit.locale, audit.missing);
69
+ }
70
+ }
71
+ /** Merge per-file extractions before auditing. */
72
+ export function mergeExtractions(...extractions) {
73
+ return {
74
+ usages: extractions.flatMap((extraction) => extraction.usages),
75
+ dynamic: extractions.flatMap((extraction) => extraction.dynamic),
76
+ };
77
+ }
78
+ /** Read + scan real files. Bun-only by design; the CLI passes the glob results. */
79
+ export async function extractFromFiles(paths, options = {}) {
80
+ const extractions = await Promise.all(paths.map(async (path) => extractKeys(await Bun.file(path).text(), path, options)));
81
+ return mergeExtractions(...extractions);
82
+ }
83
+ function definesKey(catalog, key) {
84
+ return variantsOf(key).some((candidate) => Object.hasOwn(catalog, candidate));
85
+ }
86
+ function variantsOf(key) {
87
+ return [key, `${key}_plural`, ...PLURAL_CATEGORIES.map((category) => `${key}_${category}`)];
88
+ }
89
+ function isIgnored(key, patterns) {
90
+ return patterns.some((pattern) => pattern.endsWith('*') ? key.startsWith(pattern.slice(0, -1)) : key === pattern);
91
+ }
92
+ function positionOf(source, index) {
93
+ let line = 1;
94
+ let lineStart = 0;
95
+ for (let i = 0; i < index; i += 1) {
96
+ if (source[i] === '\n') {
97
+ line += 1;
98
+ lineStart = i + 1;
99
+ }
100
+ }
101
+ return { line, column: index - lineStart + 1 };
102
+ }
103
+ function escapeRegExp(value) {
104
+ return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
105
+ }
106
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["extract.ts"],"names":[],"mappings":"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"}
package/src/extract.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Static extraction: which keys the source calls, which are missing per locale, which
3
+ * are defined and never used. `x verify` fails on a missing key in a shipped locale —
4
+ * a translation gap is a build error, not a ticket.
5
+ */
6
+
7
+ import { type Catalog, catalogKeys } from './catalog';
8
+ import { catalogMissingKeys } from './errors';
9
+ import { PLURAL_CATEGORIES } from './interpolate';
10
+ import type { Locale } from './locales';
11
+
12
+ export interface KeyUsage {
13
+ key: string;
14
+ file: string;
15
+ line: number;
16
+ column: number;
17
+ }
18
+
19
+ /** `t(key)` where `key` is a variable — the extractor cannot verify it, so it says so. */
20
+ export interface DynamicUsage {
21
+ expression: string;
22
+ file: string;
23
+ line: number;
24
+ column: number;
25
+ }
26
+
27
+ export interface Extraction {
28
+ usages: KeyUsage[];
29
+ dynamic: DynamicUsage[];
30
+ }
31
+
32
+ export interface LocaleAudit {
33
+ locale: Locale;
34
+ missing: string[];
35
+ unused: string[];
36
+ }
37
+
38
+ export interface ExtractReport {
39
+ /** Distinct static keys, sorted. */
40
+ used: string[];
41
+ usages: KeyUsage[];
42
+ dynamic: DynamicUsage[];
43
+ locales: LocaleAudit[];
44
+ ok: boolean;
45
+ }
46
+
47
+ export interface ExtractOptions {
48
+ /** Call names treated as translator calls. */
49
+ callees?: readonly string[];
50
+ }
51
+
52
+ const DEFAULT_CALLEES: readonly string[] = ['t', '$t', 'translate'];
53
+ const STRING_LITERAL = /^(['"`])((?:[^\\]|\\.)*?)\1$/s;
54
+
55
+ /**
56
+ * Scan one source file. Deliberately a lexer-free scan: a translator call is a
57
+ * recognisable shape, and a regex that runs in milliseconds over a whole repo beats a
58
+ * parser that needs a tsconfig to start.
59
+ */
60
+ export function extractKeys(
61
+ source: string,
62
+ file = '<memory>',
63
+ options: ExtractOptions = {},
64
+ ): Extraction {
65
+ const callees = options.callees ?? DEFAULT_CALLEES;
66
+ const pattern = new RegExp(
67
+ // not preceded by an identifier char or `.`, so `ctx.t(` and `format(` don't match
68
+ String.raw`(?<![\w$.])(?:${callees.map(escapeRegExp).join('|')})(?:\.has)?\s*\(\s*([^,)]*)`,
69
+ 'g',
70
+ );
71
+
72
+ const usages: KeyUsage[] = [];
73
+ const dynamic: DynamicUsage[] = [];
74
+
75
+ for (const match of source.matchAll(pattern)) {
76
+ const argument = (match[1] ?? '').trim();
77
+ const index = match.index ?? 0;
78
+ const position = positionOf(source, index);
79
+ if (argument === '') continue;
80
+ const literal = STRING_LITERAL.exec(argument);
81
+ const key = literal?.[2];
82
+ if (key === undefined || key.includes('${')) {
83
+ dynamic.push({ expression: argument, file, ...position });
84
+ continue;
85
+ }
86
+ usages.push({ key, file, ...position });
87
+ }
88
+
89
+ return { usages, dynamic };
90
+ }
91
+
92
+ export interface AuditInput {
93
+ extraction: Extraction;
94
+ /** Every locale that ships. A locale absent here is not audited. */
95
+ catalogs: Readonly<Record<Locale, Catalog>>;
96
+ /** Keys resolved at runtime (e.g. `time.cron.*`) — never reported unused. */
97
+ ignoreUnused?: readonly string[];
98
+ }
99
+
100
+ /**
101
+ * A used key counts as present when the locale defines it *or* any of its plural
102
+ * variants: `pl` legitimately defines `items_one`/`items_few`/`items_many` and no bare
103
+ * `items`, and that must not read as a missing key.
104
+ */
105
+ export function auditCatalogs(input: AuditInput): ExtractReport {
106
+ const used = [...new Set(input.extraction.usages.map((usage) => usage.key))].sort();
107
+ const ignore = input.ignoreUnused ?? [];
108
+
109
+ const locales: LocaleAudit[] = Object.keys(input.catalogs)
110
+ .sort()
111
+ .map((locale) => {
112
+ const catalog = input.catalogs[locale] ?? {};
113
+ const missing = used.filter((key) => !definesKey(catalog, key));
114
+ const usedWithVariants = new Set(used.flatMap(variantsOf));
115
+ const unused = catalogKeys(catalog).filter(
116
+ (key) => !usedWithVariants.has(key) && !isIgnored(key, ignore),
117
+ );
118
+ return { locale, missing, unused };
119
+ });
120
+
121
+ return {
122
+ used,
123
+ usages: input.extraction.usages,
124
+ dynamic: input.extraction.dynamic,
125
+ locales,
126
+ ok: locales.every((audit) => audit.missing.length === 0),
127
+ };
128
+ }
129
+
130
+ /** The `x verify` gate. Throws on the first locale with a gap, naming file and keys. */
131
+ export function assertCatalogsComplete(report: ExtractReport): void {
132
+ for (const audit of report.locales) {
133
+ if (audit.missing.length > 0) throw catalogMissingKeys(audit.locale, audit.missing);
134
+ }
135
+ }
136
+
137
+ /** Merge per-file extractions before auditing. */
138
+ export function mergeExtractions(...extractions: readonly Extraction[]): Extraction {
139
+ return {
140
+ usages: extractions.flatMap((extraction) => extraction.usages),
141
+ dynamic: extractions.flatMap((extraction) => extraction.dynamic),
142
+ };
143
+ }
144
+
145
+ /** Read + scan real files. Bun-only by design; the CLI passes the glob results. */
146
+ export async function extractFromFiles(
147
+ paths: readonly string[],
148
+ options: ExtractOptions = {},
149
+ ): Promise<Extraction> {
150
+ const extractions = await Promise.all(
151
+ paths.map(async (path) => extractKeys(await Bun.file(path).text(), path, options)),
152
+ );
153
+ return mergeExtractions(...extractions);
154
+ }
155
+
156
+ function definesKey(catalog: Catalog, key: string): boolean {
157
+ return variantsOf(key).some((candidate) => Object.hasOwn(catalog, candidate));
158
+ }
159
+
160
+ function variantsOf(key: string): string[] {
161
+ return [key, `${key}_plural`, ...PLURAL_CATEGORIES.map((category) => `${key}_${category}`)];
162
+ }
163
+
164
+ function isIgnored(key: string, patterns: readonly string[]): boolean {
165
+ return patterns.some((pattern) =>
166
+ pattern.endsWith('*') ? key.startsWith(pattern.slice(0, -1)) : key === pattern,
167
+ );
168
+ }
169
+
170
+ function positionOf(source: string, index: number): { line: number; column: number } {
171
+ let line = 1;
172
+ let lineStart = 0;
173
+ for (let i = 0; i < index; i += 1) {
174
+ if (source[i] === '\n') {
175
+ line += 1;
176
+ lineStart = i + 1;
177
+ }
178
+ }
179
+ return { line, column: index - lineStart + 1 };
180
+ }
181
+
182
+ function escapeRegExp(value: string): string {
183
+ return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
184
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The framework's own strings, flattened once at import.
3
+ * Register these before app catalogs so an app can override any framework key.
4
+ */
5
+ import { type Catalog } from './catalog';
6
+ import { type Locale } from './locales';
7
+ /** `errors.*`, `auth.*`, `pagination.*`, `admin.*`, `validation.*`, `common.*`, `time.*`. */
8
+ export declare const FRAMEWORK_CATALOG: Catalog;
9
+ export declare const FRAMEWORK_CATALOG_LOCALE: Locale;
10
+ /** Called by the HTTP layer at boot. Idempotent. */
11
+ export declare function registerFrameworkCatalog(locale?: Locale): void;
12
+ //# sourceMappingURL=framework.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework.d.ts","sourceRoot":"","sources":["framework.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAe,MAAM,WAAW,CAAC;AAGtD,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAExD,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,EAAE,OAAyB,CAAC;AAE1D,eAAO,MAAM,wBAAwB,EAAE,MAAuB,CAAC;AAE/D,oDAAoD;AACpD,wBAAgB,wBAAwB,CAAC,MAAM,GAAE,MAAiC,GAAG,IAAI,CAExF"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The framework's own strings, flattened once at import.
3
+ * Register these before app catalogs so an app can override any framework key.
4
+ */
5
+ import { loadCatalog } from './catalog';
6
+ import en from './catalogs/en.json';
7
+ import { registerCatalog } from './context';
8
+ import { DEFAULT_LOCALE } from './locales';
9
+ /** `errors.*`, `auth.*`, `pagination.*`, `admin.*`, `validation.*`, `common.*`, `time.*`. */
10
+ export const FRAMEWORK_CATALOG = loadCatalog(en);
11
+ export const FRAMEWORK_CATALOG_LOCALE = DEFAULT_LOCALE;
12
+ /** Called by the HTTP layer at boot. Idempotent. */
13
+ export function registerFrameworkCatalog(locale = FRAMEWORK_CATALOG_LOCALE) {
14
+ registerCatalog(locale, FRAMEWORK_CATALOG);
15
+ }
16
+ //# sourceMappingURL=framework.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework.js","sourceRoot":"","sources":["framework.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAgB,WAAW,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAe,MAAM,WAAW,CAAC;AAExD,6FAA6F;AAC7F,MAAM,CAAC,MAAM,iBAAiB,GAAY,WAAW,CAAC,EAAE,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,wBAAwB,GAAW,cAAc,CAAC;AAE/D,oDAAoD;AACpD,MAAM,UAAU,wBAAwB,CAAC,MAAM,GAAW,wBAAwB;IAChF,eAAe,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The framework's own strings, flattened once at import.
3
+ * Register these before app catalogs so an app can override any framework key.
4
+ */
5
+
6
+ import { type Catalog, loadCatalog } from './catalog';
7
+ import en from './catalogs/en.json';
8
+ import { registerCatalog } from './context';
9
+ import { DEFAULT_LOCALE, type Locale } from './locales';
10
+
11
+ /** `errors.*`, `auth.*`, `pagination.*`, `admin.*`, `validation.*`, `common.*`, `time.*`. */
12
+ export const FRAMEWORK_CATALOG: Catalog = loadCatalog(en);
13
+
14
+ export const FRAMEWORK_CATALOG_LOCALE: Locale = DEFAULT_LOCALE;
15
+
16
+ /** Called by the HTTP layer at boot. Idempotent. */
17
+ export function registerFrameworkCatalog(locale: Locale = FRAMEWORK_CATALOG_LOCALE): void {
18
+ registerCatalog(locale, FRAMEWORK_CATALOG);
19
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** Public surface of @ultimat3/i18n. Explicit exports only. */
2
+ export { type Catalog, catalogKeys, flattenCatalog, loadCatalog, mergeCatalogs, missingFrom, type NestedCatalog, parseNestedCatalog, } from './catalog';
3
+ export { attachLocale, catalogFor, configureLocales, currentDirection, currentLocale, currentTranslator, LOCALE_COOKIE, type LocaleConfig, type LocaleResolution, type LocaleSourceName, type LocaleSources, localeConfig, localeCookieOf, localeOf, registerCatalog, registeredLocales, resetCatalogs, resolveLocale, t, translatorFor, } from './context';
4
+ export { catalogInvalid, catalogMissingKeys, I18N_ERROR_CODES, I18nError, type I18nErrorCode, localeUnsupported, } from './errors';
5
+ export { type AuditInput, assertCatalogsComplete, auditCatalogs, type DynamicUsage, type Extraction, type ExtractOptions, type ExtractReport, extractFromFiles, extractKeys, type KeyUsage, type LocaleAudit, mergeExtractions, } from './extract';
6
+ export { FRAMEWORK_CATALOG, FRAMEWORK_CATALOG_LOCALE, registerFrameworkCatalog, } from './framework';
7
+ export { type InterpolationValue, type InterpolationVars, interpolate, PLURAL_CATEGORIES, placeholdersOf, pluralCategory, pluralKeyCandidates, pluralVariantsOf, selectPluralKey, } from './interpolate';
8
+ export { assertSupportedLocale, DEFAULT_LOCALE, type Direction, directionOf, isRtl, isSupportedLocale, type LanguageRange, type Locale, negotiateLocale, normalizeLocale, parseAcceptLanguage, SUPPORTED_LOCALES, } from './locales';
9
+ export { createTranslator, isMiss, type TranslateVars, type Translator } from './translator';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,OAAO,EACL,KAAK,OAAO,EACZ,WAAW,EACX,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,KAAK,aAAa,EAClB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,CAAC,EACD,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,KAAK,aAAa,EAClB,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,UAAU,EACf,sBAAsB,EACtB,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,gBAAgB,EAChB,WAAW,EACX,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,KAAK,SAAS,EACd,WAAW,EACX,KAAK,EACL,iBAAiB,EACjB,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC"}
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /** Public surface of @ultimat3/i18n. Explicit exports only. */
2
+ export { catalogKeys, flattenCatalog, loadCatalog, mergeCatalogs, missingFrom, parseNestedCatalog, } from './catalog';
3
+ export { attachLocale, catalogFor, configureLocales, currentDirection, currentLocale, currentTranslator, LOCALE_COOKIE, localeConfig, localeCookieOf, localeOf, registerCatalog, registeredLocales, resetCatalogs, resolveLocale, t, translatorFor, } from './context';
4
+ export { catalogInvalid, catalogMissingKeys, I18N_ERROR_CODES, I18nError, localeUnsupported, } from './errors';
5
+ export { assertCatalogsComplete, auditCatalogs, extractFromFiles, extractKeys, mergeExtractions, } from './extract';
6
+ export { FRAMEWORK_CATALOG, FRAMEWORK_CATALOG_LOCALE, registerFrameworkCatalog, } from './framework';
7
+ export { interpolate, PLURAL_CATEGORIES, placeholdersOf, pluralCategory, pluralKeyCandidates, pluralVariantsOf, selectPluralKey, } from './interpolate';
8
+ export { assertSupportedLocale, DEFAULT_LOCALE, directionOf, isRtl, isSupportedLocale, negotiateLocale, normalizeLocale, parseAcceptLanguage, SUPPORTED_LOCALES, } from './locales';
9
+ export { createTranslator, isMiss } from './translator';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,OAAO,EAEL,WAAW,EACX,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EAEX,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,aAAa,EAKb,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,CAAC,EACD,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EAET,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EAEL,sBAAsB,EACtB,aAAa,EAKb,gBAAgB,EAChB,WAAW,EAGX,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAGL,WAAW,EACX,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,cAAc,EAEd,WAAW,EACX,KAAK,EACL,iBAAiB,EAGjB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAuC,MAAM,cAAc,CAAC"}
package/src/index.ts ADDED
@@ -0,0 +1,101 @@
1
+ /** Public surface of @ultimat3/i18n. Explicit exports only. */
2
+
3
+ export {
4
+ type Catalog,
5
+ catalogKeys,
6
+ flattenCatalog,
7
+ loadCatalog,
8
+ mergeCatalogs,
9
+ missingFrom,
10
+ type NestedCatalog,
11
+ nestCatalog,
12
+ parseNestedCatalog,
13
+ } from './catalog';
14
+ export {
15
+ attachLocale,
16
+ catalogFor,
17
+ configureLocales,
18
+ currentDirection,
19
+ currentLocale,
20
+ LOCALE_COOKIE,
21
+ type LocaleConfig,
22
+ type LocaleResolution,
23
+ type LocaleSourceName,
24
+ type LocaleSources,
25
+ localeConfig,
26
+ localeCookieOf,
27
+ localeOf,
28
+ registerCatalog,
29
+ registeredLocales,
30
+ resetCatalogs,
31
+ resolveLocale,
32
+ t,
33
+ translatorFor,
34
+ useI18n,
35
+ } from './context';
36
+ export {
37
+ type CatalogSet,
38
+ type CatalogSources,
39
+ type DefineCatalogsInput,
40
+ defineCatalogs,
41
+ } from './define-catalogs';
42
+ export {
43
+ catalogInvalid,
44
+ catalogMissingKeys,
45
+ I18N_ERROR_CODES,
46
+ I18N_ERROR_TITLES,
47
+ I18nError,
48
+ type I18nErrorCode,
49
+ localeUnsupported,
50
+ } from './errors';
51
+ export {
52
+ type AuditInput,
53
+ assertCatalogsComplete,
54
+ auditCatalogs,
55
+ type DynamicUsage,
56
+ type Extraction,
57
+ type ExtractOptions,
58
+ type ExtractReport,
59
+ extractFromFiles,
60
+ extractKeys,
61
+ type KeyUsage,
62
+ type LocaleAudit,
63
+ mergeExtractions,
64
+ } from './extract';
65
+ export {
66
+ FRAMEWORK_CATALOG,
67
+ FRAMEWORK_CATALOG_LOCALE,
68
+ registerFrameworkCatalog,
69
+ } from './framework';
70
+ export {
71
+ type InterpolationValue,
72
+ type InterpolationVars,
73
+ interpolate,
74
+ PLURAL_CATEGORIES,
75
+ placeholdersOf,
76
+ pluralCategory,
77
+ pluralKeyCandidates,
78
+ pluralVariantsOf,
79
+ selectPluralKey,
80
+ } from './interpolate';
81
+ export {
82
+ assertSupportedLocale,
83
+ DEFAULT_LOCALE,
84
+ type Direction,
85
+ directionOf,
86
+ isRtl,
87
+ isSupportedLocale,
88
+ type LanguageRange,
89
+ type Locale,
90
+ negotiateLocale,
91
+ normalizeLocale,
92
+ parseAcceptLanguage,
93
+ SUPPORTED_LOCALES,
94
+ } from './locales';
95
+ export {
96
+ createTranslator,
97
+ isMiss,
98
+ type TranslateVars,
99
+ type TranslationKey,
100
+ type Translator,
101
+ } from './translator';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `{var}` substitution and CLDR plural-form selection.
3
+ * Pure string work: it never reads a catalog and never resolves a locale.
4
+ */
5
+ export type InterpolationValue = string | number | boolean;
6
+ export type InterpolationVars = Record<string, InterpolationValue>;
7
+ /** CLDR plural categories, in the order we probe catalog suffixes. */
8
+ export declare const PLURAL_CATEGORIES: readonly Intl.LDMLPluralRule[];
9
+ /**
10
+ * Replace `{name}` with `vars.name`. `{{` and `}}` escape a literal brace.
11
+ * An unknown variable renders loudly as `⟦name⟧` for the same reason a missing key
12
+ * does: a silently blank slot ships to production, a bracketed one does not.
13
+ */
14
+ export declare function interpolate(template: string, vars?: InterpolationVars): string;
15
+ /** Variable names a template expects — used by the extractor to flag arity drift. */
16
+ export declare function placeholdersOf(template: string): string[];
17
+ /**
18
+ * Pick the plural category for `count` in `locale` via `Intl.PluralRules` — CLDR
19
+ * categories, never an English `n === 1` check. `pl` has one/few/many/other, `ar`
20
+ * adds zero/two: a two-branch translator is wrong in both.
21
+ */
22
+ export declare function pluralCategory(count: number, locale: string): Intl.LDMLPluralRule;
23
+ /**
24
+ * Candidate keys for a plural lookup, most specific first.
25
+ *
26
+ * `items` + count 3 in `pl` → `items_many`, `items_plural`, `items_other`, `items`.
27
+ * `items` + count 1 in `pl` → `items_one`, `items`.
28
+ * `_plural` is the two-form authoring shortcut from the house style; the `_<category>`
29
+ * suffixes are what a 3+ form locale needs.
30
+ */
31
+ export declare function pluralKeyCandidates(key: string, count: number, locale: string): string[];
32
+ /** First candidate that exists, per the caller's `has` predicate; falls back to `key`. */
33
+ export declare function selectPluralKey(key: string, count: number, locale: string, has: (candidate: string) => boolean): string;
34
+ /** All suffixed variants of `key` a locale could legitimately define. */
35
+ export declare function pluralVariantsOf(key: string): string[];
36
+ //# sourceMappingURL=interpolate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.d.ts","sourceRoot":"","sources":["interpolate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAC3D,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAEnE,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,EAAE,SAAS,IAAI,CAAC,cAAc,EAO3D,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAU9E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,cAAc,CAEjF;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAIxF;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAClC,MAAM,CAKR;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAEtD"}