hanzio 2.2.0 → 3.1.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 (116) hide show
  1. package/README.md +133 -7
  2. package/dist/api-wrapper/connection-auth.d.ts +61 -0
  3. package/dist/cli/fields.d.ts +30 -0
  4. package/dist/cli/index.d.ts +122 -0
  5. package/dist/format/index.d.ts +91 -0
  6. package/dist/format/phone.d.ts +19 -0
  7. package/dist/http/index.d.ts +14 -0
  8. package/dist/i18n/catalog.d.ts +47 -0
  9. package/dist/i18n/check.d.ts +45 -0
  10. package/dist/i18n/cli.d.ts +2 -0
  11. package/dist/i18n/content-rules.d.ts +55 -0
  12. package/dist/i18n/i18next.d.ts +39 -0
  13. package/dist/i18n/index.d.ts +16 -1
  14. package/dist/i18n/locales.d.ts +62 -0
  15. package/dist/i18n/vite.d.ts +16 -0
  16. package/dist/i18n/vue.d.ts +93 -0
  17. package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
  18. package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
  19. package/dist/index-5n2e326q.js +409 -0
  20. package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
  21. package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
  22. package/dist/index-6ragggbs.js +473 -0
  23. package/dist/index-mq2qgc20.js +116 -0
  24. package/dist/index-najbyzpr.js +99 -0
  25. package/dist/index-ww09bm49.js +250 -0
  26. package/dist/iso/countries.d.ts +262 -0
  27. package/dist/iso/currencies.d.ts +165 -0
  28. package/dist/iso/index.d.ts +4 -0
  29. package/dist/iso/languages.d.ts +195 -0
  30. package/dist/iso/phone-language.d.ts +5 -0
  31. package/dist/promise/index.d.ts +31 -0
  32. package/dist/redis/client.d.ts +49 -0
  33. package/dist/redis/concurrency-limit.d.ts +63 -0
  34. package/dist/redis/index.d.ts +3 -0
  35. package/dist/redis/lua-script.d.ts +13 -0
  36. package/dist/secrets/infisical-identity-file.d.ts +12 -0
  37. package/dist/src/api-wrapper/connection-auth.js +82 -0
  38. package/dist/src/api-wrapper/index.js +1 -1
  39. package/dist/src/cli/index.js +10 -0
  40. package/dist/src/format/index.js +79 -2
  41. package/dist/src/format/phone.js +21 -0
  42. package/dist/src/http/index.js +69 -0
  43. package/dist/src/i18n/check.js +15 -0
  44. package/dist/src/i18n/cli.js +71 -0
  45. package/dist/src/i18n/i18next.js +48 -0
  46. package/dist/src/i18n/index.js +16 -34
  47. package/dist/src/i18n/vite.js +25 -0
  48. package/dist/src/i18n/vue.js +51 -0
  49. package/dist/src/index.js +22 -6
  50. package/dist/src/iso/index.js +655 -0
  51. package/dist/src/redis/index.js +577 -0
  52. package/dist/src/secrets/index.js +78 -35
  53. package/dist/src/sitemap/index.js +1 -1
  54. package/dist/src/storage/index.js +8 -117
  55. package/dist/src/testing/index.js +348 -0
  56. package/dist/src/zod/index.js +20 -253
  57. package/dist/string/index.d.ts +21 -0
  58. package/dist/testing/connection-auth-contract.d.ts +43 -0
  59. package/dist/testing/helpers.d.ts +26 -0
  60. package/dist/testing/index.d.ts +5 -0
  61. package/dist/testing/module-mocker.d.ts +30 -0
  62. package/dist/testing/prisma.d.ts +30 -0
  63. package/dist/testing/window.d.ts +18 -0
  64. package/dist/typedswitch/index.d.ts +55 -34
  65. package/dist/zod/index.d.ts +10 -0
  66. package/dist/zod/introspect.d.ts +2 -1
  67. package/package.json +97 -2
  68. package/src/api-wrapper/README.md +57 -2
  69. package/src/api-wrapper/SKILL.md +256 -0
  70. package/src/api-wrapper/connection-auth.ts +142 -0
  71. package/src/cli/README.md +220 -0
  72. package/src/cli/fields.ts +213 -0
  73. package/src/cli/index.ts +532 -0
  74. package/src/format/README.md +82 -2
  75. package/src/format/index.ts +148 -0
  76. package/src/format/phone.ts +43 -0
  77. package/src/http/README.md +25 -0
  78. package/src/http/index.ts +94 -0
  79. package/src/i18n/README.md +206 -3
  80. package/src/i18n/catalog.ts +139 -0
  81. package/src/i18n/check.ts +376 -0
  82. package/src/i18n/cli.ts +90 -0
  83. package/src/i18n/content-rules.ts +179 -0
  84. package/src/i18n/i18next.ts +89 -0
  85. package/src/i18n/index.ts +34 -0
  86. package/src/i18n/locales.ts +135 -0
  87. package/src/i18n/vite.ts +40 -0
  88. package/src/i18n/vue.ts +82 -0
  89. package/src/index.ts +1 -1
  90. package/src/iso/README.md +44 -0
  91. package/src/iso/countries.ts +272 -0
  92. package/src/iso/currencies.ts +172 -0
  93. package/src/iso/index.ts +8 -0
  94. package/src/iso/languages.ts +208 -0
  95. package/src/iso/phone-language.ts +46 -0
  96. package/src/promise/index.ts +100 -0
  97. package/src/redis/README.md +112 -0
  98. package/src/redis/client.ts +239 -0
  99. package/src/redis/concurrency-limit.ts +591 -0
  100. package/src/redis/index.ts +15 -0
  101. package/src/redis/lua-script.ts +43 -0
  102. package/src/secrets/README.md +5 -1
  103. package/src/secrets/infisical-identity-file.ts +69 -0
  104. package/src/secrets/infisical.ts +18 -3
  105. package/src/string/index.ts +59 -0
  106. package/src/testing/README.md +119 -0
  107. package/src/testing/connection-auth-contract.ts +303 -0
  108. package/src/testing/helpers.ts +41 -0
  109. package/src/testing/index.ts +12 -0
  110. package/src/testing/module-mocker.ts +50 -0
  111. package/src/testing/prisma.ts +78 -0
  112. package/src/testing/window.ts +119 -0
  113. package/src/typedswitch/README.md +55 -39
  114. package/src/typedswitch/index.ts +97 -83
  115. package/src/zod/index.ts +20 -0
  116. package/src/zod/introspect.ts +1 -1
@@ -0,0 +1,409 @@
1
+ import {
2
+ extractPlaceholders2,
3
+ flattenMessages2
4
+ } from "./index-najbyzpr.js";
5
+ import {
6
+ isPlainObject2
7
+ } from "./index-8r2vdzjg.js";
8
+
9
+ // src/i18n/check.ts
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ statSync,
16
+ writeFileSync
17
+ } from "node:fs";
18
+ import { dirname, join, relative, sep } from "node:path";
19
+
20
+ // src/i18n/catalog.ts
21
+ var MOJIBAKE = /(?:Ã.|Â.|â€|�)/;
22
+ var I18NEXT_PLURAL_SUFFIX = /_(zero|one|two|few|many|other)$/;
23
+ function parseMessages2(value, catalog, locale) {
24
+ const issues = [];
25
+ const issue = (message) => issues.push({ catalog, locale, message });
26
+ function visit(node, path) {
27
+ if (typeof node === "string") {
28
+ if (node.normalize("NFC") !== node) {
29
+ issue(`"${path}" is not Unicode NFC-normalized`);
30
+ }
31
+ if (MOJIBAKE.test(node))
32
+ issue(`"${path}" contains likely mojibake`);
33
+ return node;
34
+ }
35
+ if (!isPlainObject2(node)) {
36
+ issue(`"${path || "<root>"}" must be an object or string`);
37
+ return;
38
+ }
39
+ const result = {};
40
+ for (const [key, child] of Object.entries(node)) {
41
+ if (!key || key.includes(".")) {
42
+ issue(`"${path ? `${path}.` : ""}${key}" has an invalid key segment`);
43
+ continue;
44
+ }
45
+ const parsed = visit(child, path ? `${path}.${key}` : key);
46
+ if (parsed !== undefined)
47
+ result[key] = parsed;
48
+ }
49
+ return result;
50
+ }
51
+ const messages = visit(value, "");
52
+ return {
53
+ messages: typeof messages === "object" ? messages : undefined,
54
+ issues
55
+ };
56
+ }
57
+ function flattenCatalog(messages) {
58
+ return new Map(Object.entries(flattenMessages2(messages)));
59
+ }
60
+ var quote = (value) => `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
61
+ function generateCatalogTypes2(catalog, baseMessages, command = "hanzio-i18n generate") {
62
+ const entries = new Map;
63
+ for (const [rawKey, template] of flattenCatalog(baseMessages)) {
64
+ const isI18nextPlural = catalog.runtime === "i18next" && I18NEXT_PLURAL_SUFFIX.test(rawKey);
65
+ const key = isI18nextPlural ? rawKey.replace(I18NEXT_PLURAL_SUFFIX, "") : rawKey;
66
+ const params = entries.get(key) ?? new Set;
67
+ for (const name of extractPlaceholders2(template))
68
+ params.add(name);
69
+ const isVuePlural = catalog.runtime === "vue-i18n" && template.split("|").map((part) => part.trim()).filter(Boolean).length > 1;
70
+ if (isVuePlural || isI18nextPlural)
71
+ params.add("count");
72
+ entries.set(key, params);
73
+ }
74
+ const lines = [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([key, params]) => {
75
+ const value = params.size === 0 ? "never" : [...params].sort().map(quote).join(" | ");
76
+ return ` ${quote(key)}: ${value}`;
77
+ });
78
+ return [
79
+ "/**",
80
+ ` * Generated by \`${command}\` from the base catalog.`,
81
+ " * Do not edit by hand.",
82
+ " */",
83
+ `export type ${catalog.generatedTypeName ?? "TranslationParams"} = {`,
84
+ ...lines,
85
+ "}",
86
+ ""
87
+ ].join(`
88
+ `);
89
+ }
90
+
91
+ // src/i18n/content-rules.ts
92
+ var PLACEHOLDER_MASK = "";
93
+ var EXPRESSION_FRAGMENT = /(?:\?\?|=>|\bas\s+(?:string|number|boolean)\b)/;
94
+ var maskPlaceholders = (value) => value.replace(/\{[^}]*\}/g, PLACEHOLDER_MASK);
95
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
96
+ var wordPattern = (words, flags) => new RegExp(`\\b(?:${words.map(escapeRegExp).join("|")})\\w*`, flags);
97
+ var CODE_SHAPES = [
98
+ { name: "call expression", pattern: /^[A-Za-z_$][\w$]*\([^)]*\)$/ },
99
+ { name: "index expression", pattern: /^[A-Za-z_$][\w$]*\[[^\]]*\]$/ },
100
+ {
101
+ name: "member expression",
102
+ pattern: /^[a-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/
103
+ },
104
+ { name: "constant reference", pattern: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/ },
105
+ { name: "identifier", pattern: /^[a-z][a-z0-9]*(?:[A-Z][a-zA-Z0-9]*)+$/ },
106
+ { name: "expression fragment", pattern: EXPRESSION_FRAGMENT }
107
+ ];
108
+ var DEFAULT_BRANDS = ["API", "URL", "SMS", "CSV", "PDF", "ID", "OK"];
109
+ var NON_ASCII_LETTER = /(?![A-Za-z])\p{L}/u;
110
+ var NON_PROSE = /[/@#=_{}[\]<>]|\.[a-z]{2,}/;
111
+ function createContentRules(config = {}) {
112
+ const exempt = config.exemptKeyPrefixes ?? [];
113
+ const isExempt = (key) => exempt.some((prefix) => key.startsWith(prefix));
114
+ const codeAllow = new Set(config.allowCodeShapes ?? []);
115
+ const wrongLanguage = config.wrongLanguage;
116
+ const markerPattern = wrongLanguage?.markers.length ? new RegExp(`\\b(?:${wrongLanguage.markers.map(escapeRegExp).join("|")})\\b`, "giu") : undefined;
117
+ const markerLetters = wrongLanguage?.letters ? new RegExp(`[${escapeRegExp(wrongLanguage.letters)}]`, "u") : undefined;
118
+ const wrongLanguageAllow = new Set(wrongLanguage?.allowKeys ?? []);
119
+ const everywhere = config.bannedWords?.everywhere ?? [];
120
+ const translations = config.bannedWords?.translations ?? [];
121
+ const bannedInBase = everywhere.length ? wordPattern(everywhere, "iu") : undefined;
122
+ const bannedInTranslations = everywhere.length || translations.length ? wordPattern([...translations, ...everywhere], "iu") : undefined;
123
+ const bannedAllow = new Set(config.bannedWords?.allowKeys ?? []);
124
+ const brands = [...DEFAULT_BRANDS, ...config.identical?.brands ?? []];
125
+ const brandOrSymbols = new RegExp(`^[\\W\\d]*$|^(?:${brands.map(escapeRegExp).join("|")})\\b`, "i");
126
+ const identicalAllow = config.identical?.allowKeys ?? {};
127
+ const isAllowedIdentical = (key, locale) => {
128
+ const locales = Object.hasOwn(identicalAllow, key) ? identicalAllow[key] : undefined;
129
+ return locales === "all" || (locales?.includes(locale) ?? false);
130
+ };
131
+ return {
132
+ codeShape(value) {
133
+ const trimmed = value.trim();
134
+ if (!trimmed || codeAllow.has(trimmed))
135
+ return null;
136
+ const masked = maskPlaceholders(trimmed);
137
+ if (masked.includes(PLACEHOLDER_MASK))
138
+ return null;
139
+ if (/\s/.test(masked) && !EXPRESSION_FRAGMENT.test(masked))
140
+ return null;
141
+ return CODE_SHAPES.find(({ pattern }) => pattern.test(masked))?.name ?? null;
142
+ },
143
+ wrongLanguage(key, value) {
144
+ if (!markerPattern || wrongLanguageAllow.has(key))
145
+ return null;
146
+ const distinct = new Set((value.match(markerPattern) ?? []).map((m) => m.toLowerCase()));
147
+ if (distinct.size === 0)
148
+ return null;
149
+ if (distinct.size >= 2 || markerLetters?.test(value)) {
150
+ return [...distinct].sort().join(", ");
151
+ }
152
+ return null;
153
+ },
154
+ bannedWord(key, value, isBase) {
155
+ if (bannedAllow.has(key) || isExempt(key))
156
+ return null;
157
+ const pattern = isBase ? bannedInBase : bannedInTranslations;
158
+ return pattern?.exec(maskPlaceholders(value))?.[0] ?? null;
159
+ },
160
+ identicalLabel(key, locale, source, translated) {
161
+ if (source !== translated)
162
+ return false;
163
+ if (isAllowedIdentical(key, locale) || isExempt(key))
164
+ return false;
165
+ const value = translated.trim();
166
+ if (NON_PROSE.test(value) || brandOrSymbols.test(value))
167
+ return false;
168
+ if (NON_ASCII_LETTER.test(value))
169
+ return false;
170
+ return (value.match(/\p{L}{2,}/gu) ?? []).length >= 2;
171
+ }
172
+ };
173
+ }
174
+
175
+ // src/i18n/check.ts
176
+ function defineI18nConfig2(config) {
177
+ return config;
178
+ }
179
+ function vueSyntaxIssues(compiler, key, value) {
180
+ const issues = [];
181
+ if (compiler.detectHtmlTag(value)) {
182
+ issues.push(`"${key}" contains HTML, which vue-i18n rejects with strictMessage: "${value}"`);
183
+ }
184
+ const errors = [];
185
+ compiler.baseCompile(value, {
186
+ onError: (error) => errors.push(error.message)
187
+ });
188
+ if (errors.length > 0) {
189
+ issues.push(`"${key}" is not valid vue-i18n syntax (${errors.join("; ")}): "${value}" - escape literal { } and @ with {'{'} {'}'} {'@'}`);
190
+ }
191
+ return issues;
192
+ }
193
+ function readCatalog(path, catalog, locale) {
194
+ try {
195
+ return parseMessages2(JSON.parse(readFileSync(path, "utf8")), catalog, locale);
196
+ } catch (error) {
197
+ return {
198
+ messages: undefined,
199
+ issues: [{ catalog, locale, message: `invalid JSON - ${String(error)}` }]
200
+ };
201
+ }
202
+ }
203
+ function collectSourceFiles(path, files = []) {
204
+ if (!existsSync(path))
205
+ return files;
206
+ if (statSync(path).isFile()) {
207
+ if (/\.(tsx?|jsx?|vue|svelte)$/.test(path) && !/\.test\./.test(path)) {
208
+ files.push(path);
209
+ }
210
+ return files;
211
+ }
212
+ for (const name of readdirSync(path)) {
213
+ if (name !== "node_modules")
214
+ collectSourceFiles(join(path, name), files);
215
+ }
216
+ return files;
217
+ }
218
+ var KEY_REFERENCE = /[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+/g;
219
+ var KEY_FAMILY_PREFIX = /([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)*\.)\$\{/g;
220
+ function findUnusedKeys(root, catalog, keys) {
221
+ const references = new Set;
222
+ const prefixes = new Set(catalog.dynamicKeyPrefixes ?? []);
223
+ const catalogDir = join(root, catalog.dir) + sep;
224
+ const files = (catalog.sourceRoots ?? []).flatMap((dir) => collectSourceFiles(join(root, dir)));
225
+ for (const path of files) {
226
+ if (path.startsWith(catalogDir))
227
+ continue;
228
+ const source = readFileSync(path, "utf8");
229
+ for (const [reference] of source.matchAll(KEY_REFERENCE)) {
230
+ references.add(reference);
231
+ }
232
+ for (const [, prefix] of source.matchAll(KEY_FAMILY_PREFIX)) {
233
+ if (prefix)
234
+ prefixes.add(prefix);
235
+ }
236
+ }
237
+ return [...keys].filter((key) => !references.has(key) && ![...prefixes].some((prefix) => key.startsWith(prefix)));
238
+ }
239
+ var placeholderSet = (template) => extractPlaceholders2(template).sort().join(",");
240
+ async function validateCatalogs2(config, options) {
241
+ const { root, checkGeneratedTypes = true } = options;
242
+ const { baseLocale } = config;
243
+ const locales = [...config.locales].sort();
244
+ const rules = createContentRules(config.rules);
245
+ const issues = [];
246
+ const baseByCatalog = new Map;
247
+ let compiler;
248
+ if (config.catalogs.some((catalog) => catalog.runtime === "vue-i18n")) {
249
+ try {
250
+ compiler = await import("@intlify/message-compiler");
251
+ } catch {
252
+ issues.push({
253
+ catalog: "config",
254
+ message: "vue-i18n catalogs need @intlify/message-compiler installed to check message syntax"
255
+ });
256
+ }
257
+ }
258
+ for (const catalog of config.catalogs) {
259
+ const report = (message, locale) => issues.push({ catalog: catalog.name, locale, message });
260
+ const syntax = (locale, key, value) => {
261
+ if (catalog.runtime !== "vue-i18n" || !compiler)
262
+ return;
263
+ for (const message of vueSyntaxIssues(compiler, key, value)) {
264
+ report(message, locale);
265
+ }
266
+ };
267
+ const dir = join(root, catalog.dir);
268
+ if (!existsSync(dir)) {
269
+ report(`catalog directory does not exist: ${catalog.dir}`);
270
+ continue;
271
+ }
272
+ const files = readdirSync(dir).filter((file) => file.endsWith(".json")).map((file) => file.slice(0, -".json".length));
273
+ const missingFiles = locales.filter((locale) => !files.includes(locale));
274
+ const extraFiles = files.filter((locale) => !locales.includes(locale));
275
+ if (missingFiles.length > 0) {
276
+ report(`missing locale file(s): ${missingFiles.join(", ")}`);
277
+ }
278
+ if (extraFiles.length > 0) {
279
+ report(`unregistered locale file(s): ${extraFiles.sort().join(", ")}`);
280
+ }
281
+ const basePath = join(dir, `${baseLocale}.json`);
282
+ if (!existsSync(basePath))
283
+ continue;
284
+ const baseResult = readCatalog(basePath, catalog.name, baseLocale);
285
+ issues.push(...baseResult.issues);
286
+ if (!baseResult.messages)
287
+ continue;
288
+ baseByCatalog.set(catalog.name, baseResult.messages);
289
+ const base = flattenCatalog(baseResult.messages);
290
+ for (const [key, value] of base) {
291
+ const shape = rules.codeShape(value);
292
+ if (shape) {
293
+ report(`"${key}" looks like source code, not copy (${shape}): "${value}" - restore the expression at the call site instead of translating it`, baseLocale);
294
+ }
295
+ const markers = rules.wrongLanguage(key, value);
296
+ if (markers) {
297
+ report(`"${key}" is not ${baseLocale} (${markers}): "${value}"`, baseLocale);
298
+ }
299
+ const banned = rules.bannedWord(key, value, true);
300
+ if (banned) {
301
+ report(`"${key}" uses banned vocabulary "${banned}": "${value}"`, baseLocale);
302
+ }
303
+ syntax(baseLocale, key, value);
304
+ }
305
+ for (const locale of locales) {
306
+ if (locale === baseLocale)
307
+ continue;
308
+ const path = join(dir, `${locale}.json`);
309
+ if (!existsSync(path))
310
+ continue;
311
+ const result = readCatalog(path, catalog.name, locale);
312
+ issues.push(...result.issues);
313
+ if (!result.messages)
314
+ continue;
315
+ const translated = flattenCatalog(result.messages);
316
+ const stale = [...translated.keys()].filter((key) => !base.has(key));
317
+ const missing = [...base.keys()].filter((key) => !translated.has(key));
318
+ if (stale.length > 0) {
319
+ report(`stale key(s) not in ${baseLocale}.json: ${stale.join(", ")}`, locale);
320
+ }
321
+ if (missing.length > 0) {
322
+ report(`missing translation key(s): ${missing.join(", ")}`, locale);
323
+ }
324
+ for (const [key, value] of translated) {
325
+ const shape = rules.codeShape(value);
326
+ if (shape) {
327
+ report(`"${key}" looks like source code, not copy (${shape}): "${value}"`, locale);
328
+ }
329
+ const banned = rules.bannedWord(key, value, false);
330
+ if (banned) {
331
+ report(`"${key}" uses banned vocabulary "${banned}": "${value}"`, locale);
332
+ }
333
+ const source = base.get(key);
334
+ if (source !== undefined) {
335
+ if (rules.identicalLabel(key, locale, source, value)) {
336
+ report(`"${key}" is identical to ${baseLocale}: "${value}" - translate it, or allow it in rules.identical.allowKeys if it is correct as-is`, locale);
337
+ }
338
+ if (placeholderSet(source) !== placeholderSet(value)) {
339
+ report(`placeholder mismatch on "${key}" (${baseLocale}: "${source}" vs ${locale}: "${value}")`, locale);
340
+ }
341
+ }
342
+ syntax(locale, key, value);
343
+ }
344
+ }
345
+ if (catalog.sourceRoots?.length) {
346
+ const byNamespace = new Map;
347
+ for (const key of findUnusedKeys(root, catalog, base.keys())) {
348
+ const namespace = key.split(".")[0] ?? key;
349
+ byNamespace.set(namespace, [...byNamespace.get(namespace) ?? [], key]);
350
+ }
351
+ const scanned = catalog.sourceRoots.join(", ");
352
+ for (const [namespace, keys] of [...byNamespace].sort(([a], [b]) => a.localeCompare(b))) {
353
+ report(`${keys.length} unused key(s) under "${namespace}" - nothing in ${scanned} references them: ${keys.join(", ")}`);
354
+ }
355
+ }
356
+ if (checkGeneratedTypes && catalog.generatedTypes) {
357
+ const path = join(root, catalog.generatedTypes);
358
+ const expected = generateCatalogTypes2(catalog, baseResult.messages);
359
+ const actual = existsSync(path) ? readFileSync(path, "utf8") : null;
360
+ if (actual !== expected) {
361
+ report(`generated types are stale; run hanzio-i18n generate (${relative(root, path)})`);
362
+ }
363
+ }
364
+ }
365
+ for (const group of config.disjointNamespaces ?? []) {
366
+ const owners = new Map;
367
+ const collisions = new Set;
368
+ for (const name of group) {
369
+ for (const namespace of Object.keys(baseByCatalog.get(name) ?? {})) {
370
+ if (owners.has(namespace))
371
+ collisions.add(namespace);
372
+ else
373
+ owners.set(namespace, name);
374
+ }
375
+ }
376
+ if (collisions.size > 0) {
377
+ issues.push({
378
+ catalog: group.join("/"),
379
+ message: `shared top-level namespace(s): ${[...collisions].join(", ")}`
380
+ });
381
+ }
382
+ }
383
+ return issues;
384
+ }
385
+ function writeCatalogTypes2(config, options) {
386
+ const outputs = [];
387
+ const issues = [];
388
+ for (const catalog of config.catalogs) {
389
+ if (!catalog.generatedTypes)
390
+ continue;
391
+ const parsed = readCatalog(join(options.root, catalog.dir, `${config.baseLocale}.json`), catalog.name, config.baseLocale);
392
+ issues.push(...parsed.issues);
393
+ if (!parsed.messages)
394
+ continue;
395
+ outputs.push({
396
+ path: join(options.root, catalog.generatedTypes),
397
+ contents: generateCatalogTypes2(catalog, parsed.messages)
398
+ });
399
+ }
400
+ if (issues.length > 0)
401
+ return { written: [], issues };
402
+ for (const { path, contents } of outputs) {
403
+ mkdirSync(dirname(path), { recursive: true });
404
+ writeFileSync(path, contents);
405
+ }
406
+ return { written: outputs.map(({ path }) => path), issues };
407
+ }
408
+
409
+ export { parseMessages2, generateCatalogTypes2, defineI18nConfig2, validateCatalogs2, writeCatalogTypes2 };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stableStringify2
3
- } from "./index-kj1phgrx.js";
3
+ } from "./index-29mmh343.js";
4
4
 
5
5
  // src/array/index.ts
6
6
  function chunkArray2(array, chunkSize) {
@@ -138,6 +138,74 @@ async function pMap2(items, fn, options = {}) {
138
138
  await Promise.all(Array.from({ length: Math.min(concurrency, list.length) }, worker));
139
139
  return results;
140
140
  }
141
+ function createTaskPool2(options) {
142
+ const { concurrency, signal, stopOnError = true } = options;
143
+ if (!Number.isInteger(concurrency) || concurrency < 1)
144
+ throw new Error(`Task concurrency must be a positive integer, got ${concurrency}`);
145
+ const queued = [];
146
+ const idle = [];
147
+ let active = 0;
148
+ let failed = false;
149
+ let failure;
150
+ const throwIfFailed = () => {
151
+ signal?.throwIfAborted();
152
+ if (failed)
153
+ throw failure;
154
+ };
155
+ const stop = (error) => {
156
+ if (!failed) {
157
+ failed = true;
158
+ failure = error;
159
+ }
160
+ for (const task of queued.splice(0))
161
+ task.reject(failure);
162
+ };
163
+ const dispatch = () => {
164
+ if (signal?.aborted)
165
+ stop(signal.reason);
166
+ while (!failed && active < concurrency && queued.length) {
167
+ active += 1;
168
+ queued.shift().start();
169
+ }
170
+ if (!active && !queued.length)
171
+ for (const resolve of idle.splice(0))
172
+ resolve();
173
+ };
174
+ const run = (task) => {
175
+ const result = new Promise((resolve, reject) => {
176
+ if (failed || signal?.aborted) {
177
+ stop(signal?.aborted ? signal.reason : failure);
178
+ reject(failure);
179
+ return;
180
+ }
181
+ queued.push({
182
+ reject,
183
+ start: () => {
184
+ Promise.resolve().then(() => {
185
+ throwIfFailed();
186
+ return task();
187
+ }).then(resolve, (error) => {
188
+ if (stopOnError || signal?.aborted)
189
+ stop(error);
190
+ reject(error);
191
+ }).finally(() => {
192
+ active -= 1;
193
+ dispatch();
194
+ });
195
+ }
196
+ });
197
+ dispatch();
198
+ });
199
+ result.catch(() => {});
200
+ return result;
201
+ };
202
+ const drain = async () => {
203
+ if (active || queued.length)
204
+ await new Promise((resolve) => idle.push(resolve));
205
+ throwIfFailed();
206
+ };
207
+ return { run, drain, throwIfFailed };
208
+ }
141
209
  var clock = () => globalThis.performance?.now() ?? Date.now();
142
210
  function startTimer2() {
143
211
  const start = clock();
@@ -221,4 +289,4 @@ function createLatestGuard2() {
221
289
  };
222
290
  }
223
291
 
224
- export { ok2, err2, isSuccess2, isFailure2, unwrapResult2, backgroundPromise2, backgroundPromiseSync2, promiseTimeout2, tryCatch2, PromiseTimeoutError2, withTimeout2, exponentialBackoff2, retry2, pMap2, startTimer2, timed2, anySignal2, timeoutSignal2, raceAbort2, createLatestGuard2 };
292
+ export { ok2, err2, isSuccess2, isFailure2, unwrapResult2, backgroundPromise2, backgroundPromiseSync2, promiseTimeout2, tryCatch2, PromiseTimeoutError2, withTimeout2, exponentialBackoff2, retry2, pMap2, createTaskPool2, startTimer2, timed2, anySignal2, timeoutSignal2, raceAbort2, createLatestGuard2 };