@octanejs/i18next 0.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2015-present i18next
4
+ Copyright (c) 2026 Dominic Gannaway
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @octanejs/i18next
2
+
3
+ [react-i18next](https://github.com/i18next/react-i18next)'s binding layer for the
4
+ [Octane](https://github.com/octanejs/octane) UI framework. It uses `i18next`
5
+ unchanged and ports the context, hooks, `Trans`, ICU, HOC, and SSR integration to
6
+ Octane.
7
+
8
+ The runtime surface tracks `react-i18next@17.0.9`, so most migrations only change
9
+ the package import:
10
+
11
+ ```diff
12
+ - import { useTranslation, Trans } from 'react-i18next';
13
+ + import { useTranslation, Trans } from '@octanejs/i18next';
14
+ ```
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pnpm add @octanejs/i18next i18next
20
+ ```
21
+
22
+ Initialize i18next with the compatibility-named `initReactI18next` plugin. The
23
+ name is retained so shared setup code and ecosystem integrations keep working.
24
+
25
+ ```ts
26
+ import i18n from 'i18next';
27
+ import { initReactI18next } from '@octanejs/i18next';
28
+
29
+ await i18n.use(initReactI18next).init({
30
+ lng: 'en',
31
+ fallbackLng: 'en',
32
+ resources: {
33
+ en: {
34
+ translation: {
35
+ greeting: 'Hello {{name}}',
36
+ richGreeting: 'Hello <strong>{{name}}</strong>',
37
+ },
38
+ },
39
+ },
40
+ interpolation: { escapeValue: false },
41
+ });
42
+ ```
43
+
44
+ Provide an instance explicitly when an app can host more than one instance or
45
+ when you want a scoped default namespace:
46
+
47
+ ```tsx
48
+ import { I18nextProvider } from '@octanejs/i18next';
49
+
50
+ export function Root() @{
51
+ <I18nextProvider i18n={i18n} defaultNS="translation">
52
+ <App />
53
+ </I18nextProvider>
54
+ }
55
+ ```
56
+
57
+ ## Hooks and Suspense
58
+
59
+ `useTranslation` has the same tuple and object shape as react-i18next and reacts
60
+ to `languageChanged` and configured store events.
61
+
62
+ ```tsx
63
+ import { useTranslation } from '@octanejs/i18next';
64
+
65
+ export function Greeting(props: { name: string }) @{
66
+ const { t, ready, i18n } = useTranslation('translation');
67
+ <section>
68
+ <p>{t('greeting', { name: props.name }) as string}</p>
69
+ <button onClick={() => i18n.changeLanguage('fr')}>French</button>
70
+ <small>{String(ready)}</small>
71
+ </section>
72
+ }
73
+ ```
74
+
75
+ Suspense is enabled by default. Missing namespaces suspend through Octane's
76
+ `use(thenable)` integration; set `useSuspense: false` and check `ready` when a
77
+ non-suspending loading state is preferable.
78
+
79
+ ## Rich translations
80
+
81
+ The most portable `Trans` form uses `defaults`/`values` and a component map:
82
+
83
+ ```tsx
84
+ <Trans
85
+ i18nKey="richGreeting"
86
+ values={{ name: 'Ada' }}
87
+ components={{ strong: <strong class="emphasis" /> }}
88
+ />
89
+ ```
90
+
91
+ There is one Octane-specific authoring rule. Natural `.tsrx` block children are
92
+ compiled into an imperative render body, so `Trans` cannot inspect them to build
93
+ its default string. Put inspectable children in prop position instead:
94
+
95
+ ```tsx
96
+ <Trans
97
+ i18nKey="richGreeting"
98
+ children={<>
99
+ Hello
100
+ <strong>
101
+ {{ name: 'Ada' }}
102
+ </strong>
103
+ </>}
104
+ />
105
+ ```
106
+
107
+ A natural block-child form renders its authored fallback and emits a development
108
+ warning explaining this adaptation.
109
+
110
+ `IcuTrans` and `IcuTransWithoutContext` support react-i18next's declaration-tree
111
+ API:
112
+
113
+ ```tsx
114
+ <IcuTrans
115
+ i18nKey="cta"
116
+ defaultTranslation="Read <0>the guide</0>"
117
+ content={[{ type: 'a', props: { href: '/guide' } }]}
118
+ />
119
+ ```
120
+
121
+ ## Server rendering
122
+
123
+ Preload the namespaces needed for a request and render normally through
124
+ `octane/server`. Namespace usage is collected on `i18n.reportNamespaces` for
125
+ serialization into the client response.
126
+
127
+ ```ts
128
+ import { renderToString } from 'octane/server';
129
+
130
+ const { html, css } = renderToString(App, { i18n });
131
+ const usedNamespaces = i18n.reportNamespaces?.getUsedNamespaces() ?? [];
132
+ ```
133
+
134
+ `useSSR`, `withSSR`, `getInitialProps`, and `composeInitialProps` are retained for
135
+ applications migrating an existing react-i18next SSR integration.
136
+
137
+ ## Supported surface and differences
138
+
139
+ The root runtime export list matches `react-i18next@17.0.9`, including
140
+ `Translation`, `withTranslation`, `TransWithoutContext`, `IcuTrans`, `useSSR`,
141
+ `withSSR`, context/default helpers, and the ICU helper exports. Octane uses
142
+ ref-as-prop semantics, so `withTranslation({ withRef: true })` does not use
143
+ `forwardRef`. Class components are not supported.
144
+
145
+ The Babel/React-specific `icu.macro` subpath is intentionally not included; use
146
+ the runtime `IcuTrans` APIs. See the generated
147
+ [bindings status](../../docs/bindings-status.md) for the current compatibility
148
+ record.
149
+
150
+ This package is derived from react-i18next and is distributed under the MIT
151
+ license.
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@octanejs/i18next",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "octane": {
11
+ "hookSlots": {
12
+ "manual": [
13
+ "src"
14
+ ]
15
+ }
16
+ },
17
+ "description": "react-i18next for the octane renderer — reuses i18next unchanged and ports the React context, hooks, Trans, ICU, and SSR binding layer onto octane.",
18
+ "author": {
19
+ "name": "Dominic Gannaway",
20
+ "email": "dg@domgan.com"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/octanejs/octane.git",
28
+ "directory": "packages/i18next"
29
+ },
30
+ "main": "src/index.js",
31
+ "module": "src/index.js",
32
+ "types": "src/index.d.ts",
33
+ "files": [
34
+ "src",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "exports": {
39
+ ".": {
40
+ "types": "./src/index.d.ts",
41
+ "default": "./src/index.js"
42
+ },
43
+ "./TransWithoutContext": {
44
+ "types": "./src/TransWithoutContext.d.ts",
45
+ "default": "./src/TransWithoutContext.js"
46
+ },
47
+ "./initReactI18next": {
48
+ "types": "./src/initReactI18next.d.ts",
49
+ "default": "./src/initReactI18next.js"
50
+ }
51
+ },
52
+ "dependencies": {
53
+ "html-parse-stringify": "^3.0.1"
54
+ },
55
+ "peerDependencies": {
56
+ "i18next": ">=26.2.0",
57
+ "octane": "0.1.5"
58
+ },
59
+ "devDependencies": {
60
+ "@testing-library/jest-dom": "^6.9.1",
61
+ "@tsrx/react": "^0.2.37",
62
+ "esbuild": "^0.28.1",
63
+ "i18next": "26.3.6",
64
+ "react": "^19.2.0",
65
+ "react-dom": "^19.2.0",
66
+ "react-i18next": "17.0.9",
67
+ "vitest": "^4.1.9",
68
+ "octane": "0.1.5"
69
+ },
70
+ "scripts": {
71
+ "test": "vitest run"
72
+ }
73
+ }
@@ -0,0 +1,9 @@
1
+ // Ported from react-i18next@17.0.9 (8b4a9ea): React primitives -> octane.
2
+ import { createElement, useMemo } from 'octane';
3
+ import { I18nContext } from './context.js';
4
+ import { S } from './internal.js';
5
+
6
+ export function I18nextProvider({ i18n, defaultNS, children }) {
7
+ const value = useMemo(() => ({ i18n, defaultNS }), [i18n, defaultNS], S('I18nextProvider:value'));
8
+ return createElement(I18nContext.Provider, { value }, children);
9
+ }
@@ -0,0 +1,104 @@
1
+ // Ported from react-i18next@17.0.9 (8b4a9ea): React context -> octane context.
2
+ import { useContext } from 'octane';
3
+ import { IcuTransWithoutContext } from './IcuTransWithoutContext.js';
4
+ import { getI18n, I18nContext } from './context.js';
5
+
6
+ /**
7
+ * IcuTrans component for rendering ICU MessageFormat translations with React components
8
+ *
9
+ * This component provides a context-aware wrapper around IcuTransWithoutContext,
10
+ * automatically retrieving the i18n instance from React context when used within
11
+ * an I18nextProvider. It uses a declaration tree approach where components are
12
+ * defined as type + props blueprints, fetches the translated string, and reconstructs
13
+ * the React element tree by replacing numbered tags with actual components.
14
+ *
15
+ * Key features:
16
+ * - Supports React Context (I18nextProvider)
17
+ * - Falls back to global i18n instance
18
+ * - ICU MessageFormat compatible
19
+ * - Type-safe component declarations
20
+ * - Automatic HTML entity decoding
21
+ *
22
+ * @param {Object} props - Component props
23
+ * @param {string} props.i18nKey - The i18n key to look up the translation
24
+ * @param {string} props.defaultTranslation - The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>")
25
+ * @param {Array<{type: string|React.ComponentType, props?: Object}>} props.content - Declaration tree describing React components and their props
26
+ * @param {string|string[]} [props.ns] - Optional namespace(s) for the translation
27
+ * @param {Object} [props.values] - Optional values for ICU variable interpolation
28
+ * @param {Object} [props.i18n] - Optional i18next instance (overrides context)
29
+ * @param {Function} [props.t] - Optional translation function (overrides context)
30
+ * @returns {React.ReactElement} React fragment containing the rendered translation
31
+ *
32
+ * @example
33
+ * ```jsx
34
+ * // Basic usage with context
35
+ * <I18nextProvider i18n={i18n}>
36
+ * <IcuTrans
37
+ * i18nKey="welcome.message"
38
+ * defaultTranslation="Welcome <0>friend</0>!"
39
+ * content={[
40
+ * { type: 'strong', props: { className: 'highlight' } }
41
+ * ]}
42
+ * />
43
+ * </I18nextProvider>
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```jsx
48
+ * // With custom components and nested structure
49
+ * <IcuTrans
50
+ * i18nKey="docs.link"
51
+ * defaultTranslation="Read the <0>documentation <1></1></0> for more info"
52
+ * content={[
53
+ * { type: 'a', props: { href: '/docs' } },
54
+ * { type: Icon, props: { name: 'external' } }
55
+ * ]}
56
+ * />
57
+ * ```
58
+ *
59
+ * @example
60
+ * ```jsx
61
+ * // With nested children in declarations
62
+ * <IcuTrans
63
+ * i18nKey="list.items"
64
+ * defaultTranslation="<0><0>First item</0><1>Second item</1></0>"
65
+ * content={[
66
+ * {
67
+ * type: 'ul',
68
+ * props: {
69
+ * children: [
70
+ * { type: 'li', props: {} },
71
+ * { type: 'li', props: {} }
72
+ * ]
73
+ * }
74
+ * }
75
+ * ]}
76
+ * />
77
+ * ```
78
+ */
79
+ export function IcuTrans({
80
+ i18nKey,
81
+ defaultTranslation,
82
+ content,
83
+ ns,
84
+ values = {},
85
+ i18n: i18nFromProps,
86
+ t: tFromProps,
87
+ }) {
88
+ const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
89
+ const i18n = i18nFromProps || i18nFromContext || getI18n();
90
+
91
+ const t = tFromProps || i18n?.t.bind(i18n);
92
+
93
+ return IcuTransWithoutContext({
94
+ i18nKey,
95
+ defaultTranslation,
96
+ content,
97
+ ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
98
+ values,
99
+ i18n,
100
+ t: tFromProps,
101
+ });
102
+ }
103
+
104
+ IcuTrans.displayName = 'IcuTrans';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Error thrown during translation parsing
3
+ */
4
+ export class TranslationParserError extends Error {
5
+ /**
6
+ * @param {string} message - Error message
7
+ * @param {number} [position] - Position in the translation string where the error occurred
8
+ * @param {string} [translationString] - The full translation string being parsed
9
+ */
10
+ constructor(message, position, translationString) {
11
+ super(message);
12
+
13
+ this.name = 'TranslationParserError';
14
+
15
+ this.position = position;
16
+
17
+ this.translationString = translationString;
18
+
19
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
20
+ if (Error.captureStackTrace) {
21
+ Error.captureStackTrace(this, TranslationParserError);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Common HTML entities map for fast lookup
3
+ */
4
+ const commonEntities = {
5
+ // Basic entities
6
+ '&nbsp;': '\u00A0', // Non-breaking space
7
+ '&amp;': '&',
8
+ '&lt;': '<',
9
+ '&gt;': '>',
10
+ '&quot;': '"',
11
+ '&apos;': "'",
12
+
13
+ // Copyright, trademark, and registration
14
+ '&copy;': '©',
15
+ '&reg;': '®',
16
+ '&trade;': '™',
17
+
18
+ // Punctuation
19
+ '&hellip;': '…',
20
+ '&ndash;': '–',
21
+ '&mdash;': '—',
22
+ '&lsquo;': '\u2018',
23
+ '&rsquo;': '\u2019',
24
+ '&sbquo;': '\u201A',
25
+ '&ldquo;': '\u201C',
26
+ '&rdquo;': '\u201D',
27
+ '&bdquo;': '\u201E',
28
+ '&dagger;': '†',
29
+ '&Dagger;': '‡',
30
+ '&bull;': '•',
31
+ '&prime;': '′',
32
+ '&Prime;': '″',
33
+ '&lsaquo;': '‹',
34
+ '&rsaquo;': '›',
35
+ '&sect;': '§',
36
+ '&para;': '¶',
37
+ '&middot;': '·',
38
+
39
+ // Spaces
40
+ '&ensp;': '\u2002',
41
+ '&emsp;': '\u2003',
42
+ '&thinsp;': '\u2009',
43
+
44
+ // Currency
45
+ '&euro;': '€',
46
+ '&pound;': '£',
47
+ '&yen;': '¥',
48
+ '&cent;': '¢',
49
+ '&curren;': '¤',
50
+
51
+ // Math symbols
52
+ '&times;': '×',
53
+ '&divide;': '÷',
54
+ '&minus;': '−',
55
+ '&plusmn;': '±',
56
+ '&ne;': '≠',
57
+ '&le;': '≤',
58
+ '&ge;': '≥',
59
+ '&asymp;': '≈',
60
+ '&equiv;': '≡',
61
+ '&infin;': '∞',
62
+ '&int;': '∫',
63
+ '&sum;': '∑',
64
+ '&prod;': '∏',
65
+ '&radic;': '√',
66
+ '&part;': '∂',
67
+ '&permil;': '‰',
68
+ '&deg;': '°',
69
+ '&micro;': 'µ',
70
+
71
+ // Arrows
72
+ '&larr;': '←',
73
+ '&uarr;': '↑',
74
+ '&rarr;': '→',
75
+ '&darr;': '↓',
76
+ '&harr;': '↔',
77
+ '&crarr;': '↵',
78
+ '&lArr;': '⇐',
79
+ '&uArr;': '⇑',
80
+ '&rArr;': '⇒',
81
+ '&dArr;': '⇓',
82
+ '&hArr;': '⇔',
83
+
84
+ // Greek letters (lowercase)
85
+ '&alpha;': 'α',
86
+ '&beta;': 'β',
87
+ '&gamma;': 'γ',
88
+ '&delta;': 'δ',
89
+ '&epsilon;': 'ε',
90
+ '&zeta;': 'ζ',
91
+ '&eta;': 'η',
92
+ '&theta;': 'θ',
93
+ '&iota;': 'ι',
94
+ '&kappa;': 'κ',
95
+ '&lambda;': 'λ',
96
+ '&mu;': 'μ',
97
+ '&nu;': 'ν',
98
+ '&xi;': 'ξ',
99
+ '&omicron;': 'ο',
100
+ '&pi;': 'π',
101
+ '&rho;': 'ρ',
102
+ '&sigma;': 'σ',
103
+ '&tau;': 'τ',
104
+ '&upsilon;': 'υ',
105
+ '&phi;': 'φ',
106
+ '&chi;': 'χ',
107
+ '&psi;': 'ψ',
108
+ '&omega;': 'ω',
109
+
110
+ // Greek letters (uppercase)
111
+ '&Alpha;': 'Α',
112
+ '&Beta;': 'Β',
113
+ '&Gamma;': 'Γ',
114
+ '&Delta;': 'Δ',
115
+ '&Epsilon;': 'Ε',
116
+ '&Zeta;': 'Ζ',
117
+ '&Eta;': 'Η',
118
+ '&Theta;': 'Θ',
119
+ '&Iota;': 'Ι',
120
+ '&Kappa;': 'Κ',
121
+ '&Lambda;': 'Λ',
122
+ '&Mu;': 'Μ',
123
+ '&Nu;': 'Ν',
124
+ '&Xi;': 'Ξ',
125
+ '&Omicron;': 'Ο',
126
+ '&Pi;': 'Π',
127
+ '&Rho;': 'Ρ',
128
+ '&Sigma;': 'Σ',
129
+ '&Tau;': 'Τ',
130
+ '&Upsilon;': 'Υ',
131
+ '&Phi;': 'Φ',
132
+ '&Chi;': 'Χ',
133
+ '&Psi;': 'Ψ',
134
+ '&Omega;': 'Ω',
135
+
136
+ // Latin extended
137
+ '&Agrave;': 'À',
138
+ '&Aacute;': 'Á',
139
+ '&Acirc;': 'Â',
140
+ '&Atilde;': 'Ã',
141
+ '&Auml;': 'Ä',
142
+ '&Aring;': 'Å',
143
+ '&AElig;': 'Æ',
144
+ '&Ccedil;': 'Ç',
145
+ '&Egrave;': 'È',
146
+ '&Eacute;': 'É',
147
+ '&Ecirc;': 'Ê',
148
+ '&Euml;': 'Ë',
149
+ '&Igrave;': 'Ì',
150
+ '&Iacute;': 'Í',
151
+ '&Icirc;': 'Î',
152
+ '&Iuml;': 'Ï',
153
+ '&ETH;': 'Ð',
154
+ '&Ntilde;': 'Ñ',
155
+ '&Ograve;': 'Ò',
156
+ '&Oacute;': 'Ó',
157
+ '&Ocirc;': 'Ô',
158
+ '&Otilde;': 'Õ',
159
+ '&Ouml;': 'Ö',
160
+ '&Oslash;': 'Ø',
161
+ '&Ugrave;': 'Ù',
162
+ '&Uacute;': 'Ú',
163
+ '&Ucirc;': 'Û',
164
+ '&Uuml;': 'Ü',
165
+ '&Yacute;': 'Ý',
166
+ '&THORN;': 'Þ',
167
+ '&szlig;': 'ß',
168
+ '&agrave;': 'à',
169
+ '&aacute;': 'á',
170
+ '&acirc;': 'â',
171
+ '&atilde;': 'ã',
172
+ '&auml;': 'ä',
173
+ '&aring;': 'å',
174
+ '&aelig;': 'æ',
175
+ '&ccedil;': 'ç',
176
+ '&egrave;': 'è',
177
+ '&eacute;': 'é',
178
+ '&ecirc;': 'ê',
179
+ '&euml;': 'ë',
180
+ '&igrave;': 'ì',
181
+ '&iacute;': 'í',
182
+ '&icirc;': 'î',
183
+ '&iuml;': 'ï',
184
+ '&eth;': 'ð',
185
+ '&ntilde;': 'ñ',
186
+ '&ograve;': 'ò',
187
+ '&oacute;': 'ó',
188
+ '&ocirc;': 'ô',
189
+ '&otilde;': 'õ',
190
+ '&ouml;': 'ö',
191
+ '&oslash;': 'ø',
192
+ '&ugrave;': 'ù',
193
+ '&uacute;': 'ú',
194
+ '&ucirc;': 'û',
195
+ '&uuml;': 'ü',
196
+ '&yacute;': 'ý',
197
+ '&thorn;': 'þ',
198
+ '&yuml;': 'ÿ',
199
+
200
+ // Special characters
201
+ '&iexcl;': '¡',
202
+ '&iquest;': '¿',
203
+ '&fnof;': 'ƒ',
204
+ '&circ;': 'ˆ',
205
+ '&tilde;': '˜',
206
+ '&OElig;': 'Œ',
207
+ '&oelig;': 'œ',
208
+ '&Scaron;': 'Š',
209
+ '&scaron;': 'š',
210
+ '&Yuml;': 'Ÿ',
211
+ '&ordf;': 'ª',
212
+ '&ordm;': 'º',
213
+ '&macr;': '¯',
214
+ '&acute;': '´',
215
+ '&cedil;': '¸',
216
+ '&sup1;': '¹',
217
+ '&sup2;': '²',
218
+ '&sup3;': '³',
219
+ '&frac14;': '¼',
220
+ '&frac12;': '½',
221
+ '&frac34;': '¾',
222
+
223
+ // Card suits
224
+ '&spades;': '♠',
225
+ '&clubs;': '♣',
226
+ '&hearts;': '♥',
227
+ '&diams;': '♦',
228
+
229
+ // Miscellaneous
230
+ '&loz;': '◊',
231
+ '&oline;': '‾',
232
+ '&frasl;': '⁄',
233
+ '&weierp;': '℘',
234
+ '&image;': 'ℑ',
235
+ '&real;': 'ℜ',
236
+ '&alefsym;': 'ℵ',
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,4 @@
1
+ export * from './TranslationParserError.js';
2
+ export * from './htmlEntityDecoder.js';
3
+ export * from './tokenizer.js';
4
+ export * from './renderTranslation.js';