@cliff-format/i18n 0.0.0-stage → 0.2.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/dist/lite.cjs ADDED
@@ -0,0 +1,1798 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/lite.ts
21
+ var lite_exports = {};
22
+ __export(lite_exports, {
23
+ Catalog: () => Catalog,
24
+ CliffAmbiguousEntryError: () => CliffAmbiguousEntryError,
25
+ CliffDuplicateEntryError: () => CliffDuplicateEntryError,
26
+ CliffI18nError: () => CliffI18nError,
27
+ CliffIcuError: () => CliffIcuError,
28
+ CliffMissingEntryError: () => CliffMissingEntryError,
29
+ CliffMissingMetadataError: () => CliffMissingMetadataError,
30
+ CliffScopeError: () => CliffScopeError,
31
+ CliffScopeMismatchError: () => CliffScopeMismatchError,
32
+ EMOTION: () => EMOTION,
33
+ MessageBuilderImpl: () => MessageBuilderImpl,
34
+ STATUS: () => STATUS,
35
+ STATUS_ORDER: () => STATUS_ORDER,
36
+ STATUS_TRANSITIONS: () => STATUS_TRANSITIONS,
37
+ TYPE: () => TYPE,
38
+ VARIANT: () => VARIANT,
39
+ analyzePlain: () => analyzePlain,
40
+ argumentNames: () => argumentNames,
41
+ assertCatalogParts: () => assertCatalogParts,
42
+ assertEmotionTag: () => assertEmotionTag,
43
+ assertScope: () => assertScope,
44
+ assertStatusTag: () => assertStatusTag,
45
+ assertTypeTag: () => assertTypeTag,
46
+ assertVariantTag: () => assertVariantTag,
47
+ canTransitionStatus: () => canTransitionStatus,
48
+ createI18n: () => createI18n,
49
+ createLiteIcu: () => createLiteIcu,
50
+ createT: () => createT,
51
+ detectDialect: () => detectDialect,
52
+ isEmotionTag: () => isEmotionTag,
53
+ isStatusTag: () => isStatusTag,
54
+ isTypeTag: () => isTypeTag,
55
+ isVariantTag: () => isVariantTag,
56
+ joinCanonical: () => joinCanonical,
57
+ nextStatuses: () => nextStatuses,
58
+ reconcileEntryStatus: () => reconcileEntryStatus,
59
+ resolveEntryId: () => resolveEntryId,
60
+ statusOf: () => statusOf,
61
+ statusOrder: () => statusOrder
62
+ });
63
+ module.exports = __toCommonJS(lite_exports);
64
+
65
+ // src/engine.ts
66
+ var import_core5 = require("@cliff-format/core");
67
+
68
+ // src/builder.ts
69
+ var import_core2 = require("@cliff-format/core");
70
+
71
+ // src/errors.ts
72
+ var CliffI18nError = class extends Error {
73
+ constructor(message, options) {
74
+ super(message, options);
75
+ this.name = "CliffI18nError";
76
+ }
77
+ };
78
+ var CliffScopeError = class extends CliffI18nError {
79
+ scope;
80
+ constructor(message, scope = {}) {
81
+ super(message);
82
+ this.name = "CliffScopeError";
83
+ this.scope = scope;
84
+ }
85
+ };
86
+ var CliffScopeMismatchError = class extends CliffScopeError {
87
+ entryId;
88
+ constructor(entryId, scope, expected) {
89
+ super(
90
+ `entry ID "${entryId}" resolves outside the active scope (${scope.namespace}.${scope.clan}); expected ${expected}`,
91
+ scope
92
+ );
93
+ this.name = "CliffScopeMismatchError";
94
+ this.entryId = entryId;
95
+ }
96
+ };
97
+ var CliffMissingEntryError = class extends CliffI18nError {
98
+ entryId;
99
+ locale;
100
+ constructor(entryId, locale) {
101
+ super(`missing CLIFF entry "${entryId}" for locale "${locale}"`);
102
+ this.name = "CliffMissingEntryError";
103
+ this.entryId = entryId;
104
+ this.locale = locale;
105
+ }
106
+ };
107
+ var CliffAmbiguousEntryError = class extends CliffI18nError {
108
+ entryId;
109
+ candidates;
110
+ constructor(entryId, candidates) {
111
+ super(`ambiguous CLIFF entry "${entryId}"; candidates: ${candidates.join(", ")}`);
112
+ this.name = "CliffAmbiguousEntryError";
113
+ this.entryId = entryId;
114
+ this.candidates = candidates;
115
+ }
116
+ };
117
+ var CliffDuplicateEntryError = class extends CliffI18nError {
118
+ entryId;
119
+ locale;
120
+ constructor(entryId, locale) {
121
+ super(`duplicate canonical CLIFF entry ID "${entryId}" for locale "${locale}"`);
122
+ this.name = "CliffDuplicateEntryError";
123
+ this.entryId = entryId;
124
+ this.locale = locale;
125
+ }
126
+ };
127
+ var CliffMissingMetadataError = class extends CliffI18nError {
128
+ entryId;
129
+ fields;
130
+ constructor(entryId, fields, detail = "") {
131
+ super(
132
+ `entry ${entryId} is missing required CLIFF field${fields.length === 1 ? "" : "s"} ${fields.map((field) => `"${field}"`).join(", ")}${detail ? "; " + detail : ""}`
133
+ );
134
+ this.name = "CliffMissingMetadataError";
135
+ this.entryId = entryId;
136
+ this.fields = fields;
137
+ }
138
+ };
139
+ var CliffIcuError = class extends CliffI18nError {
140
+ messageText;
141
+ constructor(messageText, detail) {
142
+ super(`invalid ICU message: ${detail}`);
143
+ this.name = "CliffIcuError";
144
+ this.messageText = messageText;
145
+ }
146
+ };
147
+
148
+ // src/types.ts
149
+ var TYPE = {
150
+ noun: "noun",
151
+ verb: "verb",
152
+ adjective: "adjective",
153
+ adverb: "adverb",
154
+ pronoun: "pronoun",
155
+ numeral: "numeral",
156
+ preposition: "preposition",
157
+ conjunction: "conjunction",
158
+ particle: "particle",
159
+ interjection: "interjection",
160
+ "proper-noun": "proper-noun",
161
+ "noun-phrase": "noun-phrase",
162
+ "verb-phrase": "verb-phrase",
163
+ "adjective-phrase": "adjective-phrase",
164
+ "adverb-phrase": "adverb-phrase",
165
+ "fixed-phrase": "fixed-phrase",
166
+ idiom: "idiom",
167
+ sentence: "sentence",
168
+ description: "description",
169
+ narration: "narration",
170
+ dialogue: "dialogue",
171
+ monologue: "monologue",
172
+ prompt: "prompt",
173
+ label: "label",
174
+ subtitle: "subtitle",
175
+ "accessibility-cue": "accessibility-cue"
176
+ };
177
+ var EMOTION = {
178
+ neutral: "neutral",
179
+ objective: "objective",
180
+ mechanical: "mechanical",
181
+ joyful: "joyful",
182
+ sad: "sad",
183
+ angry: "angry",
184
+ fearful: "fearful",
185
+ surprised: "surprised",
186
+ curious: "curious",
187
+ disgusted: "disgusted",
188
+ anxious: "anxious",
189
+ calm: "calm",
190
+ playful: "playful",
191
+ serious: "serious",
192
+ urgent: "urgent",
193
+ romantic: "romantic",
194
+ hopeful: "hopeful",
195
+ grateful: "grateful",
196
+ formal: "formal",
197
+ informal: "informal",
198
+ polite: "polite",
199
+ rude: "rude",
200
+ nostalgic: "nostalgic"
201
+ };
202
+ var STATUS = {
203
+ initial: "initial",
204
+ translated: "translated",
205
+ reviewed: "reviewed",
206
+ final: "final"
207
+ };
208
+ var VARIANT = {
209
+ standard: "standard",
210
+ glossary: "glossary"
211
+ };
212
+ var STATUS_ORDER = ["initial", "translated", "reviewed", "final"];
213
+
214
+ // src/vocabulary.ts
215
+ var import_core = require("@cliff-format/core");
216
+ function isTypeTag(value) {
217
+ return import_core.TYPE_TAGS.has(value);
218
+ }
219
+ function isEmotionTag(value) {
220
+ return import_core.EMOTION_TAGS.has(value);
221
+ }
222
+ function isStatusTag(value) {
223
+ return import_core.STATUS_TAGS.has(value);
224
+ }
225
+ function isVariantTag(value) {
226
+ return import_core.VARIANT_TAGS.has(value);
227
+ }
228
+ function assertTypeTag(value) {
229
+ if (!isTypeTag(value)) {
230
+ throw new TypeError(`invalid CLIFF type tag "${value}"`);
231
+ }
232
+ return value;
233
+ }
234
+ function assertEmotionTag(value) {
235
+ if (!isEmotionTag(value)) {
236
+ throw new TypeError(`invalid CLIFF emotion tag "${value}"`);
237
+ }
238
+ return value;
239
+ }
240
+ function assertStatusTag(value) {
241
+ if (!isStatusTag(value)) {
242
+ throw new TypeError(`invalid CLIFF status tag "${value}"`);
243
+ }
244
+ return value;
245
+ }
246
+ function assertVariantTag(value) {
247
+ if (!isVariantTag(value)) {
248
+ throw new TypeError(`invalid CLIFF variant tag "${value}"`);
249
+ }
250
+ return value;
251
+ }
252
+
253
+ // src/status.ts
254
+ var STATUS_TRANSITIONS = {
255
+ initial: ["translated"],
256
+ translated: ["reviewed", "translated"],
257
+ reviewed: ["final", "translated"],
258
+ final: ["translated"]
259
+ };
260
+ function statusOf(entry) {
261
+ const value = entry.status ?? "";
262
+ return isStatusTag(value) ? value : "initial";
263
+ }
264
+ function canTransitionStatus(from, to) {
265
+ return STATUS_TRANSITIONS[from].includes(to);
266
+ }
267
+ function reconcileEntryStatus(entry, previous = {}) {
268
+ if (previous.explicit !== void 0) return previous.explicit;
269
+ const current = statusOf(entry);
270
+ if (entry.target === void 0) return "initial";
271
+ if (current === "initial") return "translated";
272
+ const sourceChanged = previous.source !== void 0 && previous.source !== entry.source;
273
+ if (sourceChanged) return "translated";
274
+ const targetChanged = previous.target !== void 0 && previous.target !== entry.target;
275
+ if (targetChanged && (current === "reviewed" || current === "final")) {
276
+ return "translated";
277
+ }
278
+ return current;
279
+ }
280
+ function nextStatuses(status) {
281
+ return STATUS_TRANSITIONS[status];
282
+ }
283
+ function statusOrder() {
284
+ return STATUS_ORDER;
285
+ }
286
+
287
+ // src/builder.ts
288
+ function normalizeEmotion(tags) {
289
+ const out = [];
290
+ for (const tag of tags) {
291
+ if (Array.isArray(tag)) {
292
+ for (const item of tag) out.push(assertEmotionTag(item));
293
+ } else {
294
+ out.push(assertEmotionTag(tag));
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ function sameEmotion(left, right) {
300
+ if (left.length !== right.length) return false;
301
+ return left.every((tag, index) => tag === right[index]);
302
+ }
303
+ var MessageBuilderImpl = class {
304
+ constructor(i18n, scope, entryIdInput) {
305
+ this.i18n = i18n;
306
+ this.scope = scope;
307
+ this.entryIdInput = entryIdInput;
308
+ }
309
+ i18n;
310
+ scope;
311
+ entryIdInput;
312
+ sourceText;
313
+ sourceParams = {};
314
+ targetText;
315
+ targetParams = {};
316
+ extraParams = {};
317
+ typeTag;
318
+ inheritType = false;
319
+ emotionTags = [];
320
+ statusTag;
321
+ contextText;
322
+ maxWidthCells;
323
+ referencePaths = [];
324
+ reviewerName;
325
+ extensionMap = {};
326
+ localeOverride;
327
+ source(text, params = {}) {
328
+ if (typeof text !== "string") throw new TypeError("source text must be a string");
329
+ this.sourceText = text;
330
+ this.sourceParams = { ...this.sourceParams, ...params };
331
+ return this;
332
+ }
333
+ type(tag) {
334
+ this.typeTag = assertTypeTag(tag);
335
+ this.inheritType = false;
336
+ return this;
337
+ }
338
+ typeFromGroup() {
339
+ if (this.sourceText === void 0) {
340
+ throw new CliffI18nError(".typeFromGroup() must be called after .source()");
341
+ }
342
+ if (this.typeTag !== void 0) {
343
+ throw new CliffI18nError(".typeFromGroup() cannot be used after .type()");
344
+ }
345
+ this.inheritType = true;
346
+ return this;
347
+ }
348
+ emotion(...tags) {
349
+ this.emotionTags = normalizeEmotion(tags);
350
+ return this;
351
+ }
352
+ context(text) {
353
+ this.contextText = text;
354
+ return this;
355
+ }
356
+ maxWidth(cells) {
357
+ if (!Number.isInteger(cells) || cells <= 0) {
358
+ throw new TypeError("max-width must be a positive integer");
359
+ }
360
+ this.maxWidthCells = cells;
361
+ return this;
362
+ }
363
+ reference(...paths) {
364
+ this.referencePaths = [...this.referencePaths, ...paths];
365
+ return this;
366
+ }
367
+ reviewer(name) {
368
+ this.reviewerName = name;
369
+ return this;
370
+ }
371
+ extension(name, value) {
372
+ if (!name.startsWith("x-")) {
373
+ throw new TypeError(`extension name "${name}" must start with "x-"`);
374
+ }
375
+ this.extensionMap[name] = value;
376
+ return this;
377
+ }
378
+ target(text, params = {}) {
379
+ this.targetText = text;
380
+ this.targetParams = { ...this.targetParams, ...params };
381
+ return this;
382
+ }
383
+ status(tag) {
384
+ this.statusTag = assertStatusTag(tag);
385
+ return this;
386
+ }
387
+ locale(tag) {
388
+ this.localeOverride = tag;
389
+ return this;
390
+ }
391
+ params(params) {
392
+ this.extraParams = { ...this.extraParams, ...params };
393
+ return this;
394
+ }
395
+ count(value) {
396
+ this.extraParams = { ...this.extraParams, count: value, n: value };
397
+ return this;
398
+ }
399
+ activeLocale() {
400
+ return this.localeOverride ?? this.i18n.getLocale();
401
+ }
402
+ resolved() {
403
+ return this.i18n.resolve(this.entryIdInput, this.scope);
404
+ }
405
+ recordFor(resolved) {
406
+ return this.i18n.recordFor(resolved.canonical, this.activeLocale());
407
+ }
408
+ mergedParams(params) {
409
+ return {
410
+ ...this.i18n.defaultParams,
411
+ ...this.sourceParams,
412
+ ...this.targetParams,
413
+ ...this.extraParams,
414
+ ...params
415
+ };
416
+ }
417
+ diagnose(code, severity, message, resolved) {
418
+ this.i18n.diagnose({
419
+ code,
420
+ severity,
421
+ message,
422
+ ...resolved === void 0 ? {} : { entryId: resolved.canonical },
423
+ locale: this.activeLocale(),
424
+ namespace: this.scope.namespace,
425
+ clan: this.scope.clan,
426
+ ...this.scope.group === void 0 ? {} : { group: this.scope.group }
427
+ });
428
+ }
429
+ requireEffectiveType(resolved) {
430
+ if (this.typeTag !== void 0) return this.typeTag;
431
+ if (!this.inheritType) return void 0;
432
+ const group = this.i18n.getCatalog().findGroup(resolved.namespace, resolved.clan, resolved.group);
433
+ if (group?.type === void 0) {
434
+ throw new CliffMissingMetadataError(
435
+ resolved.canonical,
436
+ ["type"],
437
+ `group [${resolved.group}] has no type; add .type(TYPE...) or declare type on the group`
438
+ );
439
+ }
440
+ return group.type;
441
+ }
442
+ ensureAuthoringMetadata(resolved) {
443
+ if (this.sourceText === void 0) return;
444
+ if (this.typeTag !== void 0) return;
445
+ if (this.inheritType) {
446
+ this.requireEffectiveType(resolved);
447
+ return;
448
+ }
449
+ throw new CliffMissingMetadataError(
450
+ resolved.canonical,
451
+ ["type"],
452
+ "add .type(TYPE...) or .typeFromGroup()"
453
+ );
454
+ }
455
+ groupEmotion(resolved) {
456
+ const group = this.i18n.getCatalog().findGroup(resolved.namespace, resolved.clan, resolved.group);
457
+ return group?.emotion ? [...group.emotion] : [];
458
+ }
459
+ requireEmotionIfConfigured(resolved, effectiveType2) {
460
+ const requirement = this.i18n.requireEmotion;
461
+ if (requirement === false) return;
462
+ if (this.emotionTags.length > 0) return;
463
+ const groupEmotion = this.groupEmotion(resolved);
464
+ if (groupEmotion.length > 0) return;
465
+ if (requirement === true) {
466
+ throw new CliffMissingMetadataError(
467
+ resolved.canonical,
468
+ ["emotion"],
469
+ "requireEmotion is enabled; add .emotion(...) or declare emotion on the group"
470
+ );
471
+ }
472
+ if (effectiveType2 !== void 0 && requirement.includes(effectiveType2)) {
473
+ throw new CliffMissingMetadataError(
474
+ resolved.canonical,
475
+ ["emotion"],
476
+ `requireEmotion requires an explicit emotion for type "${effectiveType2}"`
477
+ );
478
+ }
479
+ }
480
+ createDraft(resolved) {
481
+ const sourceLanguage = this.i18n.getSourceLanguage();
482
+ const targetLanguage = this.activeLocale();
483
+ const effectiveType2 = this.requireEffectiveType(resolved);
484
+ this.requireEmotionIfConfigured(resolved, effectiveType2);
485
+ const status = this.statusTag ?? (this.targetText === void 0 ? "initial" : "translated");
486
+ if (status !== "initial" && this.targetText === void 0) {
487
+ throw new CliffMissingMetadataError(
488
+ resolved.canonical,
489
+ ["target"],
490
+ `status "${status}" requires a target field`
491
+ );
492
+ }
493
+ const record = this.i18n.getCatalog().addDraft(
494
+ {
495
+ namespace: resolved.namespace,
496
+ clan: resolved.clan,
497
+ group: resolved.group,
498
+ id: resolved.id,
499
+ sourceLanguage,
500
+ targetLanguage
501
+ },
502
+ {
503
+ ...this.sourceText === void 0 ? {} : { source: this.sourceText },
504
+ ...this.targetText === void 0 ? {} : { target: this.targetText },
505
+ ...this.typeTag === void 0 ? {} : { type: this.typeTag },
506
+ emotion: this.emotionTags,
507
+ status,
508
+ ...this.contextText === void 0 ? {} : { context: this.contextText },
509
+ ...this.maxWidthCells === void 0 ? {} : { maxWidth: this.maxWidthCells },
510
+ ...this.referencePaths.length === 0 ? {} : { reference: this.referencePaths },
511
+ ...this.reviewerName === void 0 ? {} : { reviewer: this.reviewerName },
512
+ ...Object.keys(this.extensionMap).length === 0 ? {} : { extensions: this.extensionMap }
513
+ }
514
+ );
515
+ this.diagnose("draft-created", "info", `created CLIFF draft "${resolved.canonical}"`, resolved);
516
+ return record;
517
+ }
518
+ checkWidth(text, maxWidth, resolved, locale) {
519
+ const policy = this.i18n.enforceMaxWidth;
520
+ if (policy === "off") return;
521
+ const cells = (0, import_core2.displayWidth)(text);
522
+ if (cells <= maxWidth) return;
523
+ const message = `rendered display width ${cells} cells exceeds max-width ${maxWidth} for "${resolved.canonical}" in "${locale}"`;
524
+ if (policy === "error") {
525
+ this.diagnose("width-exceeded", "error", message, resolved);
526
+ throw new CliffI18nError(message);
527
+ }
528
+ this.diagnose("width-exceeded", "warning", message, resolved);
529
+ }
530
+ renderRecord(record, resolved, params, parts) {
531
+ const target = this.targetText ?? record.target;
532
+ const template = target ?? record.source;
533
+ const formatLocale = target !== void 0 ? record.locale : record.sourceLanguage;
534
+ const values = this.mergedParams(params);
535
+ const maxWidth = this.maxWidthCells ?? record.maxWidth;
536
+ if (parts) {
537
+ const result = this.i18n.formatToParts(template, values, { locale: formatLocale });
538
+ if (maxWidth !== void 0) {
539
+ const text2 = result.filter((part) => part.type !== "markup").map((part) => part.type === "literal" ? part.value : part.value).join("");
540
+ this.checkWidth(text2, maxWidth, resolved, formatLocale);
541
+ }
542
+ return result;
543
+ }
544
+ const text = this.i18n.format(template, values, { locale: formatLocale });
545
+ if (maxWidth !== void 0) this.checkWidth(text, maxWidth, resolved, formatLocale);
546
+ return text;
547
+ }
548
+ expectationDiagnostics(record, resolved) {
549
+ if (this.sourceText !== void 0 && this.sourceText !== record.source) {
550
+ this.diagnose(
551
+ "source-mismatch",
552
+ "warning",
553
+ `source in CLIFF differs from .source(); CLIFF keeps "${record.source}"`,
554
+ resolved
555
+ );
556
+ }
557
+ if (this.typeTag !== void 0 && record.type !== this.typeTag) {
558
+ this.diagnose(
559
+ "type-mismatch",
560
+ "warning",
561
+ `entry type is "${record.type ?? ""}", not "${this.typeTag}"`,
562
+ resolved
563
+ );
564
+ }
565
+ if (this.emotionTags.length > 0 && !sameEmotion(this.emotionTags, record.emotion)) {
566
+ this.diagnose(
567
+ "emotion-mismatch",
568
+ "warning",
569
+ `entry emotion is [${record.emotion.join(", ")}], not [${this.emotionTags.join(", ")}]`,
570
+ resolved
571
+ );
572
+ }
573
+ }
574
+ missing(resolved) {
575
+ const message = `missing CLIFF entry "${resolved.canonical}" for locale "${this.activeLocale()}"`;
576
+ this.diagnose("missing-entry", "warning", message, resolved);
577
+ const behavior = this.i18n.missing;
578
+ if (behavior === "throw") {
579
+ throw new CliffMissingEntryError(resolved.canonical, this.activeLocale());
580
+ }
581
+ if (behavior === "empty") return "";
582
+ if (behavior === "source") return this.sourceText ?? "";
583
+ if (typeof this.entryIdInput === "string") return this.entryIdInput;
584
+ return resolved.canonical;
585
+ }
586
+ render(params) {
587
+ const resolved = this.resolved();
588
+ const record = this.recordFor(resolved);
589
+ if (this.sourceText === void 0) {
590
+ if (record === void 0) return this.missing(resolved);
591
+ this.expectationDiagnostics(record, resolved);
592
+ return this.renderRecord(record, resolved, params, false);
593
+ }
594
+ this.ensureAuthoringMetadata(resolved);
595
+ if (record !== void 0) {
596
+ this.expectationDiagnostics(record, resolved);
597
+ return this.renderRecord(record, resolved, params, false);
598
+ }
599
+ const draft = this.createDraft(resolved);
600
+ return this.renderRecord(draft, resolved, params, false);
601
+ }
602
+ renderToParts(params) {
603
+ const resolved = this.resolved();
604
+ const record = this.recordFor(resolved);
605
+ if (this.sourceText === void 0) {
606
+ if (record === void 0) {
607
+ const text = this.missing(resolved);
608
+ return [{ type: "literal", value: text }];
609
+ }
610
+ this.expectationDiagnostics(record, resolved);
611
+ return this.renderRecord(record, resolved, params, true);
612
+ }
613
+ this.ensureAuthoringMetadata(resolved);
614
+ if (record !== void 0) {
615
+ this.expectationDiagnostics(record, resolved);
616
+ return this.renderRecord(record, resolved, params, true);
617
+ }
618
+ const draft = this.createDraft(resolved);
619
+ return this.renderRecord(draft, resolved, params, true);
620
+ }
621
+ meta() {
622
+ const resolved = this.resolved();
623
+ const record = this.recordFor(resolved);
624
+ const source = this.sourceText ?? record?.source ?? "";
625
+ const target = this.targetText ?? record?.target;
626
+ const type = this.typeTag ?? record?.type;
627
+ const emotion = this.emotionTags.length > 0 ? [...this.emotionTags] : record?.emotion ?? [];
628
+ const status = this.statusTag ?? record?.status;
629
+ const context = this.contextText ?? record?.context;
630
+ const maxWidth = this.maxWidthCells ?? record?.maxWidth;
631
+ const reference = this.referencePaths.length > 0 ? [...this.referencePaths] : record?.reference ?? [];
632
+ const reviewer = this.reviewerName ?? record?.reviewer;
633
+ return {
634
+ entryId: resolved.canonical,
635
+ namespace: resolved.namespace,
636
+ clan: resolved.clan,
637
+ group: resolved.group,
638
+ id: resolved.id,
639
+ locale: this.activeLocale(),
640
+ sourceLanguage: record?.sourceLanguage ?? this.i18n.getSourceLanguage(),
641
+ targetLanguage: record?.locale ?? this.activeLocale(),
642
+ variant: record?.variant ?? "standard",
643
+ source,
644
+ ...target === void 0 ? {} : { target },
645
+ ...type === void 0 ? {} : { type },
646
+ emotion,
647
+ ...status === void 0 ? {} : { status },
648
+ ...context === void 0 ? {} : { context },
649
+ ...maxWidth === void 0 ? {} : { maxWidth },
650
+ reference,
651
+ ...reviewer === void 0 ? {} : { reviewer },
652
+ extensions: record?.extensions ?? { ...this.extensionMap }
653
+ };
654
+ }
655
+ entry() {
656
+ const resolved = this.resolved();
657
+ return this.recordFor(resolved)?.entry;
658
+ }
659
+ commit(options = {}) {
660
+ const resolved = this.resolved();
661
+ const existing = this.recordFor(resolved);
662
+ if (existing === void 0) {
663
+ this.ensureAuthoringMetadata(resolved);
664
+ const draft = this.createDraft(resolved);
665
+ this.validateDocument(draft);
666
+ return this;
667
+ }
668
+ if (options.overwrite === true) {
669
+ this.ensureAuthoringMetadata(resolved);
670
+ this.applyOverwrite(existing);
671
+ } else if (this.targetText !== void 0) {
672
+ const previous = {
673
+ ...existing.entry.source === void 0 ? {} : { source: existing.entry.source },
674
+ ...existing.entry.target === void 0 ? {} : { target: existing.entry.target },
675
+ ...this.statusTag === void 0 ? {} : { explicit: this.statusTag }
676
+ };
677
+ existing.entry.target = this.targetText;
678
+ existing.entry.status = reconcileEntryStatus(existing.entry, previous);
679
+ } else {
680
+ this.ensureAuthoringMetadata(resolved);
681
+ }
682
+ this.validateDocument(existing);
683
+ return this;
684
+ }
685
+ applyOverwrite(record) {
686
+ const entry = record.entry;
687
+ const previous = {
688
+ ...entry.source === void 0 ? {} : { source: entry.source },
689
+ ...entry.target === void 0 ? {} : { target: entry.target },
690
+ ...this.statusTag === void 0 ? {} : { explicit: this.statusTag }
691
+ };
692
+ if (this.sourceText !== void 0) entry.source = this.sourceText;
693
+ if (this.targetText !== void 0) entry.target = this.targetText;
694
+ if (this.typeTag !== void 0) entry.type = this.typeTag;
695
+ if (this.emotionTags.length > 0) entry.emotion = [...this.emotionTags];
696
+ if (this.contextText !== void 0) entry.context = this.contextText;
697
+ if (this.maxWidthCells !== void 0) entry.maxWidth = this.maxWidthCells;
698
+ if (this.referencePaths.length > 0) entry.reference = [...this.referencePaths];
699
+ if (this.reviewerName !== void 0) entry.reviewer = this.reviewerName;
700
+ if (Object.keys(this.extensionMap).length > 0) {
701
+ entry.extensions = { ...entry.extensions, ...this.extensionMap };
702
+ }
703
+ entry.status = reconcileEntryStatus(entry, previous);
704
+ }
705
+ validateDocument(record) {
706
+ const issues = (0, import_core2.validateDocument)(record.document);
707
+ if ((0, import_core2.hasErrors)(issues)) {
708
+ throw new import_core2.CliffValidationError(issues, record.document.corrections);
709
+ }
710
+ }
711
+ width(params) {
712
+ return (0, import_core2.displayWidth)(this.render(params));
713
+ }
714
+ fits(params) {
715
+ const resolved = this.resolved();
716
+ const record = this.recordFor(resolved);
717
+ const maxWidth = this.maxWidthCells ?? record?.maxWidth;
718
+ if (maxWidth === void 0) return true;
719
+ return this.width(params) <= maxWidth;
720
+ }
721
+ toString() {
722
+ return this.render();
723
+ }
724
+ };
725
+ function createT(i18n, scope) {
726
+ const callable = (entryId, params) => {
727
+ const builder = new MessageBuilderImpl(i18n, scope, entryId);
728
+ if (params === void 0) {
729
+ return builder;
730
+ }
731
+ return builder.params(params).render();
732
+ };
733
+ const fn = callable;
734
+ fn.render = (entryId, params) => new MessageBuilderImpl(i18n, scope, entryId).render(params);
735
+ fn.renderToParts = (entryId, params) => new MessageBuilderImpl(i18n, scope, entryId).renderToParts(params);
736
+ fn.has = (entryId) => {
737
+ const resolved = i18n.resolve(entryId, scope);
738
+ return resolved.exists && i18n.recordFor(resolved.canonical, i18n.getLocale()) !== void 0;
739
+ };
740
+ fn.entry = (entryId) => {
741
+ const resolved = i18n.resolve(entryId, scope);
742
+ return i18n.recordFor(resolved.canonical, i18n.getLocale())?.entry;
743
+ };
744
+ fn.meta = (entryId) => new MessageBuilderImpl(i18n, scope, entryId).meta();
745
+ fn.entryIds = () => i18n.getCatalog().entryIds(scope);
746
+ return fn;
747
+ }
748
+
749
+ // src/catalog.ts
750
+ var import_core3 = require("@cliff-format/core");
751
+ function normalizeLocale(tag) {
752
+ return tag.toLowerCase();
753
+ }
754
+ function localeVariants(tag) {
755
+ const parts = tag.split("-").filter((part) => part.length > 0);
756
+ const variants = [];
757
+ for (let length = parts.length; length >= 1; length--) {
758
+ variants.push(parts.slice(0, length).join("-"));
759
+ }
760
+ return variants;
761
+ }
762
+ function addSuffix(map, suffix, canonical) {
763
+ if (suffix.length === 0) return;
764
+ const existing = map.get(suffix);
765
+ if (existing === void 0) {
766
+ map.set(suffix, /* @__PURE__ */ new Set([canonical]));
767
+ } else {
768
+ existing.add(canonical);
769
+ }
770
+ }
771
+ function recordFrom(document, group, entry) {
772
+ const namespace = document.header.namespace;
773
+ const clan = document.header.clan;
774
+ const groupPath = group.path;
775
+ const canonical = (0, import_core3.canonicalId)(namespace, clan, groupPath, entry.id);
776
+ const type = (0, import_core3.effectiveType)(entry, group);
777
+ const context = (0, import_core3.effectiveContext)(entry, group);
778
+ const maxWidth = (0, import_core3.effectiveMaxWidth)(entry, group);
779
+ return {
780
+ canonical,
781
+ namespace,
782
+ clan,
783
+ group: groupPath,
784
+ id: entry.id,
785
+ locale: document.header.targetLanguage,
786
+ sourceLanguage: document.header.sourceLanguage,
787
+ document,
788
+ groupRef: group,
789
+ entry,
790
+ variant: document.header.variant,
791
+ source: entry.source ?? "",
792
+ ...entry.target === void 0 ? {} : { target: entry.target },
793
+ ...type === void 0 ? {} : { type },
794
+ emotion: (0, import_core3.effectiveEmotion)(entry, group),
795
+ ...entry.status === void 0 ? {} : { status: entry.status },
796
+ ...context === void 0 ? {} : { context },
797
+ ...maxWidth === void 0 ? {} : { maxWidth },
798
+ reference: [...entry.reference],
799
+ ...entry.reviewer === void 0 ? {} : { reviewer: entry.reviewer },
800
+ extensions: { ...entry.extensions },
801
+ ...document.path === void 0 ? {} : { filePath: document.path }
802
+ };
803
+ }
804
+ var Catalog = class {
805
+ defaultGroup;
806
+ aliases;
807
+ documentsList = [];
808
+ byLocale = /* @__PURE__ */ new Map();
809
+ byCanonical = /* @__PURE__ */ new Map();
810
+ suffixMap = /* @__PURE__ */ new Map();
811
+ namespaceSet = /* @__PURE__ */ new Set();
812
+ clanSet = /* @__PURE__ */ new Set();
813
+ groupSet = /* @__PURE__ */ new Set();
814
+ localeSet = /* @__PURE__ */ new Set();
815
+ sourceLanguageSet = /* @__PURE__ */ new Set();
816
+ constructor(options = {}) {
817
+ this.defaultGroup = options.defaultGroup ?? "general";
818
+ this.aliases = options.aliases ?? {};
819
+ }
820
+ get knownNamespaces() {
821
+ return this.namespaceSet;
822
+ }
823
+ get knownClans() {
824
+ return this.clanSet;
825
+ }
826
+ get documents() {
827
+ return this.documentsList;
828
+ }
829
+ get sourceLanguages() {
830
+ return [...this.sourceLanguageSet];
831
+ }
832
+ addDocument(document) {
833
+ const locale = document.header.targetLanguage;
834
+ const localeTag = normalizeLocale(locale);
835
+ let localeMap = this.byLocale.get(localeTag);
836
+ if (localeMap === void 0) {
837
+ localeMap = /* @__PURE__ */ new Map();
838
+ this.byLocale.set(localeTag, localeMap);
839
+ }
840
+ const added = [];
841
+ for (const group of document.groups) {
842
+ for (const entry of group.entries) {
843
+ const record = recordFrom(document, group, entry);
844
+ if (localeMap.has(record.canonical)) {
845
+ throw new CliffDuplicateEntryError(record.canonical, locale);
846
+ }
847
+ this.indexRecord(record, localeMap);
848
+ added.push(record);
849
+ }
850
+ }
851
+ this.documentsList.push(document);
852
+ this.namespaceSet.add(document.header.namespace);
853
+ this.clanSet.add(document.header.clan);
854
+ this.localeSet.add(locale);
855
+ if (document.header.sourceLanguage.length > 0) {
856
+ this.sourceLanguageSet.add(document.header.sourceLanguage);
857
+ }
858
+ return added;
859
+ }
860
+ addDraft(parts, draft) {
861
+ const document = this.ensureDocument(parts);
862
+ let group = document.groups.find((candidate) => candidate.path === parts.group);
863
+ if (group === void 0) {
864
+ group = {
865
+ path: parts.group,
866
+ line: 0,
867
+ emotion: [],
868
+ entries: [],
869
+ extensions: (0, import_core3.createExtensions)()
870
+ };
871
+ document.groups.push(group);
872
+ this.groupSet.add(parts.group);
873
+ }
874
+ const entry = {
875
+ id: parts.id,
876
+ line: 0,
877
+ emotion: draft.emotion ? [...draft.emotion] : [],
878
+ reference: draft.reference ? [...draft.reference] : [],
879
+ extensions: draft.extensions ? { ...draft.extensions } : (0, import_core3.createExtensions)()
880
+ };
881
+ if (draft.source !== void 0) entry.source = draft.source;
882
+ if (draft.target !== void 0) entry.target = draft.target;
883
+ if (draft.type !== void 0) entry.type = draft.type;
884
+ if (draft.status !== void 0) entry.status = draft.status;
885
+ if (draft.context !== void 0) entry.context = draft.context;
886
+ if (draft.maxWidth !== void 0) entry.maxWidth = draft.maxWidth;
887
+ if (draft.reviewer !== void 0) entry.reviewer = draft.reviewer;
888
+ const canonical = (0, import_core3.canonicalId)(parts.namespace, parts.clan, parts.group, parts.id);
889
+ const localeMap = this.byLocale.get(normalizeLocale(parts.targetLanguage));
890
+ if (localeMap?.has(canonical) === true) {
891
+ throw new CliffDuplicateEntryError(canonical, parts.targetLanguage);
892
+ }
893
+ group.entries.push(entry);
894
+ const record = recordFrom(document, group, entry);
895
+ if (localeMap === void 0) {
896
+ const created = /* @__PURE__ */ new Map();
897
+ this.byLocale.set(normalizeLocale(parts.targetLanguage), created);
898
+ this.indexRecord(record, created);
899
+ } else {
900
+ this.indexRecord(record, localeMap);
901
+ }
902
+ this.localeSet.add(parts.targetLanguage);
903
+ this.sourceLanguageSet.add(parts.sourceLanguage);
904
+ return record;
905
+ }
906
+ ensureDocument(parts) {
907
+ const existing = this.documentsList.find(
908
+ (document2) => document2.header.namespace === parts.namespace && document2.header.clan === parts.clan && normalizeLocale(document2.header.targetLanguage) === normalizeLocale(parts.targetLanguage)
909
+ );
910
+ if (existing !== void 0) return existing;
911
+ const document = (0, import_core3.createDocument)("1.1");
912
+ document.header.namespace = parts.namespace;
913
+ document.header.clan = parts.clan;
914
+ document.header.sourceLanguage = parts.sourceLanguage;
915
+ document.header.targetLanguage = parts.targetLanguage;
916
+ document.header.variant = "standard";
917
+ this.documentsList.push(document);
918
+ this.namespaceSet.add(parts.namespace);
919
+ this.clanSet.add(parts.clan);
920
+ return document;
921
+ }
922
+ indexRecord(record, localeMap) {
923
+ localeMap.set(record.canonical, record);
924
+ const existing = this.byCanonical.get(record.canonical);
925
+ if (existing === void 0) {
926
+ this.byCanonical.set(record.canonical, [record]);
927
+ } else {
928
+ existing.push(record);
929
+ }
930
+ this.namespaceSet.add(record.namespace);
931
+ this.clanSet.add(record.clan);
932
+ this.groupSet.add(record.group);
933
+ addSuffix(this.suffixMap, `${record.clan}.${record.group}.${record.id}`, record.canonical);
934
+ addSuffix(this.suffixMap, `${record.group}.${record.id}`, record.canonical);
935
+ const lastGroup = record.group.split(".").at(-1);
936
+ if (lastGroup !== void 0) {
937
+ addSuffix(this.suffixMap, `${lastGroup}.${record.id}`, record.canonical);
938
+ }
939
+ addSuffix(this.suffixMap, record.id, record.canonical);
940
+ }
941
+ hasCanonical(canonical) {
942
+ return this.byCanonical.has(canonical);
943
+ }
944
+ recordForCanonical(canonical) {
945
+ return this.byCanonical.get(canonical)?.[0];
946
+ }
947
+ recordsForCanonical(canonical) {
948
+ return this.byCanonical.get(canonical) ?? [];
949
+ }
950
+ suffixMatches(suffix) {
951
+ return [...this.suffixMap.get(suffix) ?? []];
952
+ }
953
+ record(canonical, locale) {
954
+ return this.byLocale.get(normalizeLocale(locale))?.get(canonical);
955
+ }
956
+ selectRecord(canonical, localeChain, sourceLanguage) {
957
+ const records = this.byCanonical.get(canonical);
958
+ if (records === void 0) return void 0;
959
+ for (const tag of localeChain) {
960
+ const found = records.find(
961
+ (record) => normalizeLocale(record.locale) === tag && record.target !== void 0
962
+ );
963
+ if (found !== void 0) return found;
964
+ }
965
+ for (const tag of localeChain) {
966
+ const found = records.find((record) => normalizeLocale(record.locale) === tag);
967
+ if (found !== void 0) return found;
968
+ }
969
+ if (sourceLanguage !== void 0) {
970
+ const found = records.find(
971
+ (record) => normalizeLocale(record.locale) === normalizeLocale(sourceLanguage)
972
+ );
973
+ if (found !== void 0) return found;
974
+ }
975
+ return records[0];
976
+ }
977
+ localeChain(locale, fallbacks = [], sourceLanguage) {
978
+ const tags = [];
979
+ const add = (tag) => {
980
+ for (const variant of localeVariants(tag)) {
981
+ const normalized = normalizeLocale(variant);
982
+ if (normalized.length > 0 && !tags.includes(normalized)) tags.push(normalized);
983
+ }
984
+ };
985
+ add(locale);
986
+ for (const fallback of fallbacks) add(fallback);
987
+ if (sourceLanguage !== void 0) add(sourceLanguage);
988
+ return tags;
989
+ }
990
+ namespaces() {
991
+ return [...this.namespaceSet].sort();
992
+ }
993
+ clans(namespace) {
994
+ if (namespace === void 0) return [...this.clanSet].sort();
995
+ const clans = /* @__PURE__ */ new Set();
996
+ for (const records of this.byCanonical.values()) {
997
+ for (const record of records) {
998
+ if (record.namespace === namespace) clans.add(record.clan);
999
+ }
1000
+ }
1001
+ return [...clans].sort();
1002
+ }
1003
+ groups(scope = {}) {
1004
+ const groups = /* @__PURE__ */ new Set();
1005
+ for (const records of this.byCanonical.values()) {
1006
+ for (const record of records) {
1007
+ if (scope.namespace !== void 0 && record.namespace !== scope.namespace) continue;
1008
+ if (scope.clan !== void 0 && record.clan !== scope.clan) continue;
1009
+ if (scope.group !== void 0 && scope.group.length > 0 && !(record.group === scope.group || record.group.startsWith(`${scope.group}.`))) {
1010
+ continue;
1011
+ }
1012
+ groups.add(record.group);
1013
+ }
1014
+ }
1015
+ return [...groups].sort();
1016
+ }
1017
+ entryIds(scope = {}) {
1018
+ const entryIds = /* @__PURE__ */ new Set();
1019
+ for (const records of this.byCanonical.values()) {
1020
+ for (const record of records) {
1021
+ if (scope.namespace !== void 0 && record.namespace !== scope.namespace) continue;
1022
+ if (scope.clan !== void 0 && record.clan !== scope.clan) continue;
1023
+ if (scope.group !== void 0 && scope.group.length > 0 && !(record.group === scope.group || record.group.startsWith(`${scope.group}.`))) {
1024
+ continue;
1025
+ }
1026
+ entryIds.add(record.canonical);
1027
+ }
1028
+ }
1029
+ return [...entryIds].sort();
1030
+ }
1031
+ allRecords() {
1032
+ const records = [];
1033
+ for (const group of this.byCanonical.values()) records.push(...group);
1034
+ return records;
1035
+ }
1036
+ query(options = {}) {
1037
+ const statuses = options.status === void 0 ? void 0 : new Set(Array.isArray(options.status) ? options.status : [options.status]);
1038
+ return this.allRecords().filter((record) => {
1039
+ if (options.namespace !== void 0 && record.namespace !== options.namespace) return false;
1040
+ if (options.clan !== void 0 && record.clan !== options.clan) return false;
1041
+ if (options.group !== void 0 && options.group.length > 0 && !(record.group === options.group || record.group.startsWith(`${options.group}.`))) {
1042
+ return false;
1043
+ }
1044
+ if (options.locale !== void 0 && options.locale !== "all" && normalizeLocale(record.locale) !== normalizeLocale(options.locale)) {
1045
+ return false;
1046
+ }
1047
+ if (statuses !== void 0 && !statuses.has(statusOf(record.entry))) return false;
1048
+ if (options.target === "present" && record.target === void 0) return false;
1049
+ if (options.target === "missing" && record.target !== void 0) return false;
1050
+ return true;
1051
+ });
1052
+ }
1053
+ untranslated(options = {}) {
1054
+ return this.query(options).filter(
1055
+ (record) => record.target === void 0 || statusOf(record.entry) === "initial"
1056
+ );
1057
+ }
1058
+ byStatus(status, options = {}) {
1059
+ return this.query({ ...options, status });
1060
+ }
1061
+ statusCounts(options = {}) {
1062
+ const counts = {
1063
+ initial: 0,
1064
+ translated: 0,
1065
+ reviewed: 0,
1066
+ final: 0
1067
+ };
1068
+ for (const record of this.query(options)) counts[statusOf(record.entry)]++;
1069
+ return counts;
1070
+ }
1071
+ locales() {
1072
+ return [...this.localeSet].sort();
1073
+ }
1074
+ findGroup(namespace, clan, groupPath) {
1075
+ for (const document of this.documentsList) {
1076
+ if (document.header.namespace !== namespace || document.header.clan !== clan) continue;
1077
+ const group = document.groups.find((candidate) => candidate.path === groupPath);
1078
+ if (group !== void 0) return group;
1079
+ }
1080
+ return void 0;
1081
+ }
1082
+ files(options = {}) {
1083
+ const files = [];
1084
+ const used = /* @__PURE__ */ new Set();
1085
+ const layout = options.layout ?? "folder";
1086
+ for (const document of this.documentsList) {
1087
+ const header = document.header;
1088
+ let filePath = document.path;
1089
+ if (filePath === void 0 || filePath.length === 0) {
1090
+ filePath = layout === "flat" ? `${header.clan}.${header.targetLanguage}.cliff` : `${header.targetLanguage}/${header.clan}.cliff`;
1091
+ }
1092
+ if (used.has(filePath)) {
1093
+ throw new CliffI18nError(`duplicate CLIFF output path "${filePath}"`);
1094
+ }
1095
+ used.add(filePath);
1096
+ files.push({
1097
+ filePath,
1098
+ namespace: header.namespace,
1099
+ clan: header.clan,
1100
+ locale: header.targetLanguage,
1101
+ text: (0, import_core3.serialize)(document),
1102
+ document
1103
+ });
1104
+ }
1105
+ return files;
1106
+ }
1107
+ toCliff(options = {}) {
1108
+ const files = this.files(options);
1109
+ if (files.length === 0) return "";
1110
+ if (files.length > 1) {
1111
+ throw new CliffI18nError(
1112
+ `${files.length} CLIFF documents are loaded; use files() and write each one separately`
1113
+ );
1114
+ }
1115
+ const first = files[0];
1116
+ if (first === void 0) return "";
1117
+ return first.text;
1118
+ }
1119
+ validate() {
1120
+ const issues = [];
1121
+ for (const document of this.documentsList) {
1122
+ issues.push(...(0, import_core3.validateDocument)(document));
1123
+ }
1124
+ return issues;
1125
+ }
1126
+ };
1127
+ function assertCatalogParts(parts) {
1128
+ if (!(0, import_core3.isName)(parts.namespace)) throw new CliffI18nError(`invalid namespace "${parts.namespace}"`);
1129
+ if (!(0, import_core3.isName)(parts.clan)) throw new CliffI18nError(`invalid clan "${parts.clan}"`);
1130
+ if (!(0, import_core3.isName)(parts.id)) throw new CliffI18nError(`invalid entry id "${parts.id}"`);
1131
+ for (const segment of parts.group.split(".")) {
1132
+ if (!(0, import_core3.isName)(segment)) {
1133
+ throw new CliffI18nError(`invalid group path "${parts.group}"`);
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ // src/entry-id.ts
1139
+ var import_core4 = require("@cliff-format/core");
1140
+ function joinCanonical(namespace, clan, group, id) {
1141
+ return [namespace, clan, group, id].filter((part) => part.length > 0).join(".");
1142
+ }
1143
+ function assertScope(scope) {
1144
+ if (!(0, import_core4.isName)(scope.namespace)) {
1145
+ throw new CliffScopeError(
1146
+ `namespace "${scope.namespace}" is not a valid CLIFF name (A-Z a-z 0-9 _ -)`,
1147
+ scope
1148
+ );
1149
+ }
1150
+ if (!(0, import_core4.isName)(scope.clan)) {
1151
+ throw new CliffScopeError(
1152
+ `clan "${scope.clan}" is not a valid CLIFF name (A-Z a-z 0-9 _ -)`,
1153
+ scope
1154
+ );
1155
+ }
1156
+ if (scope.group !== void 0 && scope.group.length > 0) {
1157
+ for (const segment of scope.group.split(".")) {
1158
+ if (!(0, import_core4.isName)(segment)) {
1159
+ throw new CliffScopeError(
1160
+ `group path "${scope.group}" contains invalid segment "${segment}"`,
1161
+ scope
1162
+ );
1163
+ }
1164
+ }
1165
+ }
1166
+ }
1167
+ function applyAlias(raw, aliases) {
1168
+ let current = raw;
1169
+ const seen = /* @__PURE__ */ new Set();
1170
+ while (Object.hasOwn(aliases, current)) {
1171
+ if (seen.has(current)) {
1172
+ throw new CliffScopeError(`alias cycle while resolving "${raw}"`);
1173
+ }
1174
+ seen.add(current);
1175
+ current = aliases[current];
1176
+ }
1177
+ return current;
1178
+ }
1179
+ function parseStringEntryId(raw) {
1180
+ const text = raw.trim();
1181
+ if (text.length === 0) {
1182
+ throw new CliffScopeError("entry ID must not be empty");
1183
+ }
1184
+ const colon = /^([A-Za-z0-9_-]+):(.+)$/.exec(text);
1185
+ const namespace = colon?.[1];
1186
+ const body = colon?.[2] ?? text;
1187
+ if (namespace !== void 0 && !(0, import_core4.isName)(namespace)) {
1188
+ throw new CliffScopeError(`namespace "${namespace}" is not a valid CLIFF name`);
1189
+ }
1190
+ const segments = body.split(/[./]/).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
1191
+ if (segments.length === 0) {
1192
+ throw new CliffScopeError(`entry ID "${raw}" has no path segments`);
1193
+ }
1194
+ for (const segment of segments) {
1195
+ if (!(0, import_core4.isName)(segment)) {
1196
+ throw new CliffScopeError(
1197
+ `entry ID "${raw}" contains invalid segment "${segment}"; CLIFF names use A-Z a-z 0-9 _ -`
1198
+ );
1199
+ }
1200
+ }
1201
+ return namespace === void 0 ? { raw, segments } : { raw, namespace, segments };
1202
+ }
1203
+ function candidateEntryIds(parsed, scope) {
1204
+ const joined = parsed.segments.join(".");
1205
+ const out = [];
1206
+ const add = (value) => {
1207
+ if (value.length > 0 && !out.includes(value)) out.push(value);
1208
+ };
1209
+ if (parsed.namespace !== void 0) {
1210
+ add(`${parsed.namespace}.${joined}`);
1211
+ add(`${parsed.namespace}.${scope.clan}.${joined}`);
1212
+ if (scope.group !== void 0 && scope.group.length > 0) {
1213
+ add(`${parsed.namespace}.${scope.clan}.${scope.group}.${joined}`);
1214
+ }
1215
+ return out;
1216
+ }
1217
+ add(joined);
1218
+ add(`${scope.namespace}.${joined}`);
1219
+ add(`${scope.namespace}.${scope.clan}.${joined}`);
1220
+ if (scope.group !== void 0 && scope.group.length > 0) {
1221
+ add(`${scope.namespace}.${scope.clan}.${scope.group}.${joined}`);
1222
+ }
1223
+ return out;
1224
+ }
1225
+ function partsOfRecord(record) {
1226
+ return {
1227
+ namespace: record.namespace,
1228
+ clan: record.clan,
1229
+ group: record.group,
1230
+ id: record.id,
1231
+ canonical: record.canonical,
1232
+ exists: true
1233
+ };
1234
+ }
1235
+ function groupPrefixLength(group, prefix) {
1236
+ const groupSegments = group.split(".");
1237
+ const prefixSegments = prefix.split(".").filter((segment) => segment.length > 0);
1238
+ let count = 0;
1239
+ while (count < groupSegments.length && count < prefixSegments.length && groupSegments[count] === prefixSegments[count]) {
1240
+ count++;
1241
+ }
1242
+ return count;
1243
+ }
1244
+ function chooseByGroup(records, scope) {
1245
+ const prefix = scope.group ?? "";
1246
+ const sorted = [...records].sort((left, right) => {
1247
+ const leftPrefix = prefix.length > 0 ? groupPrefixLength(left.group, prefix) : 0;
1248
+ const rightPrefix = prefix.length > 0 ? groupPrefixLength(right.group, prefix) : 0;
1249
+ if (leftPrefix !== rightPrefix) return rightPrefix - leftPrefix;
1250
+ const leftSegments = left.group.split(".").length;
1251
+ const rightSegments = right.group.split(".").length;
1252
+ if (leftSegments !== rightSegments) return rightSegments - leftSegments;
1253
+ return left.canonical.localeCompare(right.canonical);
1254
+ });
1255
+ return sorted[0];
1256
+ }
1257
+ function validateGroup(group) {
1258
+ if (group.length === 0) return;
1259
+ for (const segment of group.split(".")) {
1260
+ if (!(0, import_core4.isName)(segment)) {
1261
+ throw new CliffScopeError(`group path "${group}" contains invalid segment "${segment}"`);
1262
+ }
1263
+ }
1264
+ }
1265
+ function parseAuthoringEntryId(parsed, scope, context) {
1266
+ if (parsed.namespace !== void 0 && parsed.namespace !== scope.namespace) {
1267
+ throw new CliffScopeMismatchError(
1268
+ parsed.raw,
1269
+ scope,
1270
+ `${scope.namespace}.${scope.clan}.<group>.<id>`
1271
+ );
1272
+ }
1273
+ let segments = [...parsed.segments];
1274
+ if (segments[0] === scope.namespace) {
1275
+ segments = segments.slice(1);
1276
+ if (segments[0] === scope.clan) {
1277
+ segments = segments.slice(1);
1278
+ } else if (segments[0] !== void 0 && context.knownClans.has(segments[0])) {
1279
+ throw new CliffScopeMismatchError(
1280
+ parsed.raw,
1281
+ scope,
1282
+ `${scope.namespace}.${scope.clan}.<group>.<id>`
1283
+ );
1284
+ }
1285
+ } else if (segments[0] !== void 0 && context.knownNamespaces.has(segments[0]) && segments[0] !== scope.namespace) {
1286
+ throw new CliffScopeMismatchError(
1287
+ parsed.raw,
1288
+ scope,
1289
+ `${scope.namespace}.${scope.clan}.<group>.<id>`
1290
+ );
1291
+ }
1292
+ if (segments[0] === scope.clan) {
1293
+ segments = segments.slice(1);
1294
+ } else if (segments[0] !== void 0 && context.knownClans.has(segments[0]) && segments[0] !== scope.clan) {
1295
+ throw new CliffScopeMismatchError(
1296
+ parsed.raw,
1297
+ scope,
1298
+ `${scope.namespace}.${scope.clan}.<group>.<id>`
1299
+ );
1300
+ }
1301
+ if (segments.length === 0) {
1302
+ throw new CliffScopeError(`entry ID "${parsed.raw}" has no entry id`);
1303
+ }
1304
+ const id = segments[segments.length - 1];
1305
+ if (!(0, import_core4.isName)(id)) {
1306
+ throw new CliffScopeError(`entry id "${id}" is not a valid CLIFF name`);
1307
+ }
1308
+ const groupSegments = segments.slice(0, -1);
1309
+ let group = groupSegments.length > 0 ? groupSegments.join(".") : scope.group && scope.group.length > 0 ? scope.group : context.defaultGroup;
1310
+ if (scope.group !== void 0 && scope.group.length > 0 && groupSegments.length > 0) {
1311
+ group = `${scope.group}.${groupSegments.join(".")}`;
1312
+ }
1313
+ if (group.length === 0) group = context.defaultGroup;
1314
+ validateGroup(group);
1315
+ return {
1316
+ namespace: scope.namespace,
1317
+ clan: scope.clan,
1318
+ group,
1319
+ id,
1320
+ canonical: joinCanonical(scope.namespace, scope.clan, group, id),
1321
+ exists: false
1322
+ };
1323
+ }
1324
+ function resolveObjectEntryId(input, scope, context) {
1325
+ const namespace = input.namespace ?? scope.namespace;
1326
+ const clan = input.clan ?? scope.clan;
1327
+ const group = input.group && input.group.length > 0 ? input.group : scope.group && scope.group.length > 0 ? scope.group : context.defaultGroup;
1328
+ if (namespace !== scope.namespace || clan !== scope.clan) {
1329
+ throw new CliffScopeMismatchError(
1330
+ input.id,
1331
+ scope,
1332
+ `${scope.namespace}.${scope.clan}.<group>.<id>`
1333
+ );
1334
+ }
1335
+ if (!(0, import_core4.isName)(input.id)) {
1336
+ throw new CliffScopeError(`entry id "${input.id}" is not a valid CLIFF name`);
1337
+ }
1338
+ validateGroup(group);
1339
+ const canonical = joinCanonical(namespace, clan, group, input.id);
1340
+ const record = context.recordForCanonical(canonical);
1341
+ if (record !== void 0) return partsOfRecord(record);
1342
+ return {
1343
+ namespace,
1344
+ clan,
1345
+ group,
1346
+ id: input.id,
1347
+ canonical,
1348
+ exists: context.hasCanonical(canonical)
1349
+ };
1350
+ }
1351
+ function resolveEntryId(input, scope, context, strictEntryIds = true) {
1352
+ assertScope(scope);
1353
+ if (typeof input !== "string") {
1354
+ return resolveObjectEntryId(input, scope, context);
1355
+ }
1356
+ const raw = applyAlias(input, context.aliases);
1357
+ const parsed = parseStringEntryId(raw);
1358
+ if (parsed.namespace !== void 0 && parsed.namespace !== scope.namespace) {
1359
+ throw new CliffScopeMismatchError(raw, scope, `${scope.namespace}.${scope.clan}.<group>.<id>`);
1360
+ }
1361
+ const exact = /* @__PURE__ */ new Map();
1362
+ const mismatched = /* @__PURE__ */ new Map();
1363
+ for (const candidate of candidateEntryIds(parsed, scope)) {
1364
+ const record = context.recordForCanonical(candidate);
1365
+ if (record === void 0) continue;
1366
+ if (record.namespace !== scope.namespace || record.clan !== scope.clan) {
1367
+ mismatched.set(record.canonical, record);
1368
+ } else {
1369
+ exact.set(record.canonical, record);
1370
+ }
1371
+ }
1372
+ if (exact.size > 0) {
1373
+ return partsOfRecord([...exact.values()][0]);
1374
+ }
1375
+ if (mismatched.size > 0) {
1376
+ throw new CliffScopeMismatchError(raw, scope, `${scope.namespace}.${scope.clan}.<group>.<id>`);
1377
+ }
1378
+ const suffix = parsed.segments.join(".");
1379
+ const suffixRecords = /* @__PURE__ */ new Map();
1380
+ for (const canonical of context.suffixMatches(suffix)) {
1381
+ const record = context.recordForCanonical(canonical);
1382
+ if (record !== void 0) suffixRecords.set(record.canonical, record);
1383
+ }
1384
+ const inScope = [...suffixRecords.values()].filter(
1385
+ (record) => record.namespace === scope.namespace && record.clan === scope.clan
1386
+ );
1387
+ if (inScope.length === 1) {
1388
+ return partsOfRecord(inScope[0]);
1389
+ }
1390
+ if (inScope.length > 1) {
1391
+ const candidates = inScope.map((record) => record.canonical).sort();
1392
+ if (strictEntryIds) {
1393
+ throw new CliffAmbiguousEntryError(raw, candidates);
1394
+ }
1395
+ const chosen = chooseByGroup(inScope, scope);
1396
+ return {
1397
+ ...partsOfRecord(chosen),
1398
+ ambiguous: true,
1399
+ candidates
1400
+ };
1401
+ }
1402
+ if (suffixRecords.size > 0) {
1403
+ throw new CliffScopeMismatchError(raw, scope, `${scope.namespace}.${scope.clan}.<group>.<id>`);
1404
+ }
1405
+ return parseAuthoringEntryId(parsed, scope, context);
1406
+ }
1407
+
1408
+ // src/engine.ts
1409
+ function isCliffDocument(resource) {
1410
+ return typeof resource === "object" && resource !== null && !(resource instanceof Uint8Array);
1411
+ }
1412
+ var I18nImpl = class {
1413
+ catalog;
1414
+ defaultParams;
1415
+ missing;
1416
+ requireEmotion;
1417
+ enforceMaxWidth;
1418
+ icu;
1419
+ fallbacks;
1420
+ strictEntryIds;
1421
+ parseOptions;
1422
+ onDiagnostic;
1423
+ maxDiagnostics;
1424
+ diagnosticsList = [];
1425
+ listeners = /* @__PURE__ */ new Set();
1426
+ currentLocale;
1427
+ sourceLanguageValue;
1428
+ version = 0;
1429
+ constructor(options, defaultIcu) {
1430
+ if (typeof options.locale !== "string" || options.locale.trim().length === 0) {
1431
+ throw new CliffScopeError("locale is required");
1432
+ }
1433
+ this.currentLocale = options.locale.trim();
1434
+ this.fallbacks = typeof options.fallbackLocale === "string" ? [options.fallbackLocale] : [...options.fallbackLocale ?? []];
1435
+ const catalogOptions = {};
1436
+ if (options.aliases !== void 0) catalogOptions.aliases = options.aliases;
1437
+ if (options.defaultGroup !== void 0) catalogOptions.defaultGroup = options.defaultGroup;
1438
+ this.catalog = new Catalog(catalogOptions);
1439
+ this.defaultParams = options.params ?? {};
1440
+ this.missing = options.missing ?? "entryId";
1441
+ this.requireEmotion = options.requireEmotion ?? false;
1442
+ this.enforceMaxWidth = options.enforceMaxWidth ?? "warn";
1443
+ this.icu = options.icu ?? defaultIcu;
1444
+ this.strictEntryIds = options.strictEntryIds ?? true;
1445
+ this.parseOptions = options.parse ?? {};
1446
+ this.onDiagnostic = options.onDiagnostic;
1447
+ this.maxDiagnostics = options.maxDiagnostics ?? 1e3;
1448
+ this.sourceLanguageValue = options.sourceLanguage;
1449
+ for (const resource of options.resources ?? []) {
1450
+ this.add(resource);
1451
+ }
1452
+ if (this.sourceLanguageValue === void 0 && this.catalog.sourceLanguages.length > 0) {
1453
+ this.sourceLanguageValue = this.catalog.sourceLanguages[0];
1454
+ }
1455
+ }
1456
+ get locale() {
1457
+ return this.currentLocale;
1458
+ }
1459
+ get language() {
1460
+ return this.currentLocale;
1461
+ }
1462
+ get sourceLanguage() {
1463
+ return this.sourceLanguageValue;
1464
+ }
1465
+ getVersion() {
1466
+ return this.version;
1467
+ }
1468
+ subscribe(listener) {
1469
+ this.listeners.add(listener);
1470
+ return () => {
1471
+ this.listeners.delete(listener);
1472
+ };
1473
+ }
1474
+ notify() {
1475
+ this.version++;
1476
+ for (const listener of this.listeners) listener();
1477
+ }
1478
+ setLocale(locale) {
1479
+ const next = locale.trim();
1480
+ if (next.length === 0) throw new CliffScopeError("locale must not be empty");
1481
+ if (next === this.currentLocale) return;
1482
+ this.currentLocale = next;
1483
+ this.notify();
1484
+ }
1485
+ changeLanguage(locale) {
1486
+ this.setLocale(locale);
1487
+ }
1488
+ scope(scope) {
1489
+ assertScope(scope);
1490
+ return { t: createT(this, scope), scope };
1491
+ }
1492
+ add(resource, options = {}) {
1493
+ let document;
1494
+ let issues;
1495
+ let corrections = [];
1496
+ if (isCliffDocument(resource)) {
1497
+ document = resource;
1498
+ issues = (0, import_core5.validateDocument)(document);
1499
+ } else {
1500
+ const parseOptions = {
1501
+ ...this.parseOptions,
1502
+ ...options.sourcePath === void 0 ? {} : { path: options.sourcePath }
1503
+ };
1504
+ if (options.tolerant === true) {
1505
+ const result = (0, import_core5.parseTolerant)(resource, parseOptions);
1506
+ document = result.document;
1507
+ corrections = result.corrections;
1508
+ issues = (0, import_core5.validateDocument)(document);
1509
+ } else {
1510
+ document = (0, import_core5.parse)(resource, parseOptions);
1511
+ issues = (0, import_core5.validateDocument)(document);
1512
+ }
1513
+ }
1514
+ if (options.validate !== false && (0, import_core5.hasErrors)(issues)) {
1515
+ throw new import_core5.CliffValidationError(issues, corrections);
1516
+ }
1517
+ const added = this.catalog.addDocument(document);
1518
+ if (this.sourceLanguageValue === void 0) {
1519
+ this.sourceLanguageValue = document.header.sourceLanguage;
1520
+ }
1521
+ this.notify();
1522
+ void added;
1523
+ return { documents: [document], issues, corrections };
1524
+ }
1525
+ addDocument(document, options = {}) {
1526
+ return this.add(document, options);
1527
+ }
1528
+ resolve(entryId, scope) {
1529
+ const resolved = resolveEntryId(entryId, scope, this.catalog, this.strictEntryIds);
1530
+ if (resolved.ambiguous === true) {
1531
+ this.diagnose({
1532
+ code: "ambiguous-entry",
1533
+ severity: "warning",
1534
+ message: `ambiguous entry "${typeof entryId === "string" ? entryId : entryId.id}"; chose "${resolved.canonical}"`,
1535
+ entryId: resolved.canonical,
1536
+ locale: this.currentLocale,
1537
+ namespace: resolved.namespace,
1538
+ clan: resolved.clan,
1539
+ group: resolved.group,
1540
+ ...resolved.candidates === void 0 ? {} : { details: { candidates: resolved.candidates } }
1541
+ });
1542
+ }
1543
+ return resolved;
1544
+ }
1545
+ recordFor(canonical, locale) {
1546
+ const chain = this.catalog.localeChain(locale, this.fallbacks, this.sourceLanguageValue);
1547
+ return this.catalog.selectRecord(canonical, chain, this.sourceLanguageValue);
1548
+ }
1549
+ has(entryId, scope) {
1550
+ const resolved = this.resolve(entryId, scope);
1551
+ return resolved.exists && this.recordFor(resolved.canonical, this.currentLocale) !== void 0;
1552
+ }
1553
+ entryIds(scope = {}) {
1554
+ return this.catalog.entryIds(scope);
1555
+ }
1556
+ query(options = {}) {
1557
+ return this.catalog.query(options);
1558
+ }
1559
+ untranslated(options = {}) {
1560
+ return this.catalog.untranslated(options);
1561
+ }
1562
+ byStatus(status, options = {}) {
1563
+ return this.catalog.byStatus(status, options);
1564
+ }
1565
+ statusCounts(options = {}) {
1566
+ return this.catalog.statusCounts(options);
1567
+ }
1568
+ namespaces() {
1569
+ return this.catalog.namespaces();
1570
+ }
1571
+ clans(namespace) {
1572
+ return this.catalog.clans(namespace);
1573
+ }
1574
+ groups(scope = {}) {
1575
+ return this.catalog.groups(scope);
1576
+ }
1577
+ locales() {
1578
+ return this.catalog.locales();
1579
+ }
1580
+ files(options = {}) {
1581
+ return this.catalog.files(options);
1582
+ }
1583
+ toCliff(options = {}) {
1584
+ return this.catalog.toCliff(options);
1585
+ }
1586
+ validate() {
1587
+ return this.catalog.validate();
1588
+ }
1589
+ diagnostics() {
1590
+ return this.diagnosticsList;
1591
+ }
1592
+ clearDiagnostics() {
1593
+ this.diagnosticsList.length = 0;
1594
+ }
1595
+ diagnose(diagnostic) {
1596
+ this.diagnosticsList.push(diagnostic);
1597
+ if (this.diagnosticsList.length > this.maxDiagnostics) {
1598
+ this.diagnosticsList.splice(0, this.diagnosticsList.length - this.maxDiagnostics);
1599
+ }
1600
+ if (this.onDiagnostic !== void 0) {
1601
+ try {
1602
+ this.onDiagnostic(diagnostic);
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ }
1607
+ getLocale() {
1608
+ return this.currentLocale;
1609
+ }
1610
+ getSourceLanguage() {
1611
+ if (this.sourceLanguageValue === void 0) {
1612
+ throw new CliffI18nError(
1613
+ "CLIFF source-language is not set; pass sourceLanguage or load a CLIFF document"
1614
+ );
1615
+ }
1616
+ return this.sourceLanguageValue;
1617
+ }
1618
+ getCatalog() {
1619
+ return this.catalog;
1620
+ }
1621
+ format(message, params = {}, options = {}) {
1622
+ const locale = options.locale ?? this.currentLocale;
1623
+ const icuOptions = {};
1624
+ if (options.currency !== void 0) icuOptions.currency = options.currency;
1625
+ if (options.timeZone !== void 0) icuOptions.timeZone = options.timeZone;
1626
+ try {
1627
+ const result = this.icu.format(message, locale, params, icuOptions);
1628
+ for (const error of result.errors) {
1629
+ this.diagnose({
1630
+ code: "icu-error",
1631
+ severity: "warning",
1632
+ message: `${error.type}: ${error.message}`,
1633
+ locale,
1634
+ ...error.source === void 0 ? {} : { details: { source: error.source } }
1635
+ });
1636
+ }
1637
+ return result.text;
1638
+ } catch (error) {
1639
+ this.diagnose({
1640
+ code: "icu-error",
1641
+ severity: "error",
1642
+ message: String(error),
1643
+ locale
1644
+ });
1645
+ return message;
1646
+ }
1647
+ }
1648
+ formatToParts(message, params = {}, options = {}) {
1649
+ const locale = options.locale ?? this.currentLocale;
1650
+ const icuOptions = {};
1651
+ if (options.currency !== void 0) icuOptions.currency = options.currency;
1652
+ if (options.timeZone !== void 0) icuOptions.timeZone = options.timeZone;
1653
+ try {
1654
+ const result = this.icu.formatToParts(message, locale, params, icuOptions);
1655
+ for (const error of result.errors) {
1656
+ this.diagnose({
1657
+ code: "icu-error",
1658
+ severity: "warning",
1659
+ message: `${error.type}: ${error.message}`,
1660
+ locale,
1661
+ ...error.source === void 0 ? {} : { details: { source: error.source } }
1662
+ });
1663
+ }
1664
+ return result.parts;
1665
+ } catch (error) {
1666
+ this.diagnose({
1667
+ code: "icu-error",
1668
+ severity: "error",
1669
+ message: String(error),
1670
+ locale
1671
+ });
1672
+ return [{ type: "literal", value: message }];
1673
+ }
1674
+ }
1675
+ };
1676
+ function createI18nEngine(options, defaultIcu) {
1677
+ return new I18nImpl(options, defaultIcu);
1678
+ }
1679
+
1680
+ // src/icu/detect.ts
1681
+ var MF2_DIRECTIVE_RE = /(^|[\s.])\.(input|local|match)\b/;
1682
+ var MF2_VARIABLE_RE = /\{\s*\$[A-Za-z_]/;
1683
+ var MF2_MARKUP_RE = /\{[#/][A-Za-z]/;
1684
+ var MF1_TYPE_RE = /\{\s*[A-Za-z_][A-Za-z0-9_-]*\s*,\s*(plural|select|selectordinal|number|date|time)\b/;
1685
+ function detectDialect(message) {
1686
+ if (MF2_DIRECTIVE_RE.test(message) || MF2_VARIABLE_RE.test(message) || MF2_MARKUP_RE.test(message)) {
1687
+ return "mf2";
1688
+ }
1689
+ if (MF1_TYPE_RE.test(message)) return "mf1";
1690
+ if (message.includes("{{") || message.includes("}}")) return "mf2";
1691
+ if (message.includes("{") || message.includes("}")) return "mf1";
1692
+ return "plain";
1693
+ }
1694
+ function argumentNames(message, dialect) {
1695
+ const names = [];
1696
+ const seen = /* @__PURE__ */ new Set();
1697
+ const add = (name) => {
1698
+ if (!seen.has(name)) {
1699
+ seen.add(name);
1700
+ names.push(name);
1701
+ }
1702
+ };
1703
+ if (dialect === "mf2") {
1704
+ for (const match of message.matchAll(/\{\s*\$([A-Za-z_][A-Za-z0-9_-]*)/g)) {
1705
+ if (match[1] !== void 0) add(match[1]);
1706
+ }
1707
+ return names;
1708
+ }
1709
+ for (const match of message.matchAll(/\{\s*([A-Za-z_][A-Za-z0-9_-]*)\s*[,}]/g)) {
1710
+ if (match[1] !== void 0) add(match[1]);
1711
+ }
1712
+ return names;
1713
+ }
1714
+ function analyzePlain(message, dialect, errors = []) {
1715
+ return { dialect, arguments: argumentNames(message, dialect), errors };
1716
+ }
1717
+
1718
+ // src/icu/lite.ts
1719
+ var SIMPLE_MESSAGE = /^[^{}]*(?:\{[A-Za-z_][A-Za-z0-9_]*\}[^{}]*)*$/;
1720
+ var SIMPLE_ARGUMENT = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
1721
+ function unsupported(message) {
1722
+ return {
1723
+ type: "icu-unsupported",
1724
+ message: "the lite ICU runtime does not format plural/select/number/date messages; use @cliff-format/i18n instead",
1725
+ source: message
1726
+ };
1727
+ }
1728
+ function simpleParts(message, values) {
1729
+ const parts = [];
1730
+ let cursor = 0;
1731
+ for (const match of message.matchAll(SIMPLE_ARGUMENT)) {
1732
+ const index = match.index ?? 0;
1733
+ if (index > cursor) parts.push({ type: "literal", value: message.slice(cursor, index) });
1734
+ const name = match[1];
1735
+ const value = values[name];
1736
+ parts.push({
1737
+ type: "expression",
1738
+ source: `{${name}}`,
1739
+ value: value === void 0 ? `{${name}}` : String(value)
1740
+ });
1741
+ cursor = index + match[0].length;
1742
+ }
1743
+ if (cursor < message.length) parts.push({ type: "literal", value: message.slice(cursor) });
1744
+ if (parts.length === 0 && message.length > 0) parts.push({ type: "literal", value: message });
1745
+ return parts;
1746
+ }
1747
+ function createLiteIcu() {
1748
+ return {
1749
+ format(message, _locale, values) {
1750
+ const dialect = detectDialect(message);
1751
+ if (dialect === "plain") return { text: message, errors: [] };
1752
+ if (!SIMPLE_MESSAGE.test(message)) {
1753
+ return { text: message, errors: [unsupported(message)] };
1754
+ }
1755
+ const errors = [];
1756
+ const text = message.replace(SIMPLE_ARGUMENT, (whole, name) => {
1757
+ const value = values[name];
1758
+ if (value === void 0) {
1759
+ errors.push({
1760
+ type: "unresolved-var",
1761
+ message: `missing ICU argument "${name}"`,
1762
+ source: whole
1763
+ });
1764
+ return whole;
1765
+ }
1766
+ return String(value);
1767
+ });
1768
+ return { text, errors };
1769
+ },
1770
+ formatToParts(message, _locale, values) {
1771
+ const dialect = detectDialect(message);
1772
+ if (dialect === "plain") {
1773
+ return { parts: [{ type: "literal", value: message }], errors: [] };
1774
+ }
1775
+ if (!SIMPLE_MESSAGE.test(message)) {
1776
+ return {
1777
+ parts: [{ type: "literal", value: message }],
1778
+ errors: [unsupported(message)]
1779
+ };
1780
+ }
1781
+ return { parts: simpleParts(message, values), errors: [] };
1782
+ },
1783
+ analyze(message) {
1784
+ const dialect = detectDialect(message);
1785
+ const errors = [];
1786
+ if (dialect !== "plain" && !SIMPLE_MESSAGE.test(message)) {
1787
+ errors.push(unsupported(message));
1788
+ }
1789
+ return analyzePlain(message, dialect, errors);
1790
+ }
1791
+ };
1792
+ }
1793
+
1794
+ // src/create-lite.ts
1795
+ function createI18n(options) {
1796
+ return createI18nEngine(options, options.icu ?? createLiteIcu());
1797
+ }
1798
+ //# sourceMappingURL=lite.cjs.map