intor-translator 1.4.15 → 1.5.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/README.md CHANGED
@@ -7,17 +7,11 @@ The <a href="https://github.com/yiming-liao/intor">Intor</a> translation engine
7
7
  <div align="center">
8
8
 
9
9
  [![NPM version](https://img.shields.io/npm/v/intor-translator?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/intor-translator)
10
- [![Coverage Status](https://img.shields.io/coveralls/github/yiming-liao/intor-translator.svg?branch=main&style=flat&colorA=000000&colorB=000000)](https://coveralls.io/github/yiming-liao/intor-translator?branch=main)
11
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-%E2%9C%94-blue?style=flat&colorA=000000&colorB=000000)](https://www.typescriptlang.org/)
12
11
  [![License](https://img.shields.io/npm/l/intor-translator?style=flat&colorA=000000&colorB=000000)](LICENSE)
13
12
 
14
13
  </div>
15
14
 
16
- > [!NOTE]
17
- > intor-translator is the execution core of the Intor ecosystem.
18
- > It provides deterministic translation resolution and rendering,
19
- > without coupling to routing, configuration systems, or frameworks.
20
-
21
15
  ## Features
22
16
 
23
17
  - **Modular Pipeline** – A pluggable, hook-driven flow for any translation logic.
@@ -37,12 +31,6 @@ yarn add intor-translator
37
31
  pnpm add intor-translator
38
32
  ```
39
33
 
40
- Or load it directly from a CDN:
41
-
42
- ```js
43
- import { Translator } from "https://cdn.jsdelivr.net/npm/intor-translator/+esm";
44
- ```
45
-
46
34
  ## Quick Start
47
35
 
48
36
  ```typescript
@@ -67,42 +55,24 @@ translator.t("greeting", { name: "John doe" }); // -> Hello, John doe!
67
55
 
68
56
  ## Handlers & Hooks
69
57
 
70
- Intor Translator is powered by **a flexible pipeline** that lets you control how translations behave and how they are rendered.
71
-
72
- ### Handlers — format the final output
73
-
74
- <sup>_changing how translations look_.</sup>
58
+ Intor Translator runs on an explicit, hook-driven pipeline.
75
59
 
76
- Handlers operate on the resolved message, use them to:
60
+ **Ordered pipeline:**
61
+ resolveLocales → findMessage → **_loading_** → **_missing_** → **_format_** → interpolate
77
62
 
78
- - format ICU messages
79
- - apply custom plural logic
80
- - post-process output
81
- - style or transform the final string
63
+ ### Handlers
82
64
 
83
- ### Hooks shape the translation flow
84
-
85
- <sup>_changing how translations work_.</sup>
86
-
87
- Hooks run through the pipeline and can intercept any stage, use them to:
88
-
89
- - transform keys or messages
90
- - adjust fallback behavior
91
- - implement loading or missing logic
92
- - attach metadata or analytics
93
-
94
- > Together, they form a customizable translation pipeline — structured, predictable, beautifully simple.
95
-
96
- ---
65
+ Handlers override specific pipeline stages:
97
66
 
98
- ## Rich Message Processing
67
+ - loading
68
+ - missing
69
+ - formatting
99
70
 
100
- This module provides a semantic message processing flow for **_translated rich-formatted strings_**.
71
+ ### Hooks
101
72
 
102
- - Tokenize AST renderer-driven output
103
- - Environment-agnostic by design
73
+ Hooks participate in the ordered pipeline and control how the translation process executes.
104
74
 
105
- Read the documentation: [Message Processing ↗](https://github.com/yiming-liao/intor-translator/tree/main/src/message)
75
+ They allow external logic to extend or adjust the pipeline behavior.
106
76
 
107
77
  ---
108
78
 
@@ -0,0 +1,173 @@
1
+ // src/message/tokenize/utils/extract-attributes.ts
2
+ var ATTR_REGEX = /\s+([a-zA-Z_][a-zA-Z0-9_]*)="([^"]*)"/g;
3
+ var extractAttributes = (input) => {
4
+ const attributes = {};
5
+ let match;
6
+ let consumed = "";
7
+ while (match = ATTR_REGEX.exec(input)) {
8
+ const key = match[1];
9
+ const value = match[2];
10
+ attributes[key] = value;
11
+ consumed += match[0];
12
+ }
13
+ if (consumed.length !== input.length) {
14
+ return null;
15
+ }
16
+ return attributes;
17
+ };
18
+
19
+ // src/message/tokenize/tokenize.ts
20
+ var OPEN_TAG_REGEX = /^<([a-zA-Z0-9_]+)([^>]*)>/;
21
+ var CLOSE_TAG_REGEX = /^<\/([a-zA-Z0-9_]+)>/;
22
+ var tokenize = (message) => {
23
+ const tokens = [];
24
+ let pos = 0;
25
+ let buffer = "";
26
+ const flushText = () => {
27
+ if (!buffer) return;
28
+ tokens.push({
29
+ type: "text",
30
+ value: buffer,
31
+ position: pos - buffer.length
32
+ });
33
+ buffer = "";
34
+ };
35
+ while (pos < message.length) {
36
+ const char = message[pos];
37
+ if (char === "<") {
38
+ const openMatch = message.slice(pos).match(OPEN_TAG_REGEX);
39
+ if (openMatch) {
40
+ const name = openMatch[1];
41
+ const rawAttributes = openMatch[2];
42
+ const attributes = extractAttributes(rawAttributes);
43
+ if (attributes) {
44
+ flushText();
45
+ tokens.push({
46
+ type: "tag-open",
47
+ name,
48
+ attributes,
49
+ position: pos
50
+ });
51
+ pos += openMatch[0].length;
52
+ continue;
53
+ }
54
+ }
55
+ const closeMatch = message.slice(pos).match(CLOSE_TAG_REGEX);
56
+ if (closeMatch) {
57
+ const name = closeMatch[1];
58
+ flushText();
59
+ tokens.push({
60
+ type: "tag-close",
61
+ name,
62
+ position: pos
63
+ });
64
+ pos += closeMatch[0].length;
65
+ continue;
66
+ }
67
+ }
68
+ buffer += char;
69
+ pos += 1;
70
+ }
71
+ flushText();
72
+ return tokens;
73
+ };
74
+
75
+ // src/message/ast/build-ast.ts
76
+ function buildAST(tokens) {
77
+ const root = [];
78
+ const stack = [];
79
+ const pushNode = (node) => {
80
+ const parent = stack.at(-1);
81
+ if (parent) {
82
+ parent.children.push(node);
83
+ } else {
84
+ root.push(node);
85
+ }
86
+ };
87
+ for (const token of tokens) {
88
+ switch (token.type) {
89
+ case "text": {
90
+ pushNode({
91
+ type: "text",
92
+ value: token.value
93
+ });
94
+ break;
95
+ }
96
+ case "tag-open": {
97
+ const node = {
98
+ type: "tag",
99
+ name: token.name,
100
+ attributes: token.attributes,
101
+ children: []
102
+ };
103
+ pushNode(node);
104
+ stack.push(node);
105
+ break;
106
+ }
107
+ case "tag-close": {
108
+ const last = stack.pop();
109
+ if (!last || last.name !== token.name) {
110
+ throw new Error(
111
+ `Unmatched closing tag </${token.name}> at position ${token.position}`
112
+ );
113
+ }
114
+ break;
115
+ }
116
+ }
117
+ }
118
+ if (stack.length > 0) {
119
+ throw new Error(`Unclosed tag detected: <${stack.at(-1)?.name}>`);
120
+ }
121
+ return root;
122
+ }
123
+
124
+ // src/message/parse-rich-message.ts
125
+ function parseRichMessage(message) {
126
+ if (message == null) return [];
127
+ if (typeof message === "string") {
128
+ const tokens = tokenize(message);
129
+ return buildAST(tokens);
130
+ }
131
+ if (typeof message === "number" || typeof message === "boolean") {
132
+ const tokens = tokenize(String(message));
133
+ return buildAST(tokens);
134
+ }
135
+ if (Array.isArray(message)) {
136
+ return message.flatMap((m) => parseRichMessage(m));
137
+ }
138
+ return [
139
+ {
140
+ type: "raw",
141
+ value: message
142
+ }
143
+ ];
144
+ }
145
+
146
+ // src/message/render/render.ts
147
+ function render(nodes, renderer) {
148
+ return nodes.map((node) => {
149
+ switch (node.type) {
150
+ // Plain text node
151
+ case "text": {
152
+ return renderer.text(node.value);
153
+ }
154
+ // Semantic tag node
155
+ case "tag": {
156
+ const children = render(node.children, renderer);
157
+ return renderer.tag(node.name, node.attributes, children);
158
+ }
159
+ // Raw message value
160
+ case "raw": {
161
+ return renderer.raw(node.value);
162
+ }
163
+ }
164
+ });
165
+ }
166
+
167
+ // src/message/render-rich-message.ts
168
+ function renderRichMessage(message, renderer) {
169
+ const nodes = parseRichMessage(message);
170
+ return render(nodes, renderer);
171
+ }
172
+
173
+ export { renderRichMessage, tokenize };