intor-translator 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,8 @@ translator.t("hello"); // -> Hello World
64
64
  translator.t("greeting", { name: "John doe" }); // -> Hello, John doe!
65
65
  ```
66
66
 
67
+ ---
68
+
67
69
  ## Handlers & Hooks
68
70
 
69
71
  Intor Translator is powered by **a flexible pipeline** that lets you control how translations behave and how they are rendered.
@@ -94,5 +96,16 @@ Hooks run through the pipeline and can intercept any stage, use them to:
94
96
 
95
97
  ---
96
98
 
99
+ # Rich Message Processing
100
+
101
+ This module provides a semantic message processing flow for **_translated rich-formatted strings_**.
102
+
103
+ - Tokenize → AST → renderer-driven output
104
+ - Environment-agnostic by design
105
+
106
+ Read the documentation: [Message Processing ↗](https://github.com/yiming-liao/intor-translator/tree/main/src/message)
107
+
108
+ ---
109
+
97
110
  **_For more advanced usage, see the full examples._**
98
111
  [View examples ↗](https://github.com/yiming-liao/intor-translator/tree/main/examples)
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/utils/extract-attributes.ts
344
+ var ATTR_REGEX = /\s+([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"/g;
345
+ var extractAttributes = (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 = extractAttributes(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/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
@@ -430,4 +430,39 @@ declare class ScopeTranslator<M extends LocaleMessages | unknown = unknown, L ex
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
+ /** Semantic 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 function is a high-level entry point for processing translated
455
+ * messages that contain semantic tags (e.g. <b>, <a>, <i>).
456
+ *
457
+ * Internally, it performs the following steps:
458
+ *
459
+ * - message (string) ⬇
460
+ * - tokenize
461
+ * - build AST
462
+ *
463
+ * The returned AST represents the semantic structure of the message
464
+ * and is intended to be consumed by renderers or further processing stages.
465
+ */
466
+ declare function parseRichMessage(message: string): ASTNode[];
467
+
468
+ 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
@@ -430,4 +430,39 @@ declare class ScopeTranslator<M extends LocaleMessages | unknown = unknown, L ex
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
+ /** Semantic 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 function is a high-level entry point for processing translated
455
+ * messages that contain semantic tags (e.g. <b>, <a>, <i>).
456
+ *
457
+ * Internally, it performs the following steps:
458
+ *
459
+ * - message (string) ⬇
460
+ * - tokenize
461
+ * - build AST
462
+ *
463
+ * The returned AST represents the semantic structure of the message
464
+ * and is intended to be consumed by renderers or further processing stages.
465
+ */
466
+ declare function parseRichMessage(message: string): ASTNode[];
467
+
468
+ 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/utils/extract-attributes.ts
342
+ var ATTR_REGEX = /\s+([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"/g;
343
+ var extractAttributes = (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 = extractAttributes(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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "intor-translator",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "🤖 A modern, type-safe i18n engine.",
5
5
  "author": {
6
6
  "name": "Yiming Liao",
@@ -57,7 +57,8 @@
57
57
  "lint": "eslint",
58
58
  "lint:debug": "eslint --debug",
59
59
  "knip": "knip --config .config/knip.config.ts",
60
- "examples:html": "vite --config examples/html/vite.config.ts"
60
+ "examples:html": "vite --config examples/html/basic/vite.config.ts",
61
+ "examples:html:rich": "vite --config examples/html/rich/vite.config.ts"
61
62
  },
62
63
  "dependencies": {
63
64
  "rura": "1.0.7"