react-i18next 16.0.1 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/commonjs/IcuTrans.js +35 -0
- package/dist/commonjs/IcuTransUtils/TranslationParserError.js +18 -0
- package/dist/commonjs/IcuTransUtils/htmlEntityDecoder.js +218 -0
- package/dist/commonjs/IcuTransUtils/index.js +49 -0
- package/dist/commonjs/IcuTransUtils/renderTranslation.js +114 -0
- package/dist/commonjs/IcuTransUtils/tokenizer.js +58 -0
- package/dist/commonjs/IcuTransWithoutContext.js +56 -0
- package/dist/es/IcuTrans.js +29 -0
- package/dist/es/IcuTransUtils/TranslationParserError.js +11 -0
- package/dist/es/IcuTransUtils/htmlEntityDecoder.js +211 -0
- package/dist/es/IcuTransUtils/index.js +4 -0
- package/dist/es/IcuTransUtils/renderTranslation.js +106 -0
- package/dist/es/IcuTransUtils/tokenizer.js +51 -0
- package/dist/es/IcuTransWithoutContext.js +49 -0
- package/dist/es/package.json +1 -1
- package/icu.macro.js +170 -48
- package/package.json +6 -3
- package/src/IcuTrans.js +103 -0
- package/src/IcuTransUtils/TranslationParserError.js +24 -0
- package/src/IcuTransUtils/htmlEntityDecoder.js +264 -0
- package/src/IcuTransUtils/index.js +4 -0
- package/src/IcuTransUtils/renderTranslation.js +215 -0
- package/src/IcuTransUtils/tokenizer.js +78 -0
- package/src/IcuTransWithoutContext.js +146 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common HTML entities map for fast lookup
|
|
3
|
+
*/
|
|
4
|
+
const commonEntities = {
|
|
5
|
+
// Basic entities
|
|
6
|
+
' ': '\u00A0', // Non-breaking space
|
|
7
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
''': "'",
|
|
12
|
+
|
|
13
|
+
// Copyright, trademark, and registration
|
|
14
|
+
'©': '©',
|
|
15
|
+
'®': '®',
|
|
16
|
+
'™': '™',
|
|
17
|
+
|
|
18
|
+
// Punctuation
|
|
19
|
+
'…': '…',
|
|
20
|
+
'–': '–',
|
|
21
|
+
'—': '—',
|
|
22
|
+
'‘': '\u2018',
|
|
23
|
+
'’': '\u2019',
|
|
24
|
+
'‚': '\u201A',
|
|
25
|
+
'“': '\u201C',
|
|
26
|
+
'”': '\u201D',
|
|
27
|
+
'„': '\u201E',
|
|
28
|
+
'†': '†',
|
|
29
|
+
'‡': '‡',
|
|
30
|
+
'•': '•',
|
|
31
|
+
'′': '′',
|
|
32
|
+
'″': '″',
|
|
33
|
+
'‹': '‹',
|
|
34
|
+
'›': '›',
|
|
35
|
+
'§': '§',
|
|
36
|
+
'¶': '¶',
|
|
37
|
+
'·': '·',
|
|
38
|
+
|
|
39
|
+
// Spaces
|
|
40
|
+
' ': '\u2002',
|
|
41
|
+
' ': '\u2003',
|
|
42
|
+
' ': '\u2009',
|
|
43
|
+
|
|
44
|
+
// Currency
|
|
45
|
+
'€': '€',
|
|
46
|
+
'£': '£',
|
|
47
|
+
'¥': '¥',
|
|
48
|
+
'¢': '¢',
|
|
49
|
+
'¤': '¤',
|
|
50
|
+
|
|
51
|
+
// Math symbols
|
|
52
|
+
'×': '×',
|
|
53
|
+
'÷': '÷',
|
|
54
|
+
'−': '−',
|
|
55
|
+
'±': '±',
|
|
56
|
+
'≠': '≠',
|
|
57
|
+
'≤': '≤',
|
|
58
|
+
'≥': '≥',
|
|
59
|
+
'≈': '≈',
|
|
60
|
+
'≡': '≡',
|
|
61
|
+
'∞': '∞',
|
|
62
|
+
'∫': '∫',
|
|
63
|
+
'∑': '∑',
|
|
64
|
+
'∏': '∏',
|
|
65
|
+
'√': '√',
|
|
66
|
+
'∂': '∂',
|
|
67
|
+
'‰': '‰',
|
|
68
|
+
'°': '°',
|
|
69
|
+
'µ': 'µ',
|
|
70
|
+
|
|
71
|
+
// Arrows
|
|
72
|
+
'←': '←',
|
|
73
|
+
'↑': '↑',
|
|
74
|
+
'→': '→',
|
|
75
|
+
'↓': '↓',
|
|
76
|
+
'↔': '↔',
|
|
77
|
+
'↵': '↵',
|
|
78
|
+
'⇐': '⇐',
|
|
79
|
+
'⇑': '⇑',
|
|
80
|
+
'⇒': '⇒',
|
|
81
|
+
'⇓': '⇓',
|
|
82
|
+
'⇔': '⇔',
|
|
83
|
+
|
|
84
|
+
// Greek letters (lowercase)
|
|
85
|
+
'α': 'α',
|
|
86
|
+
'β': 'β',
|
|
87
|
+
'γ': 'γ',
|
|
88
|
+
'δ': 'δ',
|
|
89
|
+
'ε': 'ε',
|
|
90
|
+
'ζ': 'ζ',
|
|
91
|
+
'η': 'η',
|
|
92
|
+
'θ': 'θ',
|
|
93
|
+
'ι': 'ι',
|
|
94
|
+
'κ': 'κ',
|
|
95
|
+
'λ': 'λ',
|
|
96
|
+
'μ': 'μ',
|
|
97
|
+
'ν': 'ν',
|
|
98
|
+
'ξ': 'ξ',
|
|
99
|
+
'ο': 'ο',
|
|
100
|
+
'π': 'π',
|
|
101
|
+
'ρ': 'ρ',
|
|
102
|
+
'σ': 'σ',
|
|
103
|
+
'τ': 'τ',
|
|
104
|
+
'υ': 'υ',
|
|
105
|
+
'φ': 'φ',
|
|
106
|
+
'χ': 'χ',
|
|
107
|
+
'ψ': 'ψ',
|
|
108
|
+
'ω': 'ω',
|
|
109
|
+
|
|
110
|
+
// Greek letters (uppercase)
|
|
111
|
+
'Α': 'Α',
|
|
112
|
+
'Β': 'Β',
|
|
113
|
+
'Γ': 'Γ',
|
|
114
|
+
'Δ': 'Δ',
|
|
115
|
+
'Ε': 'Ε',
|
|
116
|
+
'Ζ': 'Ζ',
|
|
117
|
+
'Η': 'Η',
|
|
118
|
+
'Θ': 'Θ',
|
|
119
|
+
'Ι': 'Ι',
|
|
120
|
+
'Κ': 'Κ',
|
|
121
|
+
'Λ': 'Λ',
|
|
122
|
+
'Μ': 'Μ',
|
|
123
|
+
'Ν': 'Ν',
|
|
124
|
+
'Ξ': 'Ξ',
|
|
125
|
+
'Ο': 'Ο',
|
|
126
|
+
'Π': 'Π',
|
|
127
|
+
'Ρ': 'Ρ',
|
|
128
|
+
'Σ': 'Σ',
|
|
129
|
+
'Τ': 'Τ',
|
|
130
|
+
'Υ': 'Υ',
|
|
131
|
+
'Φ': 'Φ',
|
|
132
|
+
'Χ': 'Χ',
|
|
133
|
+
'Ψ': 'Ψ',
|
|
134
|
+
'Ω': 'Ω',
|
|
135
|
+
|
|
136
|
+
// Latin extended
|
|
137
|
+
'À': 'À',
|
|
138
|
+
'Á': 'Á',
|
|
139
|
+
'Â': 'Â',
|
|
140
|
+
'Ã': 'Ã',
|
|
141
|
+
'Ä': 'Ä',
|
|
142
|
+
'Å': 'Å',
|
|
143
|
+
'Æ': 'Æ',
|
|
144
|
+
'Ç': 'Ç',
|
|
145
|
+
'È': 'È',
|
|
146
|
+
'É': 'É',
|
|
147
|
+
'Ê': 'Ê',
|
|
148
|
+
'Ë': 'Ë',
|
|
149
|
+
'Ì': 'Ì',
|
|
150
|
+
'Í': 'Í',
|
|
151
|
+
'Î': 'Î',
|
|
152
|
+
'Ï': 'Ï',
|
|
153
|
+
'Ð': 'Ð',
|
|
154
|
+
'Ñ': 'Ñ',
|
|
155
|
+
'Ò': 'Ò',
|
|
156
|
+
'Ó': 'Ó',
|
|
157
|
+
'Ô': 'Ô',
|
|
158
|
+
'Õ': 'Õ',
|
|
159
|
+
'Ö': 'Ö',
|
|
160
|
+
'Ø': 'Ø',
|
|
161
|
+
'Ù': 'Ù',
|
|
162
|
+
'Ú': 'Ú',
|
|
163
|
+
'Û': 'Û',
|
|
164
|
+
'Ü': 'Ü',
|
|
165
|
+
'Ý': 'Ý',
|
|
166
|
+
'Þ': 'Þ',
|
|
167
|
+
'ß': 'ß',
|
|
168
|
+
'à': 'à',
|
|
169
|
+
'á': 'á',
|
|
170
|
+
'â': 'â',
|
|
171
|
+
'ã': 'ã',
|
|
172
|
+
'ä': 'ä',
|
|
173
|
+
'å': 'å',
|
|
174
|
+
'æ': 'æ',
|
|
175
|
+
'ç': 'ç',
|
|
176
|
+
'è': 'è',
|
|
177
|
+
'é': 'é',
|
|
178
|
+
'ê': 'ê',
|
|
179
|
+
'ë': 'ë',
|
|
180
|
+
'ì': 'ì',
|
|
181
|
+
'í': 'í',
|
|
182
|
+
'î': 'î',
|
|
183
|
+
'ï': 'ï',
|
|
184
|
+
'ð': 'ð',
|
|
185
|
+
'ñ': 'ñ',
|
|
186
|
+
'ò': 'ò',
|
|
187
|
+
'ó': 'ó',
|
|
188
|
+
'ô': 'ô',
|
|
189
|
+
'õ': 'õ',
|
|
190
|
+
'ö': 'ö',
|
|
191
|
+
'ø': 'ø',
|
|
192
|
+
'ù': 'ù',
|
|
193
|
+
'ú': 'ú',
|
|
194
|
+
'û': 'û',
|
|
195
|
+
'ü': 'ü',
|
|
196
|
+
'ý': 'ý',
|
|
197
|
+
'þ': 'þ',
|
|
198
|
+
'ÿ': 'ÿ',
|
|
199
|
+
|
|
200
|
+
// Special characters
|
|
201
|
+
'¡': '¡',
|
|
202
|
+
'¿': '¿',
|
|
203
|
+
'ƒ': 'ƒ',
|
|
204
|
+
'ˆ': 'ˆ',
|
|
205
|
+
'˜': '˜',
|
|
206
|
+
'Œ': 'Œ',
|
|
207
|
+
'œ': 'œ',
|
|
208
|
+
'Š': 'Š',
|
|
209
|
+
'š': 'š',
|
|
210
|
+
'Ÿ': 'Ÿ',
|
|
211
|
+
'ª': 'ª',
|
|
212
|
+
'º': 'º',
|
|
213
|
+
'¯': '¯',
|
|
214
|
+
'´': '´',
|
|
215
|
+
'¸': '¸',
|
|
216
|
+
'¹': '¹',
|
|
217
|
+
'²': '²',
|
|
218
|
+
'³': '³',
|
|
219
|
+
'¼': '¼',
|
|
220
|
+
'½': '½',
|
|
221
|
+
'¾': '¾',
|
|
222
|
+
|
|
223
|
+
// Card suits
|
|
224
|
+
'♠': '♠',
|
|
225
|
+
'♣': '♣',
|
|
226
|
+
'♥': '♥',
|
|
227
|
+
'♦': '♦',
|
|
228
|
+
|
|
229
|
+
// Miscellaneous
|
|
230
|
+
'◊': '◊',
|
|
231
|
+
'‾': '‾',
|
|
232
|
+
'⁄': '⁄',
|
|
233
|
+
'℘': '℘',
|
|
234
|
+
'ℑ': 'ℑ',
|
|
235
|
+
'ℜ': 'ℜ',
|
|
236
|
+
'ℵ': 'ℵ',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Create regex pattern for all entities
|
|
240
|
+
const entityPattern = new RegExp(
|
|
241
|
+
Object.keys(commonEntities)
|
|
242
|
+
.map((entity) => entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
243
|
+
.join('|'),
|
|
244
|
+
'g',
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Decode HTML entities in text
|
|
249
|
+
*
|
|
250
|
+
* Uses a hybrid approach:
|
|
251
|
+
* 1. First pass: decode common named entities using a map
|
|
252
|
+
* 2. Second pass: decode numeric entities (decimal and hexadecimal)
|
|
253
|
+
*
|
|
254
|
+
* @param {string} text - Text with HTML entities
|
|
255
|
+
* @returns {string} Decoded text
|
|
256
|
+
*/
|
|
257
|
+
export const decodeHtmlEntities = (text) =>
|
|
258
|
+
text
|
|
259
|
+
// First pass: common named entities
|
|
260
|
+
.replace(entityPattern, (match) => commonEntities[match])
|
|
261
|
+
// Second pass: numeric entities (decimal)
|
|
262
|
+
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))
|
|
263
|
+
// Third pass: numeric entities (hexadecimal)
|
|
264
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { TranslationParserError } from './TranslationParserError';
|
|
4
|
+
import { tokenize } from './tokenizer';
|
|
5
|
+
import { decodeHtmlEntities } from './htmlEntityDecoder';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a React element tree from a declaration node and its children
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} declaration - The component declaration (type + props)
|
|
11
|
+
* @param {Array<React.ReactNode>} children - Array of child nodes (text, numbers, React elements)
|
|
12
|
+
* @param {Array<Object>} [childDeclarations] - Optional array of child declarations to use for nested rendering
|
|
13
|
+
* @returns {React.ReactElement} A React element
|
|
14
|
+
*/
|
|
15
|
+
const renderDeclarationNode = (declaration, children, childDeclarations) => {
|
|
16
|
+
const { type, props = {} } = declaration;
|
|
17
|
+
|
|
18
|
+
// If props contain a children declaration AND we have childDeclarations to work with,
|
|
19
|
+
// we need to recursively render the content with those child declarations
|
|
20
|
+
if (props.children && Array.isArray(props.children) && childDeclarations) {
|
|
21
|
+
// The children array contains the parsed content from inside this tag
|
|
22
|
+
// We need to rebuild the translation string and re-parse it with child declarations
|
|
23
|
+
// For now, we'll use the children directly as they're already parsed
|
|
24
|
+
// This happens when renderTranslation is called recursively
|
|
25
|
+
|
|
26
|
+
// Remove children from props since we'll pass them as the third argument
|
|
27
|
+
// eslint-disable-next-line no-unused-vars
|
|
28
|
+
const { children: _childrenToRemove, ...propsWithoutChildren } = props;
|
|
29
|
+
|
|
30
|
+
return React.createElement(type, propsWithoutChildren, ...children);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Standard rendering with children from translation
|
|
34
|
+
if (children.length === 0) {
|
|
35
|
+
return React.createElement(type, props);
|
|
36
|
+
}
|
|
37
|
+
if (children.length === 1) {
|
|
38
|
+
return React.createElement(type, props, children[0]);
|
|
39
|
+
}
|
|
40
|
+
return React.createElement(type, props, ...children);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render translation string with declaration tree to create React elements
|
|
45
|
+
*
|
|
46
|
+
* This function parses an ICU format translation string and reconstructs
|
|
47
|
+
* a React element tree using the provided declaration tree. It replaces
|
|
48
|
+
* numbered tags (e.g., <0>, <1>) with the corresponding components from
|
|
49
|
+
* the declaration array and fills them with the translated text.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} translation - ICU format string (e.g., "<0>Click here</0>")
|
|
52
|
+
* @param {Array<Object>} [declarations=[]] - Array of component declarations matching tag numbers
|
|
53
|
+
* @returns {Array<React.ReactNode>} Array of React nodes (elements and text)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```jsx
|
|
57
|
+
* const result = renderTranslation(
|
|
58
|
+
* "<0>bonjour</0> monde",
|
|
59
|
+
* [{ type: 'strong', props: { className: 'bold' } }]
|
|
60
|
+
* );
|
|
61
|
+
* // Returns: [<strong className="bold">bonjour</strong>, " monde"]
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```jsx
|
|
66
|
+
* // With nested children in declaration
|
|
67
|
+
* const result = renderTranslation(
|
|
68
|
+
* "<0>Click <1>here</1></0>",
|
|
69
|
+
* [
|
|
70
|
+
* {
|
|
71
|
+
* type: 'div',
|
|
72
|
+
* props: {
|
|
73
|
+
* children: [{ type: 'span', props: {} }]
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* ]
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const renderTranslation = (translation, declarations = []) => {
|
|
81
|
+
if (!translation) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tokens = tokenize(translation);
|
|
86
|
+
const result = [];
|
|
87
|
+
const stack = [];
|
|
88
|
+
|
|
89
|
+
// Track tag numbers that should be treated as literal text (no declaration found)
|
|
90
|
+
const literalTagNumbers = new Set();
|
|
91
|
+
|
|
92
|
+
// Helper to get the current declarations array based on context
|
|
93
|
+
const getCurrentDeclarations = () => {
|
|
94
|
+
if (stack.length === 0) {
|
|
95
|
+
return declarations;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parentFrame = stack[stack.length - 1];
|
|
99
|
+
|
|
100
|
+
// If the parent declaration has children declarations, use those
|
|
101
|
+
if (
|
|
102
|
+
parentFrame.declaration.props?.children &&
|
|
103
|
+
Array.isArray(parentFrame.declaration.props.children)
|
|
104
|
+
) {
|
|
105
|
+
return parentFrame.declaration.props.children;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Otherwise, use the parent's declarations array
|
|
109
|
+
return parentFrame.declarations;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
tokens.forEach((token) => {
|
|
113
|
+
// eslint-disable-next-line default-case
|
|
114
|
+
switch (token.type) {
|
|
115
|
+
case 'Text':
|
|
116
|
+
{
|
|
117
|
+
const decoded = decodeHtmlEntities(token.value);
|
|
118
|
+
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
119
|
+
|
|
120
|
+
targetArray.push(decoded);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'TagOpen':
|
|
126
|
+
{
|
|
127
|
+
const { tagNumber } = token;
|
|
128
|
+
const currentDeclarations = getCurrentDeclarations();
|
|
129
|
+
const declaration = currentDeclarations[tagNumber];
|
|
130
|
+
|
|
131
|
+
if (!declaration) {
|
|
132
|
+
// No declaration found - treat this tag as literal text
|
|
133
|
+
literalTagNumbers.add(tagNumber);
|
|
134
|
+
|
|
135
|
+
const literalText = `<${tagNumber}>`;
|
|
136
|
+
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
137
|
+
|
|
138
|
+
targetArray.push(literalText);
|
|
139
|
+
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
stack.push({
|
|
144
|
+
tagNumber,
|
|
145
|
+
children: [],
|
|
146
|
+
position: token.position,
|
|
147
|
+
declaration,
|
|
148
|
+
declarations: currentDeclarations,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case 'TagClose':
|
|
155
|
+
{
|
|
156
|
+
const { tagNumber } = token;
|
|
157
|
+
|
|
158
|
+
// If this tag was treated as literal, output the closing tag as literal text
|
|
159
|
+
if (literalTagNumbers.has(tagNumber)) {
|
|
160
|
+
const literalText = `</${tagNumber}>`;
|
|
161
|
+
const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
162
|
+
|
|
163
|
+
literalTargetArray.push(literalText);
|
|
164
|
+
|
|
165
|
+
literalTagNumbers.delete(tagNumber);
|
|
166
|
+
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (stack.length === 0) {
|
|
171
|
+
throw new TranslationParserError(
|
|
172
|
+
`Unexpected closing tag </${tagNumber}> at position ${token.position}`,
|
|
173
|
+
token.position,
|
|
174
|
+
translation,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const frame = stack.pop();
|
|
179
|
+
|
|
180
|
+
if (frame.tagNumber !== tagNumber) {
|
|
181
|
+
throw new TranslationParserError(
|
|
182
|
+
`Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`,
|
|
183
|
+
token.position,
|
|
184
|
+
translation,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Render the element using the declaration and collected children
|
|
189
|
+
const element = renderDeclarationNode(
|
|
190
|
+
frame.declaration,
|
|
191
|
+
frame.children,
|
|
192
|
+
frame.declarations,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
196
|
+
|
|
197
|
+
elementTargetArray.push(element);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (stack.length > 0) {
|
|
205
|
+
const unclosed = stack[stack.length - 1];
|
|
206
|
+
|
|
207
|
+
throw new TranslationParserError(
|
|
208
|
+
`Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`,
|
|
209
|
+
unclosed.position,
|
|
210
|
+
translation,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize a translation string with numbered tags
|
|
3
|
+
* Note: Variables are already interpolated by the i18n system before we receive the string
|
|
4
|
+
*
|
|
5
|
+
* @param {string} translation - Translation string with numbered tags
|
|
6
|
+
* @returns {Array<Token>} Array of tokens
|
|
7
|
+
*/
|
|
8
|
+
export const tokenize = (translation) => {
|
|
9
|
+
const tokens = [];
|
|
10
|
+
|
|
11
|
+
let position = 0;
|
|
12
|
+
|
|
13
|
+
let currentText = '';
|
|
14
|
+
|
|
15
|
+
const flushText = () => {
|
|
16
|
+
if (currentText) {
|
|
17
|
+
tokens.push({
|
|
18
|
+
type: 'Text',
|
|
19
|
+
value: currentText,
|
|
20
|
+
position: position - currentText.length,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
currentText = '';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
while (position < translation.length) {
|
|
28
|
+
const char = translation[position];
|
|
29
|
+
|
|
30
|
+
// Check for opening tag: <0>, <1>, etc.
|
|
31
|
+
if (char === '<') {
|
|
32
|
+
const tagMatch = translation.slice(position).match(/^<(\d+)>/);
|
|
33
|
+
|
|
34
|
+
if (tagMatch) {
|
|
35
|
+
flushText();
|
|
36
|
+
|
|
37
|
+
tokens.push({
|
|
38
|
+
type: 'TagOpen',
|
|
39
|
+
value: tagMatch[0],
|
|
40
|
+
position,
|
|
41
|
+
tagNumber: parseInt(tagMatch[1], 10),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
position += tagMatch[0].length;
|
|
45
|
+
} else {
|
|
46
|
+
// Check for closing tag: </0>, </1>, etc.
|
|
47
|
+
const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
|
|
48
|
+
|
|
49
|
+
if (closeTagMatch) {
|
|
50
|
+
flushText();
|
|
51
|
+
|
|
52
|
+
tokens.push({
|
|
53
|
+
type: 'TagClose',
|
|
54
|
+
value: closeTagMatch[0],
|
|
55
|
+
position,
|
|
56
|
+
tagNumber: parseInt(closeTagMatch[1], 10),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
position += closeTagMatch[0].length;
|
|
60
|
+
} else {
|
|
61
|
+
// Regular text (including any { } characters that aren't our tags)
|
|
62
|
+
currentText += char;
|
|
63
|
+
|
|
64
|
+
position += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Regular text (including any { } characters that aren't our tags)
|
|
69
|
+
currentText += char;
|
|
70
|
+
|
|
71
|
+
position += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
flushText();
|
|
76
|
+
|
|
77
|
+
return tokens;
|
|
78
|
+
};
|