@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 +22 -0
- package/README.md +151 -0
- package/package.json +73 -0
- package/src/I18nextProvider.js +9 -0
- package/src/IcuTrans.js +104 -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 +216 -0
- package/src/IcuTransUtils/tokenizer.js +78 -0
- package/src/IcuTransWithoutContext.js +147 -0
- package/src/Trans.js +46 -0
- package/src/TransWithoutContext.d.ts +160 -0
- package/src/TransWithoutContext.js +687 -0
- package/src/Translation.js +15 -0
- package/src/context.js +61 -0
- package/src/defaults.js +21 -0
- package/src/helpers.d.ts +3 -0
- package/src/i18nInstance.js +7 -0
- package/src/index.d.ts +315 -0
- package/src/index.js +26 -0
- package/src/initReactI18next.d.ts +3 -0
- package/src/initReactI18next.js +11 -0
- package/src/internal.js +42 -0
- package/src/unescape.js +31 -0
- package/src/useSSR.js +55 -0
- package/src/useTranslation.js +285 -0
- package/src/utils.js +93 -0
- package/src/withSSR.js +22 -0
- package/src/withTranslation.js +38 -0
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
|
+
}
|
package/src/IcuTrans.js
ADDED
|
@@ -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
|
+
' ': '\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)));
|