intor-translator 1.2.2 → 1.2.4

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/index.cjs CHANGED
@@ -4,7 +4,7 @@ var rura = require('rura');
4
4
 
5
5
  // src/translators/core-translator/core-translator.ts
6
6
 
7
- // src/translators/shared/utils/find-message-in-locales.ts
7
+ // src/shared/utils/find-message-in-locales.ts
8
8
  var findMessageInLocales = ({
9
9
  messages,
10
10
  candidateLocales,
@@ -43,13 +43,13 @@ var findMessage = rura.rura.createHook(
43
43
  // src/pipeline/utils/make-handler-context.ts
44
44
  function makeHandlerContext(ctx) {
45
45
  return Object.freeze({
46
+ config: ctx.config,
47
+ messages: ctx.messages,
46
48
  locale: ctx.locale,
49
+ isLoading: ctx.isLoading,
47
50
  key: ctx.key,
48
51
  replacements: ctx.replacements,
49
- messages: ctx.messages,
50
52
  candidateLocales: ctx.candidateLocales,
51
- config: ctx.config,
52
- isLoading: ctx.isLoading,
53
53
  rawMessage: ctx.rawMessage,
54
54
  formattedMessage: ctx.formattedMessage,
55
55
  meta: ctx.meta
@@ -70,7 +70,7 @@ var format = rura.rura.createHook(
70
70
  500
71
71
  );
72
72
 
73
- // src/translators/shared/utils/replace-values.ts
73
+ // src/pipeline/hooks/interpolate/replace-values.ts
74
74
  var replaceValues = (message, params) => {
75
75
  if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
76
76
  return message;
@@ -89,7 +89,7 @@ var replaceValues = (message, params) => {
89
89
  return replaced;
90
90
  };
91
91
 
92
- // src/pipeline/hooks/interpolate.ts
92
+ // src/pipeline/hooks/interpolate/interpolate.ts
93
93
  var interpolate = rura.rura.createHook(
94
94
  "interpolate",
95
95
  (ctx) => {
@@ -143,7 +143,7 @@ var missing = rura.rura.createHook(
143
143
  400
144
144
  );
145
145
 
146
- // src/translators/shared/utils/resolve-candidate-locales.ts
146
+ // src/shared/utils/resolve-candidate-locales.ts
147
147
  var resolveCandidateLocales = (locale, fallbackLocalesMap) => {
148
148
  const fallbacks = fallbackLocalesMap?.[locale] || [];
149
149
  const filteredFallbacks = fallbacks.filter((l) => l !== locale);
@@ -220,7 +220,7 @@ var BaseTranslator = class {
220
220
  }
221
221
  };
222
222
 
223
- // src/translators/shared/has-key.ts
223
+ // src/translators/methods/has-key.ts
224
224
  var hasKey = ({
225
225
  messages,
226
226
  locale,
@@ -228,11 +228,7 @@ var hasKey = ({
228
228
  targetLocale
229
229
  }) => {
230
230
  const candidateLocales = resolveCandidateLocales(targetLocale || locale);
231
- const message = findMessageInLocales({
232
- messages,
233
- candidateLocales,
234
- key
235
- });
231
+ const message = findMessageInLocales({ messages, candidateLocales, key });
236
232
  return !!message;
237
233
  };
238
234
  function translate(options) {
@@ -344,4 +340,131 @@ var ScopeTranslator = class extends CoreTranslator {
344
340
  }
345
341
  };
346
342
 
343
+ // src/message/tokenize/parse-attributes.ts
344
+ var ATTR_REGEX = /\s+([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"/g;
345
+ var parseAttributes = (input) => {
346
+ const attributes = {};
347
+ let match;
348
+ let consumed = "";
349
+ while (match = ATTR_REGEX.exec(input)) {
350
+ const [, key, value] = match;
351
+ attributes[key] = value;
352
+ consumed += match[0];
353
+ }
354
+ if (consumed.length !== input.length) {
355
+ return null;
356
+ }
357
+ return attributes;
358
+ };
359
+
360
+ // src/message/tokenize/tokenize.ts
361
+ var OPEN_TAG_REGEX = /^<([a-zA-Z0-9_]+)([^>]*)>/;
362
+ var CLOSE_TAG_REGEX = /^<\/([a-zA-Z0-9_]+)>/;
363
+ var tokenize = (message) => {
364
+ const tokens = [];
365
+ let pos = 0;
366
+ let buffer = "";
367
+ const flushText = () => {
368
+ if (!buffer) return;
369
+ tokens.push({
370
+ type: "text",
371
+ value: buffer,
372
+ position: pos - buffer.length
373
+ });
374
+ buffer = "";
375
+ };
376
+ while (pos < message.length) {
377
+ const char = message[pos];
378
+ if (char === "<") {
379
+ const openMatch = message.slice(pos).match(OPEN_TAG_REGEX);
380
+ if (openMatch) {
381
+ const [, name, rawAttributes] = openMatch;
382
+ const attributes = parseAttributes(rawAttributes);
383
+ if (attributes) {
384
+ flushText();
385
+ tokens.push({
386
+ type: "tag-open",
387
+ name,
388
+ attributes,
389
+ position: pos
390
+ });
391
+ pos += openMatch[0].length;
392
+ continue;
393
+ }
394
+ }
395
+ const closeMatch = message.slice(pos).match(CLOSE_TAG_REGEX);
396
+ if (closeMatch) {
397
+ flushText();
398
+ tokens.push({
399
+ type: "tag-close",
400
+ name: closeMatch[1],
401
+ position: pos
402
+ });
403
+ pos += closeMatch[0].length;
404
+ continue;
405
+ }
406
+ }
407
+ buffer += char;
408
+ pos += 1;
409
+ }
410
+ flushText();
411
+ return tokens;
412
+ };
413
+
414
+ // src/message/build-ast/build-ast.ts
415
+ function buildAST(tokens) {
416
+ const root = [];
417
+ const stack = [];
418
+ const pushNode = (node) => {
419
+ const parent = stack.at(-1);
420
+ if (parent) {
421
+ parent.children.push(node);
422
+ } else {
423
+ root.push(node);
424
+ }
425
+ };
426
+ for (const token of tokens) {
427
+ switch (token.type) {
428
+ case "text": {
429
+ pushNode({
430
+ type: "text",
431
+ value: token.value
432
+ });
433
+ break;
434
+ }
435
+ case "tag-open": {
436
+ const node = {
437
+ type: "tag",
438
+ name: token.name,
439
+ attributes: token.attributes,
440
+ children: []
441
+ };
442
+ pushNode(node);
443
+ stack.push(node);
444
+ break;
445
+ }
446
+ case "tag-close": {
447
+ const last = stack.pop();
448
+ if (!last || last.name !== token.name) {
449
+ throw new Error(
450
+ `Unmatched closing tag </${token.name}> at position ${token.position}`
451
+ );
452
+ }
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ if (stack.length > 0) {
458
+ throw new Error(`Unclosed tag detected: <${stack.at(-1)?.name}>`);
459
+ }
460
+ return root;
461
+ }
462
+
463
+ // src/message/parse-rich-message.ts
464
+ function parseRichMessage(message) {
465
+ const tokens = tokenize(message);
466
+ return buildAST(tokens);
467
+ }
468
+
347
469
  exports.Translator = ScopeTranslator;
470
+ exports.parseRichMessage = parseRichMessage;
package/dist/index.d.cts CHANGED
@@ -188,7 +188,7 @@ type LeafKeys<M, D extends number = DefaultDepth> = [D] extends [never] ? never
188
188
  * LocalizedNodeKeys // → string
189
189
  * ```
190
190
  */
191
- type LocalizedNodeKeys<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = [M] extends [undefined] ? string : L extends "union" ? NodeKeys<M[keyof M], D> : NodeKeys<M[Extract<L, keyof M>], D>;
191
+ type LocalizedNodeKeys<M = unknown, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = M extends LocaleMessages ? L extends "union" ? NodeKeys<M[keyof M], D> : NodeKeys<M[Extract<L, keyof M>], D> : string;
192
192
  /**
193
193
  * Extracts all **leaf keys** from the messages
194
194
  * of a specified locale (or union of locales).
@@ -211,7 +211,7 @@ type LocalizedNodeKeys<M extends LocaleMessages | undefined = undefined, L exten
211
211
  * LocalizedLeafKeys // → string
212
212
  * ```
213
213
  */
214
- type LocalizedLeafKeys<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = [M] extends [undefined] ? string : L extends "union" ? LeafKeys<M[keyof M], D> : LeafKeys<M[Extract<L, keyof M>], D>;
214
+ type LocalizedLeafKeys<M = unknown, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = M extends LocaleMessages ? L extends "union" ? LeafKeys<M[keyof M], D> : LeafKeys<M[Extract<L, keyof M>], D> : string;
215
215
 
216
216
  /**
217
217
  * Resolves the type at a dot-separated key in a nested object.
@@ -351,7 +351,7 @@ interface BaseTranslatorOptions<M = unknown> {
351
351
  *
352
352
  * @template M - Shape of the messages object.
353
353
  */
354
- declare class BaseTranslator<M extends LocaleMessages> {
354
+ declare class BaseTranslator<M extends LocaleMessages | unknown = unknown> {
355
355
  /** Current messages for translation */
356
356
  protected _messages: Readonly<M>;
357
357
  /** Current active locale */
@@ -400,7 +400,7 @@ interface TranslatorPlugin {
400
400
  * @template M - Shape of the messages object.
401
401
  * @template L - Locale selection strategy ("union" or specific locale keys).
402
402
  */
403
- declare class CoreTranslator<M extends LocaleMessages, L extends keyof M | "union" = "union"> extends BaseTranslator<M> {
403
+ declare class CoreTranslator<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union"> extends BaseTranslator<M> {
404
404
  /** User-provided options including messages, locale, and config. */
405
405
  protected translateConfig: TranslateConfig<M>;
406
406
  /** Active pipeline hooks applied during translation. */
@@ -419,15 +419,48 @@ declare class CoreTranslator<M extends LocaleMessages, L extends keyof M | "unio
419
419
  }
420
420
 
421
421
  type ScopeTranslatorOptions<M> = CoreTranslatorOptions<M>;
422
- type ScopeTranslatorMethods<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", K = LocalizedLeafKeys<M, L>> = {
422
+ type ScopeTranslatorMethods<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union", K = LocalizedLeafKeys<M, L>> = {
423
423
  hasKey: (key?: K, targetLocale?: Locale<M>) => boolean;
424
424
  t: <Result = string>(key?: K, replacements?: Replacement) => Result;
425
425
  };
426
426
 
427
- declare class ScopeTranslator<M extends LocaleMessages, L extends keyof M | "union" = "union"> extends CoreTranslator<M> {
427
+ declare class ScopeTranslator<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union"> extends CoreTranslator<M> {
428
428
  constructor(options: ScopeTranslatorOptions<M>);
429
429
  /** Create a scoped translator with a prefix key, providing `t` and `hasKey` for nested keys. */
430
430
  scoped<PK extends LocalizedNodeKeys<M, L> | undefined = undefined>(preKey?: PK): PK extends string ? ScopeTranslatorMethods<M, L, ScopedLeafKeys<M, PK, L>> : ScopeTranslatorMethods<M, L>;
431
431
  }
432
432
 
433
- export { type DefaultDepth, type FallbackLocalesMap, type FormatHandler, type HandlerContext, type LeafKeys, type LoadingHandler, type Locale, type LocaleMessages, type LocalizedLeafKeys, type LocalizedMessagesUnion, type LocalizedNodeKeys, type MissingHandler, type NestedMessage, type NodeKeys, type Replacement, type ScopedLeafKeys, type TranslateConfig, type TranslateContext, type TranslateHandlers, type TranslateHook, ScopeTranslator as Translator, type ScopeTranslatorMethods as TranslatorMethods, type ScopeTranslatorOptions as TranslatorOptions, type TranslatorPlugin };
433
+ /** Parsed tag attributes map. */
434
+ type Attributes = Record<string, string>;
435
+
436
+ /** Semantic node produced by the AST builder. */
437
+ type ASTNode = TextNode | TagNode;
438
+ /** Plain text node in the semantic AST. */
439
+ interface TextNode {
440
+ type: "text";
441
+ value: string;
442
+ }
443
+ /** Semantic tag node with attributes and nested children. */
444
+ interface TagNode {
445
+ type: "tag";
446
+ name: string;
447
+ attributes: Attributes;
448
+ children: ASTNode[];
449
+ }
450
+
451
+ /**
452
+ * Parse a rich-formatted message string into a semantic AST.
453
+ *
454
+ * This is the high-level entry point for processing translated messages
455
+ * that contain semantic tags (e.g. <b>, <a>, <i>).
456
+ *
457
+ * Internally, this function:
458
+ * - Tokenizes the input string into semantic tokens
459
+ * - Builds a nested AST from the token stream
460
+ *
461
+ * Consumers should treat the returned AST as a semantic structure
462
+ * suitable for rendering or further processing.
463
+ */
464
+ declare function parseRichMessage(message: string): ASTNode[];
465
+
466
+ export { type DefaultDepth, type FallbackLocalesMap, type FormatHandler, type HandlerContext, type LeafKeys, type LoadingHandler, type Locale, type LocaleMessages, type LocalizedLeafKeys, type LocalizedMessagesUnion, type LocalizedNodeKeys, type MissingHandler, type NestedMessage, type NodeKeys, type Replacement, type ScopedLeafKeys, type TranslateConfig, type TranslateContext, type TranslateHandlers, type TranslateHook, ScopeTranslator as Translator, type ScopeTranslatorMethods as TranslatorMethods, type ScopeTranslatorOptions as TranslatorOptions, type TranslatorPlugin, parseRichMessage };
package/dist/index.d.ts CHANGED
@@ -188,7 +188,7 @@ type LeafKeys<M, D extends number = DefaultDepth> = [D] extends [never] ? never
188
188
  * LocalizedNodeKeys // → string
189
189
  * ```
190
190
  */
191
- type LocalizedNodeKeys<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = [M] extends [undefined] ? string : L extends "union" ? NodeKeys<M[keyof M], D> : NodeKeys<M[Extract<L, keyof M>], D>;
191
+ type LocalizedNodeKeys<M = unknown, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = M extends LocaleMessages ? L extends "union" ? NodeKeys<M[keyof M], D> : NodeKeys<M[Extract<L, keyof M>], D> : string;
192
192
  /**
193
193
  * Extracts all **leaf keys** from the messages
194
194
  * of a specified locale (or union of locales).
@@ -211,7 +211,7 @@ type LocalizedNodeKeys<M extends LocaleMessages | undefined = undefined, L exten
211
211
  * LocalizedLeafKeys // → string
212
212
  * ```
213
213
  */
214
- type LocalizedLeafKeys<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = [M] extends [undefined] ? string : L extends "union" ? LeafKeys<M[keyof M], D> : LeafKeys<M[Extract<L, keyof M>], D>;
214
+ type LocalizedLeafKeys<M = unknown, L extends keyof M | "union" = "union", D extends number = DefaultDepth> = M extends LocaleMessages ? L extends "union" ? LeafKeys<M[keyof M], D> : LeafKeys<M[Extract<L, keyof M>], D> : string;
215
215
 
216
216
  /**
217
217
  * Resolves the type at a dot-separated key in a nested object.
@@ -351,7 +351,7 @@ interface BaseTranslatorOptions<M = unknown> {
351
351
  *
352
352
  * @template M - Shape of the messages object.
353
353
  */
354
- declare class BaseTranslator<M extends LocaleMessages> {
354
+ declare class BaseTranslator<M extends LocaleMessages | unknown = unknown> {
355
355
  /** Current messages for translation */
356
356
  protected _messages: Readonly<M>;
357
357
  /** Current active locale */
@@ -400,7 +400,7 @@ interface TranslatorPlugin {
400
400
  * @template M - Shape of the messages object.
401
401
  * @template L - Locale selection strategy ("union" or specific locale keys).
402
402
  */
403
- declare class CoreTranslator<M extends LocaleMessages, L extends keyof M | "union" = "union"> extends BaseTranslator<M> {
403
+ declare class CoreTranslator<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union"> extends BaseTranslator<M> {
404
404
  /** User-provided options including messages, locale, and config. */
405
405
  protected translateConfig: TranslateConfig<M>;
406
406
  /** Active pipeline hooks applied during translation. */
@@ -419,15 +419,48 @@ declare class CoreTranslator<M extends LocaleMessages, L extends keyof M | "unio
419
419
  }
420
420
 
421
421
  type ScopeTranslatorOptions<M> = CoreTranslatorOptions<M>;
422
- type ScopeTranslatorMethods<M extends LocaleMessages | undefined = undefined, L extends keyof M | "union" = "union", K = LocalizedLeafKeys<M, L>> = {
422
+ type ScopeTranslatorMethods<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union", K = LocalizedLeafKeys<M, L>> = {
423
423
  hasKey: (key?: K, targetLocale?: Locale<M>) => boolean;
424
424
  t: <Result = string>(key?: K, replacements?: Replacement) => Result;
425
425
  };
426
426
 
427
- declare class ScopeTranslator<M extends LocaleMessages, L extends keyof M | "union" = "union"> extends CoreTranslator<M> {
427
+ declare class ScopeTranslator<M extends LocaleMessages | unknown = unknown, L extends keyof M | "union" = "union"> extends CoreTranslator<M> {
428
428
  constructor(options: ScopeTranslatorOptions<M>);
429
429
  /** Create a scoped translator with a prefix key, providing `t` and `hasKey` for nested keys. */
430
430
  scoped<PK extends LocalizedNodeKeys<M, L> | undefined = undefined>(preKey?: PK): PK extends string ? ScopeTranslatorMethods<M, L, ScopedLeafKeys<M, PK, L>> : ScopeTranslatorMethods<M, L>;
431
431
  }
432
432
 
433
- export { type DefaultDepth, type FallbackLocalesMap, type FormatHandler, type HandlerContext, type LeafKeys, type LoadingHandler, type Locale, type LocaleMessages, type LocalizedLeafKeys, type LocalizedMessagesUnion, type LocalizedNodeKeys, type MissingHandler, type NestedMessage, type NodeKeys, type Replacement, type ScopedLeafKeys, type TranslateConfig, type TranslateContext, type TranslateHandlers, type TranslateHook, ScopeTranslator as Translator, type ScopeTranslatorMethods as TranslatorMethods, type ScopeTranslatorOptions as TranslatorOptions, type TranslatorPlugin };
433
+ /** Parsed tag attributes map. */
434
+ type Attributes = Record<string, string>;
435
+
436
+ /** Semantic node produced by the AST builder. */
437
+ type ASTNode = TextNode | TagNode;
438
+ /** Plain text node in the semantic AST. */
439
+ interface TextNode {
440
+ type: "text";
441
+ value: string;
442
+ }
443
+ /** Semantic tag node with attributes and nested children. */
444
+ interface TagNode {
445
+ type: "tag";
446
+ name: string;
447
+ attributes: Attributes;
448
+ children: ASTNode[];
449
+ }
450
+
451
+ /**
452
+ * Parse a rich-formatted message string into a semantic AST.
453
+ *
454
+ * This is the high-level entry point for processing translated messages
455
+ * that contain semantic tags (e.g. <b>, <a>, <i>).
456
+ *
457
+ * Internally, this function:
458
+ * - Tokenizes the input string into semantic tokens
459
+ * - Builds a nested AST from the token stream
460
+ *
461
+ * Consumers should treat the returned AST as a semantic structure
462
+ * suitable for rendering or further processing.
463
+ */
464
+ declare function parseRichMessage(message: string): ASTNode[];
465
+
466
+ export { type DefaultDepth, type FallbackLocalesMap, type FormatHandler, type HandlerContext, type LeafKeys, type LoadingHandler, type Locale, type LocaleMessages, type LocalizedLeafKeys, type LocalizedMessagesUnion, type LocalizedNodeKeys, type MissingHandler, type NestedMessage, type NodeKeys, type Replacement, type ScopedLeafKeys, type TranslateConfig, type TranslateContext, type TranslateHandlers, type TranslateHook, ScopeTranslator as Translator, type ScopeTranslatorMethods as TranslatorMethods, type ScopeTranslatorOptions as TranslatorOptions, type TranslatorPlugin, parseRichMessage };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { rura } from 'rura';
2
2
 
3
3
  // src/translators/core-translator/core-translator.ts
4
4
 
5
- // src/translators/shared/utils/find-message-in-locales.ts
5
+ // src/shared/utils/find-message-in-locales.ts
6
6
  var findMessageInLocales = ({
7
7
  messages,
8
8
  candidateLocales,
@@ -41,13 +41,13 @@ var findMessage = rura.createHook(
41
41
  // src/pipeline/utils/make-handler-context.ts
42
42
  function makeHandlerContext(ctx) {
43
43
  return Object.freeze({
44
+ config: ctx.config,
45
+ messages: ctx.messages,
44
46
  locale: ctx.locale,
47
+ isLoading: ctx.isLoading,
45
48
  key: ctx.key,
46
49
  replacements: ctx.replacements,
47
- messages: ctx.messages,
48
50
  candidateLocales: ctx.candidateLocales,
49
- config: ctx.config,
50
- isLoading: ctx.isLoading,
51
51
  rawMessage: ctx.rawMessage,
52
52
  formattedMessage: ctx.formattedMessage,
53
53
  meta: ctx.meta
@@ -68,7 +68,7 @@ var format = rura.createHook(
68
68
  500
69
69
  );
70
70
 
71
- // src/translators/shared/utils/replace-values.ts
71
+ // src/pipeline/hooks/interpolate/replace-values.ts
72
72
  var replaceValues = (message, params) => {
73
73
  if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
74
74
  return message;
@@ -87,7 +87,7 @@ var replaceValues = (message, params) => {
87
87
  return replaced;
88
88
  };
89
89
 
90
- // src/pipeline/hooks/interpolate.ts
90
+ // src/pipeline/hooks/interpolate/interpolate.ts
91
91
  var interpolate = rura.createHook(
92
92
  "interpolate",
93
93
  (ctx) => {
@@ -141,7 +141,7 @@ var missing = rura.createHook(
141
141
  400
142
142
  );
143
143
 
144
- // src/translators/shared/utils/resolve-candidate-locales.ts
144
+ // src/shared/utils/resolve-candidate-locales.ts
145
145
  var resolveCandidateLocales = (locale, fallbackLocalesMap) => {
146
146
  const fallbacks = fallbackLocalesMap?.[locale] || [];
147
147
  const filteredFallbacks = fallbacks.filter((l) => l !== locale);
@@ -218,7 +218,7 @@ var BaseTranslator = class {
218
218
  }
219
219
  };
220
220
 
221
- // src/translators/shared/has-key.ts
221
+ // src/translators/methods/has-key.ts
222
222
  var hasKey = ({
223
223
  messages,
224
224
  locale,
@@ -226,11 +226,7 @@ var hasKey = ({
226
226
  targetLocale
227
227
  }) => {
228
228
  const candidateLocales = resolveCandidateLocales(targetLocale || locale);
229
- const message = findMessageInLocales({
230
- messages,
231
- candidateLocales,
232
- key
233
- });
229
+ const message = findMessageInLocales({ messages, candidateLocales, key });
234
230
  return !!message;
235
231
  };
236
232
  function translate(options) {
@@ -342,4 +338,130 @@ var ScopeTranslator = class extends CoreTranslator {
342
338
  }
343
339
  };
344
340
 
345
- export { ScopeTranslator as Translator };
341
+ // src/message/tokenize/parse-attributes.ts
342
+ var ATTR_REGEX = /\s+([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"/g;
343
+ var parseAttributes = (input) => {
344
+ const attributes = {};
345
+ let match;
346
+ let consumed = "";
347
+ while (match = ATTR_REGEX.exec(input)) {
348
+ const [, key, value] = match;
349
+ attributes[key] = value;
350
+ consumed += match[0];
351
+ }
352
+ if (consumed.length !== input.length) {
353
+ return null;
354
+ }
355
+ return attributes;
356
+ };
357
+
358
+ // src/message/tokenize/tokenize.ts
359
+ var OPEN_TAG_REGEX = /^<([a-zA-Z0-9_]+)([^>]*)>/;
360
+ var CLOSE_TAG_REGEX = /^<\/([a-zA-Z0-9_]+)>/;
361
+ var tokenize = (message) => {
362
+ const tokens = [];
363
+ let pos = 0;
364
+ let buffer = "";
365
+ const flushText = () => {
366
+ if (!buffer) return;
367
+ tokens.push({
368
+ type: "text",
369
+ value: buffer,
370
+ position: pos - buffer.length
371
+ });
372
+ buffer = "";
373
+ };
374
+ while (pos < message.length) {
375
+ const char = message[pos];
376
+ if (char === "<") {
377
+ const openMatch = message.slice(pos).match(OPEN_TAG_REGEX);
378
+ if (openMatch) {
379
+ const [, name, rawAttributes] = openMatch;
380
+ const attributes = parseAttributes(rawAttributes);
381
+ if (attributes) {
382
+ flushText();
383
+ tokens.push({
384
+ type: "tag-open",
385
+ name,
386
+ attributes,
387
+ position: pos
388
+ });
389
+ pos += openMatch[0].length;
390
+ continue;
391
+ }
392
+ }
393
+ const closeMatch = message.slice(pos).match(CLOSE_TAG_REGEX);
394
+ if (closeMatch) {
395
+ flushText();
396
+ tokens.push({
397
+ type: "tag-close",
398
+ name: closeMatch[1],
399
+ position: pos
400
+ });
401
+ pos += closeMatch[0].length;
402
+ continue;
403
+ }
404
+ }
405
+ buffer += char;
406
+ pos += 1;
407
+ }
408
+ flushText();
409
+ return tokens;
410
+ };
411
+
412
+ // src/message/build-ast/build-ast.ts
413
+ function buildAST(tokens) {
414
+ const root = [];
415
+ const stack = [];
416
+ const pushNode = (node) => {
417
+ const parent = stack.at(-1);
418
+ if (parent) {
419
+ parent.children.push(node);
420
+ } else {
421
+ root.push(node);
422
+ }
423
+ };
424
+ for (const token of tokens) {
425
+ switch (token.type) {
426
+ case "text": {
427
+ pushNode({
428
+ type: "text",
429
+ value: token.value
430
+ });
431
+ break;
432
+ }
433
+ case "tag-open": {
434
+ const node = {
435
+ type: "tag",
436
+ name: token.name,
437
+ attributes: token.attributes,
438
+ children: []
439
+ };
440
+ pushNode(node);
441
+ stack.push(node);
442
+ break;
443
+ }
444
+ case "tag-close": {
445
+ const last = stack.pop();
446
+ if (!last || last.name !== token.name) {
447
+ throw new Error(
448
+ `Unmatched closing tag </${token.name}> at position ${token.position}`
449
+ );
450
+ }
451
+ break;
452
+ }
453
+ }
454
+ }
455
+ if (stack.length > 0) {
456
+ throw new Error(`Unclosed tag detected: <${stack.at(-1)?.name}>`);
457
+ }
458
+ return root;
459
+ }
460
+
461
+ // src/message/parse-rich-message.ts
462
+ function parseRichMessage(message) {
463
+ const tokens = tokenize(message);
464
+ return buildAST(tokens);
465
+ }
466
+
467
+ export { ScopeTranslator as Translator, parseRichMessage };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "intor-translator",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "🤖 A modern, type-safe i18n engine.",
5
- "author": "Yiming Liao",
6
- "license": "MIT",
5
+ "author": {
6
+ "name": "Yiming Liao",
7
+ "email": "yimingliao.official@gmail.com",
8
+ "url": "https://github.com/yiming-liao"
9
+ },
7
10
  "homepage": "https://github.com/yiming-liao/intor-translator#readme",
8
11
  "repository": {
9
12
  "type": "git",
@@ -12,6 +15,10 @@
12
15
  "bugs": {
13
16
  "url": "https://github.com/yiming-liao/intor-translator/issues"
14
17
  },
18
+ "funding": {
19
+ "type": "github",
20
+ "url": "https://github.com/sponsors/yiming-liao"
21
+ },
15
22
  "keywords": [
16
23
  "i18n",
17
24
  "internationalization",
@@ -21,6 +28,8 @@
21
28
  "i18n engine",
22
29
  "translator"
23
30
  ],
31
+ "license": "MIT",
32
+ "type": "module",
24
33
  "exports": {
25
34
  ".": {
26
35
  "types": "./dist/index.d.ts",
@@ -36,7 +45,10 @@
36
45
  "README.md",
37
46
  "LICENSE"
38
47
  ],
39
- "type": "module",
48
+ "sideEffects": false,
49
+ "engines": {
50
+ "node": ">=16.0.0"
51
+ },
40
52
  "scripts": {
41
53
  "build": "tsup",
42
54
  "prepublishOnly": "yarn build",
@@ -47,10 +59,6 @@
47
59
  "knip": "knip --config .config/knip.config.ts",
48
60
  "examples:html": "vite --config examples/html/vite.config.ts"
49
61
  },
50
- "sideEffects": false,
51
- "engines": {
52
- "node": ">=16.0.0"
53
- },
54
62
  "dependencies": {
55
63
  "rura": "1.0.7"
56
64
  },