first commit

This commit is contained in:
Ichitux
2026-04-05 03:08:53 +02:00
commit 1082d36c12
28015 changed files with 3767672 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
{
"extends": ["airbnb", "prettier"],
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 8,
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"impliedStrict": true,
"classes": true
}
},
"env": {
"browser": true,
"node": true
},
"reportUnusedDisableDirectives": true,
"rules": {
"no-debugger": 0,
"no-alert": 0,
"no-unused-vars": [1, { "argsIgnorePattern": "res|next|^err" }],
"prefer-arrow-callback": ["error", { "allowNamedFunctions": true }],
"prefer-const": ["error", { "destructuring": "all" }],
"arrow-body-style": [2, "as-needed"],
"no-unused-expressions": [2, { "allowTaggedTemplates": true }],
"no-param-reassign": [2, { "props": false }],
"no-console": 0,
"no-use-before-define": 0,
"no-nested-ternary": 0,
"import/prefer-default-export": 0,
"import/no-extraneous-dependencies": 1,
"import/no-named-as-default-member": 1,
"import": 0,
"func-names": 0,
"space-before-function-paren": 0,
"comma-dangle": 0,
"max-len": 0,
"import/extensions": 0,
"no-underscore-dangle": 0,
"consistent-return": 0,
"react/display-name": 1,
"react/no-array-index-key": 0,
"react/jsx-no-useless-fragment": ["error", { "allowExpressions": true }],
"react/react-in-jsx-scope": 0,
"react/prefer-stateless-function": 0,
"react/forbid-prop-types": 0,
"react/no-unescaped-entities": 0,
"react/prop-types": 0,
"jsx-a11y/accessible-emoji": 0,
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/no-unknown-property": ["error", { "ignore": ["i18nIsDynamicList"] }],
"radix": 0,
"no-shadow": [
2,
{ "hoist": "all", "allow": ["resolve", "reject", "done", "next", "err", "error"] }
],
"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"jsx-a11y/href-no-hash": "off",
"jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }],
"react/jsx-props-no-spreading": 0
},
"overrides": [
{
"files": ["test/*"],
"extends": ["plugin:testing-library/react", "plugin:jest-dom/recommended"],
"globals": {
"globalThis": false
},
"rules": {
"testing-library/no-node-access": ["error", { "allowContainerFirstChild": true }],
"testing-library/no-manual-cleanup": "off"
}
}
]
}
+1
View File
@@ -0,0 +1 @@
npm exec lint-staged
+4
View File
@@ -0,0 +1,4 @@
**/dist/
**/react-i18next.js
**/react-i18next.min.js
**/example/
+1613
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015-present i18next
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+182
View File
@@ -0,0 +1,182 @@
# react-i18next [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Awesome%20react-i18next%20for%20react.js%20based%20on%20i18next%20internationalization%20ecosystem%20&url=https://github.com/i18next/react-i18next&via=jamuhl&hashtags=i18n,reactjs,js,dev)
[![CI](https://github.com/i18next/react-i18next/actions/workflows/CI.yml/badge.svg)](https://github.com/i18next/react-i18next/actions/workflows/CI.yml)
[![Coverage Status](https://coveralls.io/repos/github/i18next/react-i18next/badge.svg)](https://coveralls.io/github/i18next/react-i18next)
[![Quality][quality-badge]][quality-url]
[![npm][npm-dl-badge]][npm-url]
[npm-icon]: https://nodei.co/npm/react-i18next.png?downloads=true
[npm-url]: https://npmjs.org/package/react-i18next
[quality-badge]: https://npm.packagequality.com/shield/react-i18next.svg
[quality-url]: https://packagequality.com/#?package=react-i18next
[npm-dl-badge]: https://img.shields.io/npm/dw/react-i18next
### IMPORTANT:
Master Branch is the newest version using hooks (>= v10).
```bash
$ >=v10.0.0
npm i react-i18next
```
**react-native: To use hooks within react-native, you must use react-native v0.59.0 or higher**
For the legacy version please use the [v9.x.x Branch](https://github.com/i18next/react-i18next/tree/v9.x.x)
```bash
$ v9.0.10 (legacy)
npm i react-i18next@legacy
```
### Documentation
The documentation is published on [react.i18next.com](https://react.i18next.com) and PR changes can be supplied [here](https://github.com/i18next/react-i18next-gitbook).
The general i18next documentation is published on [www.i18next.com](https://www.i18next.com) and PR changes can be supplied [here](https://github.com/i18next/i18next-gitbook).
### What will my code look like?
**Before:** Your react code would have looked something like:
```html
...
<div>Just simple content</div>
<div>
Hello <strong title="this is your name">{name}</strong>, you have {count} unread message(s). <Link to="/msgs">Go to messages</Link>.
</div>
...
```
**After:** With the trans component just change it to:
```html
...
<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>
...
```
### 📖 What others say
- [How to properly internationalize a React application using i18next](https://locize.com/blog/react-i18next/) by Adriano Raiano
- [I18n with React and i18next](https://alligator.io/react/i18n-with-react-and-i18next) via Alligator.io by Danny Hurlburt
- [Ultimate Localization of React (Mobx) App with i18next](https://itnext.io/ultimate-localization-of-react-mobx-app-with-i18next-efab77712149) via itnext.io by Viktor Shevchenko
- [Internationalization for react done right Using the i18next i18n ecosystem](https://reactjsexample.com/internationalization-for-react-done-right-using-the-i18next-i18n-ecosystem/) via reactjsexample.com
- [How to translate React application with react-i18next](https://codetain.com/blog/how-to-translate-react-application-with-react-i18next/) via codetain.co by Norbert Suski
- [Building i18n with Gatsby](https://www.gatsbyjs.org/blog/2017-10-17-building-i18n-with-gatsby/) via gatsbyjs.org by Samuel Goudie
- [Get your react.js application translated with style](https://medium.com/@jamuhl/get-your-react-js-application-translated-with-style-4ad090aefc2c) by Jan Mühlemann
- [Translate your expo.io / react-native mobile application](https://medium.com/@jamuhl/translate-your-expo-io-react-native-mobile-application-aa220b2362d2) by Jan Mühlemann
- You're welcome to share your story...
### Why i18next?
- **Simplicity:** no need to change your webpack configuration or add additional babel transpilers, just use create-react-app and go.
- **Production ready** we know there are more needs for production than just doing i18n on the clientside, so we offer wider support on [serverside](https://www.i18next.com/overview/supported-frameworks) too (nodejs, php, ruby, .net, ...). **Learn once - translate everywhere**.
- **Beyond i18n** comes with [Locize](https://locize.com) bridging the gap between development and translations - covering the whole translation process. Now with a **Free plan** for your side projects!
![ecosystem](https://raw.githubusercontent.com/i18next/i18next/master/assets/i18next-ecosystem.jpg)
### Localization workflow
Want to learn more about how seamless your internationalization and translation process can be?
[![video](example/locize/video_sample.png)](https://youtu.be/osScyaGMVqo)
[watch the video](https://youtu.be/osScyaGMVqo)
### Installation
Source can be loaded via [npm](https://www.npmjs.com/package/react-i18next) or [downloaded](https://github.com/i18next/react-i18next/blob/master/react-i18next.min.js) from this repo.
```
# npm package
$ npm install react-i18next
```
- If you don't use a module loader it will be added to `window.reactI18next`
### Do you like to read a more complete step by step tutorial?
[Here](https://locize.com/blog/react-i18next/) you'll find a simple tutorial on how to best use react-i18next.
Some basics of i18next and some cool possibilities on how to optimize your localization workflow.
### Examples
- [Example react](https://github.com/i18next/react-i18next/tree/master/example/react)
- [React examples with typescript](https://github.com/i18next/react-i18next/tree/master/example/react-typescript)
- [Example locize.com](https://github.com/i18next/react-i18next/tree/master/example/locize)
#### v9 samples
- [Example react](https://github.com/i18next/react-i18next/tree/v9.x.x/example/react)
- [Example preact](https://github.com/i18next/react-i18next/tree/v9.x.x/example/preact)
- [Example react-native](https://github.com/i18next/react-i18next/tree/v9.x.x/example/reactnative-expo)
- [Example expo.io](https://github.com/i18next/react-i18next/tree/v9.x.x/example/reactnative-expo)
- [Example next.js](https://github.com/i18next/react-i18next/tree/v9.x.x/example/nextjs)
- [Example razzle](https://github.com/i18next/react-i18next/tree/v9.x.x/example/razzle-ssr)
- [Example hashbase / beaker browser](https://github.com/i18next/react-i18next/tree/v9.x.x/example/dat)
- [Example storybook](https://github.com/i18next/react-i18next/tree/v9.x.x/example/storybook)
- [Example locize.com](https://github.com/i18next/react-i18next/tree/v9.x.x/example/locize)
- [Example test with jest](https://github.com/i18next/react-i18next/tree/v9.x.x/example/test-jest)
### Requirements
- react >= **16.8.0**
- react-dom >= **16.8.0**
- react-native >= **0.59.0**
- i18next >= **10.0.0** (typescript users: >=17.0.9)
#### v9
- react >= **0.14.0** (in case of < v16 or preact you will need to define parent in [Trans component](https://react.i18next.com/legacy-v9/trans-component#trans-props) or globally in [i18next.react options](https://react.i18next.com/legacy-v9/trans-component#additional-options-on-i-18-next-init))
- i18next >= **2.0.0**
## Core Contributors
Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://twitter.com/jamuhl"><img src="https://avatars3.githubusercontent.com/u/977772?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jan Mühlemann</b></sub></a><br /><a href="https://github.com/i18next/react-i18next/commits?author=jamuhl" title="Code">💻</a> <a href="#example-jamuhl" title="Examples">💡</a> <a href="https://github.com/i18next/react-i18next/pulls?q=is%3Apr+reviewed-by%3Ajamuhl+" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/i18next/react-i18next/commits?author=jamuhl" title="Documentation">📖</a> <a href="#question-jamuhl" title="Answering Questions">💬</a></td>
<td align="center"><a href="http://twitter.com/#!/adrirai"><img src="https://avatars0.githubusercontent.com/u/1086194?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Adriano Raiano</b></sub></a><br /><a href="https://github.com/i18next/react-i18next/commits?author=adrai" title="Code">💻</a> <a href="#example-adrai" title="Examples">💡</a> <a href="https://github.com/i18next/react-i18next/pulls?q=is%3Apr+reviewed-by%3Aadrai+" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/i18next/react-i18next/commits?author=adrai" title="Documentation">📖</a> <a href="#question-adrai" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/pedrodurek"><img src="https://avatars1.githubusercontent.com/u/12190482?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pedro Durek</b></sub></a><br /><a href="https://github.com/i18next/react-i18next/commits?author=pedrodurek" title="Code">💻</a> <a href="#example-pedrodurek" title="Examples">💡</a> <a href="https://github.com/i18next/react-i18next/pulls?q=is%3Apr+reviewed-by%3Apedrodurek+" title="Reviewed Pull Requests">👀</a> <a href="#question-pedrodurek" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://tigerabrodi.dev/"><img src="https://avatars1.githubusercontent.com/u/49603590?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tiger Abrodi</b></sub></a><br /><a href="https://github.com/i18next/react-i18next/commits?author=tigerabrodi" title="Code">💻</a> <a href="https://github.com/i18next/react-i18next/pulls?q=is%3Apr+reviewed-by%3Atigerabrodi" title="Reviewed Pull Requests">👀</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome!
---
<h3 align="center">Gold Sponsors</h3>
<p align="center">
<a href="https://locize.com/" target="_blank">
<img src="https://raw.githubusercontent.com/i18next/i18next/master/assets/locize_sponsor_240.gif" width="240px">
</a>
</p>
---
**localization as a service - [Locize](https://www.locize.com)**
Needing a translation management? Want to edit your translations with an InContext Editor? Use the original provided to you by the maintainers of i18next!
**Now with a [Free plan](https://www.locize.com/pricing) for small projects!** Perfect for hobbyists or getting started.
![Locize](https://www.locize.com/img/ads/github_locize.png)
By using [Locize](http://www.locize.com/?utm_source=react_i18next_readme&utm_medium=github) you directly support the future of i18next and react-i18next.
---
+1
View File
@@ -0,0 +1 @@
export * from './TransWithoutContext.js';
+153
View File
@@ -0,0 +1,153 @@
import type {
i18n,
ReactOptions,
ApplyTarget,
ConstrainTarget,
GetSource,
InterpolationMap,
ParseKeys,
Namespace,
SelectorFn,
SelectorKey,
TFunctionReturn,
TypeOptions,
TOptions,
TFunction,
} from 'i18next';
import * as React from 'react';
type _DefaultNamespace = TypeOptions['defaultNS'];
type _EnableSelector = TypeOptions['enableSelector'];
type _KeySeparator = TypeOptions['keySeparator'];
type _AppendKeyPrefix<Key, KPrefix> = KPrefix extends string
? `${KPrefix}${_KeySeparator}${Key & string}`
: Key;
type TransChild = React.ReactNode | Record<string, unknown>;
type $NoInfer<T> = [T][T extends T ? 0 : never];
export type TransProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
Ret = TFunctionReturn<Ns, _AppendKeyPrefix<Key, KPrefix>, TOpt>,
E = React.HTMLProps<HTMLDivElement>,
> = E & {
children?: TransChild | readonly TransChild[];
components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement };
count?: number;
context?: TContext;
defaults?: string;
i18n?: i18n;
i18nKey?: Key | Key[];
ns?: Ns;
parent?: string | React.ComponentType<any> | null; // used in React.createElement if not null
tOptions?: TOpt;
values?: InterpolationMap<Ret>;
shouldUnescape?: boolean;
t?: TFunction<Ns, KPrefix>;
};
export interface TransLegacy {
<
const Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
Ret extends TFunctionReturn<Ns, _AppendKeyPrefix<Key, KPrefix>, TOpt> = TFunctionReturn<
Ns,
_AppendKeyPrefix<Key, KPrefix>,
TOpt
>,
E = React.HTMLProps<HTMLDivElement>,
>(
props: TransProps<Key, Ns, KPrefix, TContext, TOpt, Ret, E>,
): React.ReactElement;
}
export interface TransSelectorProps<
Key,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
> {
children?: TransChild | readonly TransChild[];
components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement };
count?: number;
context?: TContext;
defaults?: string | Key;
i18n?: i18n;
i18nKey?: Key | readonly Key[];
ns?: Ns;
parent?: string | React.ComponentType<any> | null; // used in React.createElement if not null
tOptions?: TOpt;
values?: Key extends (...args: any[]) => infer R ? InterpolationMap<R> : {};
shouldUnescape?: boolean;
t?: TFunction<Ns, KPrefix>;
}
export interface TransSelector {
<
Target extends ConstrainTarget<TOpt>,
Key extends
| SelectorFn<GetSource<$NoInfer<Ns>, KPrefix>, ApplyTarget<Target, TOpt>, TOpt>
| SelectorKey,
const Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
>(
props: TransSelectorProps<Key, Ns, KPrefix, TContext, TOpt> & E,
): React.ReactElement;
}
export const Trans: _EnableSelector extends true | 'optimize' ? TransSelector : TransLegacy;
export function nodesToString(
children: React.ReactNode,
i18nOptions?: ReactOptions,
i18n?: i18n,
i18nKey?: string,
): string;
export type ErrorCode =
| 'NO_I18NEXT_INSTANCE'
| 'NO_LANGUAGES'
| 'DEPRECATED_OPTION'
| 'TRANS_NULL_VALUE'
| 'TRANS_INVALID_OBJ'
| 'TRANS_INVALID_VAR'
| 'TRANS_INVALID_COMPONENTS'
| 'USE_T_BEFORE_READY';
export type ErrorMeta = {
code: ErrorCode;
i18nKey?: string;
[x: string]: any;
};
/**
* Use to type the logger arguments
* @example
* ```
* import type { ErrorArgs } from 'react-i18next';
*
* const logger = {
* // ....
* warn: function (...args: ErrorArgs) {
* if (args[1]?.code === 'TRANS_INVALID_OBJ') {
* const [msg, { i18nKey, ...rest }] = args;
* return log(i18nKey, msg, rest);
* }
* log(...args);
* }
* }
* i18n.use(logger).use(i18nReactPlugin).init({...});
* ```
*/
export type ErrorArgs = readonly [string, ErrorMeta | undefined, ...any[]];
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.I18nextProvider = I18nextProvider;
var _react = require("react");
var _context = require("./context.js");
function I18nextProvider({
i18n,
defaultNS,
children
}) {
const value = (0, _react.useMemo)(() => ({
i18n,
defaultNS
}), [i18n, defaultNS]);
return (0, _react.createElement)(_context.I18nContext.Provider, {
value
}, children);
}
+35
View File
@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.IcuTrans = IcuTrans;
var _react = require("react");
var _IcuTransWithoutContext = require("./IcuTransWithoutContext.js");
var _context = require("./context.js");
function IcuTrans({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps
}) {
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = (0, _react.useContext)(_context.I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || (0, _context.getI18n)();
const t = tFromProps || i18n?.t.bind(i18n);
return (0, _IcuTransWithoutContext.IcuTransWithoutContext)({
i18nKey,
defaultTranslation,
content,
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
values,
i18n,
t: tFromProps
});
}
IcuTrans.displayName = 'IcuTrans';
@@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TranslationParserError = void 0;
class TranslationParserError extends Error {
constructor(message, position, translationString) {
super(message);
this.name = 'TranslationParserError';
this.position = position;
this.translationString = translationString;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TranslationParserError);
}
}
}
exports.TranslationParserError = TranslationParserError;
@@ -0,0 +1,218 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.decodeHtmlEntities = void 0;
const commonEntities = {
'&nbsp;': '\u00A0',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&copy;': '©',
'&reg;': '®',
'&trade;': '™',
'&hellip;': '…',
'&ndash;': '–',
'&mdash;': '—',
'&lsquo;': '\u2018',
'&rsquo;': '\u2019',
'&sbquo;': '\u201A',
'&ldquo;': '\u201C',
'&rdquo;': '\u201D',
'&bdquo;': '\u201E',
'&dagger;': '†',
'&Dagger;': '‡',
'&bull;': '•',
'&prime;': '′',
'&Prime;': '″',
'&lsaquo;': '‹',
'&rsaquo;': '›',
'&sect;': '§',
'&para;': '¶',
'&middot;': '·',
'&ensp;': '\u2002',
'&emsp;': '\u2003',
'&thinsp;': '\u2009',
'&euro;': '€',
'&pound;': '£',
'&yen;': '¥',
'&cent;': '¢',
'&curren;': '¤',
'&times;': '×',
'&divide;': '÷',
'&minus;': '−',
'&plusmn;': '±',
'&ne;': '≠',
'&le;': '≤',
'&ge;': '≥',
'&asymp;': '≈',
'&equiv;': '≡',
'&infin;': '∞',
'&int;': '∫',
'&sum;': '∑',
'&prod;': '∏',
'&radic;': '√',
'&part;': '∂',
'&permil;': '‰',
'&deg;': '°',
'&micro;': 'µ',
'&larr;': '←',
'&uarr;': '↑',
'&rarr;': '→',
'&darr;': '↓',
'&harr;': '↔',
'&crarr;': '↵',
'&lArr;': '⇐',
'&uArr;': '⇑',
'&rArr;': '⇒',
'&dArr;': '⇓',
'&hArr;': '⇔',
'&alpha;': 'α',
'&beta;': 'β',
'&gamma;': 'γ',
'&delta;': 'δ',
'&epsilon;': 'ε',
'&zeta;': 'ζ',
'&eta;': 'η',
'&theta;': 'θ',
'&iota;': 'ι',
'&kappa;': 'κ',
'&lambda;': 'λ',
'&mu;': 'μ',
'&nu;': 'ν',
'&xi;': 'ξ',
'&omicron;': 'ο',
'&pi;': 'π',
'&rho;': 'ρ',
'&sigma;': 'σ',
'&tau;': 'τ',
'&upsilon;': 'υ',
'&phi;': 'φ',
'&chi;': 'χ',
'&psi;': 'ψ',
'&omega;': 'ω',
'&Alpha;': 'Α',
'&Beta;': 'Β',
'&Gamma;': 'Γ',
'&Delta;': 'Δ',
'&Epsilon;': 'Ε',
'&Zeta;': 'Ζ',
'&Eta;': 'Η',
'&Theta;': 'Θ',
'&Iota;': 'Ι',
'&Kappa;': 'Κ',
'&Lambda;': 'Λ',
'&Mu;': 'Μ',
'&Nu;': 'Ν',
'&Xi;': 'Ξ',
'&Omicron;': 'Ο',
'&Pi;': 'Π',
'&Rho;': 'Ρ',
'&Sigma;': 'Σ',
'&Tau;': 'Τ',
'&Upsilon;': 'Υ',
'&Phi;': 'Φ',
'&Chi;': 'Χ',
'&Psi;': 'Ψ',
'&Omega;': 'Ω',
'&Agrave;': 'À',
'&Aacute;': 'Á',
'&Acirc;': 'Â',
'&Atilde;': 'Ã',
'&Auml;': 'Ä',
'&Aring;': 'Å',
'&AElig;': 'Æ',
'&Ccedil;': 'Ç',
'&Egrave;': 'È',
'&Eacute;': 'É',
'&Ecirc;': 'Ê',
'&Euml;': 'Ë',
'&Igrave;': 'Ì',
'&Iacute;': 'Í',
'&Icirc;': 'Î',
'&Iuml;': 'Ï',
'&ETH;': 'Ð',
'&Ntilde;': 'Ñ',
'&Ograve;': 'Ò',
'&Oacute;': 'Ó',
'&Ocirc;': 'Ô',
'&Otilde;': 'Õ',
'&Ouml;': 'Ö',
'&Oslash;': 'Ø',
'&Ugrave;': 'Ù',
'&Uacute;': 'Ú',
'&Ucirc;': 'Û',
'&Uuml;': 'Ü',
'&Yacute;': 'Ý',
'&THORN;': 'Þ',
'&szlig;': 'ß',
'&agrave;': 'à',
'&aacute;': 'á',
'&acirc;': 'â',
'&atilde;': 'ã',
'&auml;': 'ä',
'&aring;': 'å',
'&aelig;': 'æ',
'&ccedil;': 'ç',
'&egrave;': 'è',
'&eacute;': 'é',
'&ecirc;': 'ê',
'&euml;': 'ë',
'&igrave;': 'ì',
'&iacute;': 'í',
'&icirc;': 'î',
'&iuml;': 'ï',
'&eth;': 'ð',
'&ntilde;': 'ñ',
'&ograve;': 'ò',
'&oacute;': 'ó',
'&ocirc;': 'ô',
'&otilde;': 'õ',
'&ouml;': 'ö',
'&oslash;': 'ø',
'&ugrave;': 'ù',
'&uacute;': 'ú',
'&ucirc;': 'û',
'&uuml;': 'ü',
'&yacute;': 'ý',
'&thorn;': 'þ',
'&yuml;': 'ÿ',
'&iexcl;': '¡',
'&iquest;': '¿',
'&fnof;': 'ƒ',
'&circ;': 'ˆ',
'&tilde;': '˜',
'&OElig;': 'Œ',
'&oelig;': 'œ',
'&Scaron;': 'Š',
'&scaron;': 'š',
'&Yuml;': 'Ÿ',
'&ordf;': 'ª',
'&ordm;': 'º',
'&macr;': '¯',
'&acute;': '´',
'&cedil;': '¸',
'&sup1;': '¹',
'&sup2;': '²',
'&sup3;': '³',
'&frac14;': '¼',
'&frac12;': '½',
'&frac34;': '¾',
'&spades;': '♠',
'&clubs;': '♣',
'&hearts;': '♥',
'&diams;': '♦',
'&loz;': '◊',
'&oline;': '‾',
'&frasl;': '⁄',
'&weierp;': '℘',
'&image;': 'ℑ',
'&real;': 'ℜ',
'&alefsym;': 'ℵ'
};
const entityPattern = new RegExp(Object.keys(commonEntities).map(entity => entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
const decodeHtmlEntities = text => text.replace(entityPattern, match => commonEntities[match]).replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))).replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
exports.decodeHtmlEntities = decodeHtmlEntities;
+49
View File
@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _TranslationParserError = require("./TranslationParserError.js");
Object.keys(_TranslationParserError).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _TranslationParserError[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _TranslationParserError[key];
}
});
});
var _htmlEntityDecoder = require("./htmlEntityDecoder.js");
Object.keys(_htmlEntityDecoder).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _htmlEntityDecoder[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _htmlEntityDecoder[key];
}
});
});
var _tokenizer = require("./tokenizer.js");
Object.keys(_tokenizer).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _tokenizer[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _tokenizer[key];
}
});
});
var _renderTranslation = require("./renderTranslation.js");
Object.keys(_renderTranslation).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _renderTranslation[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function () {
return _renderTranslation[key];
}
});
});
@@ -0,0 +1,114 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.renderTranslation = void 0;
var _react = _interopRequireDefault(require("react"));
var _TranslationParserError = require("./TranslationParserError.js");
var _tokenizer = require("./tokenizer.js");
var _htmlEntityDecoder = require("./htmlEntityDecoder.js");
const renderDeclarationNode = (declaration, children, childDeclarations) => {
const {
type,
props = {}
} = declaration;
if (props.children && Array.isArray(props.children) && childDeclarations) {
const {
children: _childrenToRemove,
...propsWithoutChildren
} = props;
return _react.default.createElement(type, propsWithoutChildren, ...children);
}
if (children.length === 0) {
return _react.default.createElement(type, props);
}
if (children.length === 1) {
return _react.default.createElement(type, props, children[0]);
}
return _react.default.createElement(type, props, ...children);
};
const renderTranslation = (translation, declarations = []) => {
if (!translation) {
return [];
}
const tokens = (0, _tokenizer.tokenize)(translation);
const result = [];
const stack = [];
const literalTagNumbers = new Set();
const getCurrentDeclarations = () => {
if (stack.length === 0) {
return declarations;
}
const parentFrame = stack[stack.length - 1];
if (parentFrame.declaration.props?.children && Array.isArray(parentFrame.declaration.props.children)) {
return parentFrame.declaration.props.children;
}
return parentFrame.declarations;
};
tokens.forEach(token => {
switch (token.type) {
case 'Text':
{
const decoded = (0, _htmlEntityDecoder.decodeHtmlEntities)(token.value);
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(decoded);
}
break;
case 'TagOpen':
{
const {
tagNumber
} = token;
const currentDeclarations = getCurrentDeclarations();
const declaration = currentDeclarations[tagNumber];
if (!declaration) {
literalTagNumbers.add(tagNumber);
const literalText = `<${tagNumber}>`;
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(literalText);
break;
}
stack.push({
tagNumber,
children: [],
position: token.position,
declaration,
declarations: currentDeclarations
});
}
break;
case 'TagClose':
{
const {
tagNumber
} = token;
if (literalTagNumbers.has(tagNumber)) {
const literalText = `</${tagNumber}>`;
const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
literalTargetArray.push(literalText);
literalTagNumbers.delete(tagNumber);
break;
}
if (stack.length === 0) {
throw new _TranslationParserError.TranslationParserError(`Unexpected closing tag </${tagNumber}> at position ${token.position}`, token.position, translation);
}
const frame = stack.pop();
if (frame.tagNumber !== tagNumber) {
throw new _TranslationParserError.TranslationParserError(`Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`, token.position, translation);
}
const element = renderDeclarationNode(frame.declaration, frame.children, frame.declarations);
const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
elementTargetArray.push(element);
}
break;
}
});
if (stack.length > 0) {
const unclosed = stack[stack.length - 1];
throw new _TranslationParserError.TranslationParserError(`Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`, unclosed.position, translation);
}
return result;
};
exports.renderTranslation = renderTranslation;
+58
View File
@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.tokenize = void 0;
const tokenize = translation => {
const tokens = [];
let position = 0;
let currentText = '';
const flushText = () => {
if (currentText) {
tokens.push({
type: 'Text',
value: currentText,
position: position - currentText.length
});
currentText = '';
}
};
while (position < translation.length) {
const char = translation[position];
if (char === '<') {
const tagMatch = translation.slice(position).match(/^<(\d+)>/);
if (tagMatch) {
flushText();
tokens.push({
type: 'TagOpen',
value: tagMatch[0],
position,
tagNumber: parseInt(tagMatch[1], 10)
});
position += tagMatch[0].length;
} else {
const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
if (closeTagMatch) {
flushText();
tokens.push({
type: 'TagClose',
value: closeTagMatch[0],
position,
tagNumber: parseInt(closeTagMatch[1], 10)
});
position += closeTagMatch[0].length;
} else {
currentText += char;
position += 1;
}
}
} else {
currentText += char;
position += 1;
}
}
flushText();
return tokens;
};
exports.tokenize = tokenize;
+56
View File
@@ -0,0 +1,56 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.IcuTransWithoutContext = IcuTransWithoutContext;
var _react = _interopRequireDefault(require("react"));
var _utils = require("./utils.js");
var _i18nInstance = require("./i18nInstance.js");
var _index = require("./IcuTransUtils/index.js");
function IcuTransWithoutContext({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps
}) {
const i18n = i18nFromProps || (0, _i18nInstance.getI18n)();
if (!i18n) {
(0, _utils.warnOnce)(i18n, 'NO_I18NEXT_INSTANCE', `IcuTrans: You need to pass in an i18next instance using i18nextReactModule`, {
i18nKey
});
return _react.default.createElement(_react.default.Fragment, {}, defaultTranslation);
}
const t = tFromProps || i18n.t?.bind(i18n) || (k => k);
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = (0, _utils.isString)(namespaces) ? [namespaces] : namespaces || ['translation'];
let mergedValues = values;
if (i18n.options?.interpolation?.defaultVariables) {
mergedValues = values && Object.keys(values).length > 0 ? {
...values,
...i18n.options.interpolation.defaultVariables
} : {
...i18n.options.interpolation.defaultVariables
};
}
const translation = t(i18nKey, {
defaultValue: defaultTranslation,
...mergedValues,
ns: namespaces
});
try {
const rendered = (0, _index.renderTranslation)(translation, content);
return _react.default.createElement(_react.default.Fragment, {}, ...rendered);
} catch (error) {
(0, _utils.warn)(i18n, 'ICU_TRANS_RENDER_ERROR', `IcuTrans component error for key "${i18nKey}": ${error.message}`, {
i18nKey,
error
});
return _react.default.createElement(_react.default.Fragment, {}, translation);
}
}
IcuTransWithoutContext.displayName = 'IcuTransWithoutContext';
+54
View File
@@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Trans = Trans;
Object.defineProperty(exports, "nodesToString", {
enumerable: true,
get: function () {
return _TransWithoutContext.nodesToString;
}
});
var _react = require("react");
var _TransWithoutContext = require("./TransWithoutContext.js");
var _context = require("./context.js");
function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = (0, _react.useContext)(_context.I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || (0, _context.getI18n)();
const t = tFromProps || i18n?.t.bind(i18n);
return (0, _TransWithoutContext.Trans)({
children,
count,
parent,
i18nKey,
context,
tOptions,
values,
defaults,
components,
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
i18n,
t: tFromProps,
shouldUnescape,
...additionalProps
});
}
+428
View File
@@ -0,0 +1,428 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Trans = Trans;
exports.nodesToString = void 0;
var _react = require("react");
var _i18next = require("i18next");
var _htmlParseStringify = _interopRequireDefault(require("html-parse-stringify"));
var _utils = require("./utils.js");
var _defaults = require("./defaults.js");
var _i18nInstance = require("./i18nInstance.js");
var _unescape = require("./unescape.js");
const hasChildren = (node, checkLength) => {
if (!node) return false;
const base = node.props?.children ?? node.children;
if (checkLength) return base.length > 0;
return !!base;
};
const getChildren = node => {
if (!node) return [];
const children = node.props?.children ?? node.children;
return node.props?.i18nIsDynamicList ? getAsArray(children) : children;
};
const hasValidReactChildren = children => Array.isArray(children) && children.every(_react.isValidElement);
const getAsArray = data => Array.isArray(data) ? data : [data];
const mergeProps = (source, target) => {
const newTarget = {
...target
};
newTarget.props = {
...target.props,
...source.props
};
return newTarget;
};
const getValuesFromChildren = children => {
const values = {};
if (!children) return values;
const getData = childs => {
const childrenArray = getAsArray(childs);
childrenArray.forEach(child => {
if ((0, _utils.isString)(child)) return;
if (hasChildren(child)) getData(getChildren(child));else if ((0, _utils.isObject)(child) && !(0, _react.isValidElement)(child)) Object.assign(values, child);
});
};
getData(children);
return values;
};
const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
if (!children) return '';
let stringNode = '';
const childrenArray = getAsArray(children);
const keepArray = i18nOptions?.transSupportBasicHtmlNodes ? i18nOptions.transKeepBasicHtmlNodesFor ?? [] : [];
childrenArray.forEach((child, childIndex) => {
if ((0, _utils.isString)(child)) {
stringNode += `${child}`;
return;
}
if ((0, _react.isValidElement)(child)) {
const {
props,
type
} = child;
const childPropsCount = Object.keys(props).length;
const shouldKeepChild = keepArray.indexOf(type) > -1;
const childChildren = props.children;
if (!childChildren && shouldKeepChild && !childPropsCount) {
stringNode += `<${type}/>`;
return;
}
if (!childChildren && (!shouldKeepChild || childPropsCount) || props.i18nIsDynamicList) {
stringNode += `<${childIndex}></${childIndex}>`;
return;
}
if (shouldKeepChild && childPropsCount <= 1) {
const cnt = (0, _utils.isString)(childChildren) ? childChildren : nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${type}>${cnt}</${type}>`;
return;
}
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
return;
}
if (child === null) {
(0, _utils.warn)(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, {
i18nKey
});
return;
}
if ((0, _utils.isObject)(child)) {
const {
format,
...clone
} = child;
const keys = Object.keys(clone);
if (keys.length === 1) {
const value = format ? `${keys[0]}, ${format}` : keys[0];
stringNode += `{{${value}}}`;
return;
}
(0, _utils.warn)(i18n, 'TRANS_INVALID_OBJ', `Invalid child - Object should only have keys {{ value, format }} (format is optional).`, {
i18nKey,
child
});
return;
}
(0, _utils.warn)(i18n, 'TRANS_INVALID_VAR', `Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`, {
i18nKey,
child
});
});
return stringNode;
};
exports.nodesToString = nodesToString;
const escapeLiteralLessThan = (str, keepArray = [], knownComponentsMap = {}) => {
if (!str) return str;
const knownNames = Object.keys(knownComponentsMap);
const allValidNames = [...keepArray, ...knownNames];
let result = '';
let i = 0;
while (i < str.length) {
if (str[i] === '<') {
let isValidTag = false;
const closingMatch = str.slice(i).match(/^<\/(\d+|[a-zA-Z][a-zA-Z0-9_-]*)>/);
if (closingMatch) {
const tagName = closingMatch[1];
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += closingMatch[0];
i += closingMatch[0].length;
}
}
if (!isValidTag) {
const openingMatch = str.slice(i).match(/^<(\d+|[a-zA-Z][a-zA-Z0-9_-]*)(\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*(\/)?>/);
if (openingMatch) {
const tagName = openingMatch[1];
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += openingMatch[0];
i += openingMatch[0].length;
}
}
}
if (!isValidTag) {
result += '&lt;';
i += 1;
}
} else {
result += str[i];
i += 1;
}
}
return result;
};
const renderNodes = (children, knownComponentsMap, targetString, i18n, i18nOptions, combinedTOpts, shouldUnescape) => {
if (targetString === '') return [];
const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || [];
const emptyChildrenButNeedsHandling = targetString && new RegExp(keepArray.map(keep => `<${keep}`).join('|')).test(targetString);
if (!children && !knownComponentsMap && !emptyChildrenButNeedsHandling && !shouldUnescape) return [targetString];
const data = knownComponentsMap ?? {};
const getData = childs => {
const childrenArray = getAsArray(childs);
childrenArray.forEach(child => {
if ((0, _utils.isString)(child)) return;
if (hasChildren(child)) getData(getChildren(child));else if ((0, _utils.isObject)(child) && !(0, _react.isValidElement)(child)) Object.assign(data, child);
});
};
getData(children);
const escapedString = escapeLiteralLessThan(targetString, keepArray, data);
const ast = _htmlParseStringify.default.parse(`<0>${escapedString}</0>`);
const opts = {
...data,
...combinedTOpts
};
const renderInner = (child, node, rootReactNode) => {
const childs = getChildren(child);
const mappedChildren = mapAST(childs, node.children, rootReactNode);
return hasValidReactChildren(childs) && mappedChildren.length === 0 || child.props?.i18nIsDynamicList ? childs : mappedChildren;
};
const pushTranslatedJSX = (child, inner, mem, i, isVoid) => {
if (child.dummy) {
child.children = inner;
mem.push((0, _react.cloneElement)(child, {
key: i
}, isVoid ? undefined : inner));
} else {
mem.push(..._react.Children.map([child], c => {
const INTERNAL_DYNAMIC_MARKER = 'data-i18n-is-dynamic-list';
const override = {
key: i,
[INTERNAL_DYNAMIC_MARKER]: undefined
};
if (c && c.props) {
Object.keys(c.props).forEach(k => {
if (k === 'ref' || k === 'children' || k === 'i18nIsDynamicList' || k === INTERNAL_DYNAMIC_MARKER) return;
override[k] = c.props[k];
});
}
return (0, _react.cloneElement)(c, override, isVoid ? null : inner);
}));
}
};
const mapAST = (reactNode, astNode, rootReactNode) => {
const reactNodes = getAsArray(reactNode);
const astNodes = getAsArray(astNode);
return astNodes.reduce((mem, node, i) => {
const translationContent = node.children?.[0]?.content && i18n.services.interpolator.interpolate(node.children[0].content, opts, i18n.language);
if (node.type === 'tag') {
let tmp = reactNodes[parseInt(node.name, 10)];
if (!tmp && knownComponentsMap) tmp = knownComponentsMap[node.name];
if (rootReactNode.length === 1 && !tmp) tmp = rootReactNode[0][node.name];
if (!tmp) tmp = {};
const props = {
...node.attrs
};
if (shouldUnescape) {
Object.keys(props).forEach(p => {
const val = props[p];
if ((0, _utils.isString)(val)) {
props[p] = (0, _unescape.unescape)(val);
}
});
}
const child = Object.keys(props).length !== 0 ? mergeProps({
props
}, tmp) : tmp;
const isElement = (0, _react.isValidElement)(child);
const isValidTranslationWithChildren = isElement && hasChildren(node, true) && !node.voidElement;
const isEmptyTransWithHTML = emptyChildrenButNeedsHandling && (0, _utils.isObject)(child) && child.dummy && !isElement;
const isKnownComponent = (0, _utils.isObject)(knownComponentsMap) && Object.hasOwnProperty.call(knownComponentsMap, node.name);
if ((0, _utils.isString)(child)) {
const value = i18n.services.interpolator.interpolate(child, opts, i18n.language);
mem.push(value);
} else if (hasChildren(child) || isValidTranslationWithChildren) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i);
} else if (isEmptyTransWithHTML) {
const inner = mapAST(reactNodes, node.children, rootReactNode);
pushTranslatedJSX(child, inner, mem, i);
} else if (Number.isNaN(parseFloat(node.name))) {
if (isKnownComponent) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i, node.voidElement);
} else if (i18nOptions.transSupportBasicHtmlNodes && keepArray.indexOf(node.name) > -1) {
if (node.voidElement) {
mem.push((0, _react.createElement)(node.name, {
key: `${node.name}-${i}`
}));
} else {
const inner = mapAST(reactNodes, node.children, rootReactNode);
mem.push((0, _react.createElement)(node.name, {
key: `${node.name}-${i}`
}, inner));
}
} else if (node.voidElement) {
mem.push(`<${node.name} />`);
} else {
const inner = mapAST(reactNodes, node.children, rootReactNode);
mem.push(`<${node.name}>${inner}</${node.name}>`);
}
} else if ((0, _utils.isObject)(child) && !isElement) {
const content = node.children[0] ? translationContent : null;
if (content) mem.push(content);
} else {
pushTranslatedJSX(child, translationContent, mem, i, node.children.length !== 1 || !translationContent);
}
} else if (node.type === 'text') {
const wrapTextNodes = i18nOptions.transWrapTextNodes;
const unescapeFn = typeof i18nOptions.unescape === 'function' ? i18nOptions.unescape : (0, _defaults.getDefaults)().unescape;
const content = shouldUnescape ? unescapeFn(i18n.services.interpolator.interpolate(node.content, opts, i18n.language)) : i18n.services.interpolator.interpolate(node.content, opts, i18n.language);
if (wrapTextNodes) {
mem.push((0, _react.createElement)(wrapTextNodes, {
key: `${node.name}-${i}`
}, content));
} else {
mem.push(content);
}
}
return mem;
}, []);
};
const result = mapAST([{
dummy: true,
children: children || []
}], ast, getAsArray(children || []));
return getChildren(result[0]);
};
const fixComponentProps = (component, index, translation) => {
const componentKey = component.key || index;
const comp = (0, _react.cloneElement)(component, {
key: componentKey
});
if (!comp.props || !comp.props.children || translation.indexOf(`${index}/>`) < 0 && translation.indexOf(`${index} />`) < 0) {
return comp;
}
function Componentized() {
return (0, _react.createElement)(_react.Fragment, null, comp);
}
return (0, _react.createElement)(Componentized, {
key: componentKey
});
};
const generateArrayComponents = (components, translation) => components.map((c, index) => fixComponentProps(c, index, translation));
const generateObjectComponents = (components, translation) => {
const componentMap = {};
Object.keys(components).forEach(c => {
Object.assign(componentMap, {
[c]: fixComponentProps(components[c], c, translation)
});
});
return componentMap;
};
const generateComponents = (components, translation, i18n, i18nKey) => {
if (!components) return null;
if (Array.isArray(components)) {
return generateArrayComponents(components, translation);
}
if ((0, _utils.isObject)(components)) {
return generateObjectComponents(components, translation);
}
(0, _utils.warnOnce)(i18n, 'TRANS_INVALID_COMPONENTS', `<Trans /> "components" prop expects an object or array`, {
i18nKey
});
return null;
};
const isComponentsMap = object => {
if (!(0, _utils.isObject)(object)) return false;
if (Array.isArray(object)) return false;
return Object.keys(object).reduce((acc, key) => acc && Number.isNaN(Number.parseFloat(key)), true);
};
function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const i18n = i18nFromProps || (0, _i18nInstance.getI18n)();
if (!i18n) {
(0, _utils.warnOnce)(i18n, 'NO_I18NEXT_INSTANCE', `Trans: You need to pass in an i18next instance using i18nextReactModule`, {
i18nKey
});
return children;
}
const t = tFromProps || i18n.t.bind(i18n) || (k => k);
const reactI18nextOptions = {
...(0, _defaults.getDefaults)(),
...i18n.options?.react
};
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = (0, _utils.isString)(namespaces) ? [namespaces] : namespaces || ['translation'];
const {
transDefaultProps
} = reactI18nextOptions;
const mergedTOptions = transDefaultProps?.tOptions ? {
...transDefaultProps.tOptions,
...tOptions
} : tOptions;
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
const mergedValues = transDefaultProps?.values ? {
...transDefaultProps.values,
...values
} : values;
const mergedComponents = transDefaultProps?.components ? {
...transDefaultProps.components,
...components
} : components;
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
const defaultValue = defaults || mergedTOptions?.defaultValue || nodeAsString || reactI18nextOptions.transEmptyNodeValue || (typeof i18nKey === 'function' ? (0, _i18next.keyFromSelector)(i18nKey) : i18nKey);
const {
hashTransKey
} = reactI18nextOptions;
const key = i18nKey || (hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue);
if (i18n.options?.interpolation?.defaultVariables) {
values = mergedValues && Object.keys(mergedValues).length > 0 ? {
...mergedValues,
...i18n.options.interpolation.defaultVariables
} : {
...i18n.options.interpolation.defaultVariables
};
} else {
values = mergedValues;
}
const valuesFromChildren = getValuesFromChildren(children);
if (valuesFromChildren && typeof valuesFromChildren.count === 'number' && count === undefined) {
count = valuesFromChildren.count;
}
const interpolationOverride = values || count !== undefined && !i18n.options?.interpolation?.alwaysFormat || !children ? mergedTOptions.interpolation : {
interpolation: {
...mergedTOptions.interpolation,
prefix: '#$?',
suffix: '?$#'
}
};
const combinedTOpts = {
...mergedTOptions,
context: context || mergedTOptions.context,
count,
...values,
...interpolationOverride,
defaultValue,
ns: namespaces
};
let translation = key ? t(key, combinedTOpts) : defaultValue;
if (translation === key && defaultValue) translation = defaultValue;
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
let indexedChildren = generatedComponents || children;
let componentsMap = null;
if (isComponentsMap(generatedComponents)) {
componentsMap = generatedComponents;
indexedChildren = children;
}
const content = renderNodes(indexedChildren, componentsMap, translation, i18n, reactI18nextOptions, combinedTOpts, mergedShouldUnescape);
const useAsParent = parent ?? reactI18nextOptions.defaultTransParent;
return useAsParent ? (0, _react.createElement)(useAsParent, additionalProps, content) : content;
}
+19
View File
@@ -0,0 +1,19 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Translation = void 0;
var _useTranslation = require("./useTranslation.js");
const Translation = ({
ns,
children,
...options
}) => {
const [t, i18n, ready] = (0, _useTranslation.useTranslation)(ns, options);
return children(t, {
i18n,
lng: i18n?.language
}, ready);
};
exports.Translation = Translation;
+85
View File
@@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.composeInitialProps = exports.ReportNamespaces = exports.I18nContext = void 0;
Object.defineProperty(exports, "getDefaults", {
enumerable: true,
get: function () {
return _defaults.getDefaults;
}
});
Object.defineProperty(exports, "getI18n", {
enumerable: true,
get: function () {
return _i18nInstance.getI18n;
}
});
exports.getInitialProps = void 0;
Object.defineProperty(exports, "initReactI18next", {
enumerable: true,
get: function () {
return _initReactI18next.initReactI18next;
}
});
Object.defineProperty(exports, "setDefaults", {
enumerable: true,
get: function () {
return _defaults.setDefaults;
}
});
Object.defineProperty(exports, "setI18n", {
enumerable: true,
get: function () {
return _i18nInstance.setI18n;
}
});
var _react = require("react");
var _defaults = require("./defaults.js");
var _i18nInstance = require("./i18nInstance.js");
var _initReactI18next = require("./initReactI18next.js");
const I18nContext = exports.I18nContext = (0, _react.createContext)();
class ReportNamespaces {
constructor() {
this.usedNamespaces = {};
}
addUsedNamespaces(namespaces) {
namespaces.forEach(ns => {
if (!this.usedNamespaces[ns]) this.usedNamespaces[ns] = true;
});
}
getUsedNamespaces() {
return Object.keys(this.usedNamespaces);
}
}
exports.ReportNamespaces = ReportNamespaces;
const composeInitialProps = ForComponent => async ctx => {
const componentsInitialProps = (await ForComponent.getInitialProps?.(ctx)) ?? {};
const i18nInitialProps = getInitialProps();
return {
...componentsInitialProps,
...i18nInitialProps
};
};
exports.composeInitialProps = composeInitialProps;
const getInitialProps = () => {
const i18n = (0, _i18nInstance.getI18n)();
if (!i18n) {
console.warn('react-i18next:: getInitialProps: You will need to pass in an i18next instance by using initReactI18next');
return {};
}
const namespaces = i18n.reportNamespaces?.getUsedNamespaces() ?? [];
const ret = {};
const initialI18nStore = {};
i18n.languages.forEach(l => {
initialI18nStore[l] = {};
namespaces.forEach(ns => {
initialI18nStore[l][ns] = i18n.getResourceBundle(l, ns) || {};
});
});
ret.initialI18nStore = initialI18nStore;
ret.initialLanguage = i18n.language;
return ret;
};
exports.getInitialProps = getInitialProps;
+27
View File
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.setDefaults = exports.getDefaults = void 0;
var _unescape = require("./unescape.js");
let defaultOptions = {
bindI18n: 'languageChanged',
bindI18nStore: '',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transWrapTextNodes: '',
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
useSuspense: true,
unescape: _unescape.unescape,
transDefaultProps: undefined
};
const setDefaults = (options = {}) => {
defaultOptions = {
...defaultOptions,
...options
};
};
exports.setDefaults = setDefaults;
const getDefaults = () => defaultOptions;
exports.getDefaults = getDefaults;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.setI18n = exports.getI18n = void 0;
let i18nInstance;
const setI18n = instance => {
i18nInstance = instance;
};
exports.setI18n = setI18n;
const getI18n = () => i18nInstance;
exports.getI18n = getI18n;
+148
View File
@@ -0,0 +1,148 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "I18nContext", {
enumerable: true,
get: function () {
return _context.I18nContext;
}
});
Object.defineProperty(exports, "I18nextProvider", {
enumerable: true,
get: function () {
return _I18nextProvider.I18nextProvider;
}
});
Object.defineProperty(exports, "IcuTrans", {
enumerable: true,
get: function () {
return _IcuTrans.IcuTrans;
}
});
Object.defineProperty(exports, "IcuTransWithoutContext", {
enumerable: true,
get: function () {
return _IcuTransWithoutContext.IcuTransWithoutContext;
}
});
Object.defineProperty(exports, "Trans", {
enumerable: true,
get: function () {
return _Trans.Trans;
}
});
Object.defineProperty(exports, "TransWithoutContext", {
enumerable: true,
get: function () {
return _TransWithoutContext.Trans;
}
});
Object.defineProperty(exports, "Translation", {
enumerable: true,
get: function () {
return _Translation.Translation;
}
});
Object.defineProperty(exports, "composeInitialProps", {
enumerable: true,
get: function () {
return _context.composeInitialProps;
}
});
exports.date = void 0;
Object.defineProperty(exports, "getDefaults", {
enumerable: true,
get: function () {
return _defaults.getDefaults;
}
});
Object.defineProperty(exports, "getI18n", {
enumerable: true,
get: function () {
return _i18nInstance.getI18n;
}
});
Object.defineProperty(exports, "getInitialProps", {
enumerable: true,
get: function () {
return _context.getInitialProps;
}
});
Object.defineProperty(exports, "initReactI18next", {
enumerable: true,
get: function () {
return _initReactI18next.initReactI18next;
}
});
Object.defineProperty(exports, "nodesToString", {
enumerable: true,
get: function () {
return _Trans.nodesToString;
}
});
exports.selectOrdinal = exports.select = exports.plural = exports.number = void 0;
Object.defineProperty(exports, "setDefaults", {
enumerable: true,
get: function () {
return _defaults.setDefaults;
}
});
Object.defineProperty(exports, "setI18n", {
enumerable: true,
get: function () {
return _i18nInstance.setI18n;
}
});
exports.time = void 0;
Object.defineProperty(exports, "useSSR", {
enumerable: true,
get: function () {
return _useSSR.useSSR;
}
});
Object.defineProperty(exports, "useTranslation", {
enumerable: true,
get: function () {
return _useTranslation.useTranslation;
}
});
Object.defineProperty(exports, "withSSR", {
enumerable: true,
get: function () {
return _withSSR.withSSR;
}
});
Object.defineProperty(exports, "withTranslation", {
enumerable: true,
get: function () {
return _withTranslation.withTranslation;
}
});
var _Trans = require("./Trans.js");
var _TransWithoutContext = require("./TransWithoutContext.js");
var _IcuTrans = require("./IcuTrans.js");
var _IcuTransWithoutContext = require("./IcuTransWithoutContext.js");
var _useTranslation = require("./useTranslation.js");
var _withTranslation = require("./withTranslation.js");
var _Translation = require("./Translation.js");
var _I18nextProvider = require("./I18nextProvider.js");
var _withSSR = require("./withSSR.js");
var _useSSR = require("./useSSR.js");
var _initReactI18next = require("./initReactI18next.js");
var _defaults = require("./defaults.js");
var _i18nInstance = require("./i18nInstance.js");
var _context = require("./context.js");
const date = () => '';
exports.date = date;
const time = () => '';
exports.time = time;
const number = () => '';
exports.number = number;
const select = () => '';
exports.select = select;
const plural = () => '';
exports.plural = plural;
const selectOrdinal = () => '';
exports.selectOrdinal = selectOrdinal;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.initReactI18next = void 0;
var _defaults = require("./defaults.js");
var _i18nInstance = require("./i18nInstance.js");
const initReactI18next = exports.initReactI18next = {
type: '3rdParty',
init(instance) {
(0, _defaults.setDefaults)(instance.options.react);
(0, _i18nInstance.setI18n)(instance);
}
};
+32
View File
@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.unescape = void 0;
const matchHtmlEntity = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
const htmlEntities = {
'&amp;': '&',
'&#38;': '&',
'&lt;': '<',
'&#60;': '<',
'&gt;': '>',
'&#62;': '>',
'&apos;': "'",
'&#39;': "'",
'&quot;': '"',
'&#34;': '"',
'&nbsp;': ' ',
'&#160;': ' ',
'&copy;': '©',
'&#169;': '©',
'&reg;': '®',
'&#174;': '®',
'&hellip;': '…',
'&#8230;': '…',
'&#x2F;': '/',
'&#47;': '/'
};
const unescapeHtmlEntity = m => htmlEntities[m];
const unescape = text => text.replace(matchHtmlEntity, unescapeHtmlEntity);
exports.unescape = unescape;
+43
View File
@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useSSR = void 0;
var _react = require("react");
var _context = require("./context.js");
var _utils = require("./utils.js");
const useSSR = (initialI18nStore, initialLanguage, props = {}) => {
const {
i18n: i18nFromProps
} = props;
const {
i18n: i18nFromContext
} = (0, _react.useContext)(_context.I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || (0, _context.getI18n)();
if (!i18n) {
(0, _utils.warnOnce)(i18n, 'NO_I18NEXT_INSTANCE', 'useSSR: You will need to pass in an i18next instance by using initReactI18next or by passing it via props or context. In monorepo setups, make sure there is only one instance of react-i18next.');
return;
}
if (i18n.options?.isClone) return;
if (initialI18nStore && !i18n.initializedStoreOnce) {
if (!i18n.services?.resourceStore) {
(0, _utils.warnOnce)(i18n, 'I18N_NOT_INITIALIZED', 'useSSR: i18n instance was found but not initialized (services.resourceStore is missing). Make sure you call i18next.init() before using useSSR — e.g. at module level, not only in getStaticProps/getServerSideProps.');
return;
}
i18n.services.resourceStore.data = initialI18nStore;
i18n.options.ns = Object.values(initialI18nStore).reduce((mem, lngResources) => {
Object.keys(lngResources).forEach(ns => {
if (mem.indexOf(ns) < 0) mem.push(ns);
});
return mem;
}, i18n.options.ns);
i18n.initializedStoreOnce = true;
i18n.isInitialized = true;
}
if (initialLanguage && !i18n.initializedLanguageOnce) {
i18n.changeLanguage(initialLanguage);
i18n.initializedLanguageOnce = true;
}
};
exports.useSSR = useSSR;
+168
View File
@@ -0,0 +1,168 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useTranslation = void 0;
var _react = require("react");
var _shim = require("use-sync-external-store/shim");
var _context = require("./context.js");
var _utils = require("./utils.js");
const notReadyT = (k, optsOrDefaultValue) => {
if ((0, _utils.isString)(optsOrDefaultValue)) return optsOrDefaultValue;
if ((0, _utils.isObject)(optsOrDefaultValue) && (0, _utils.isString)(optsOrDefaultValue.defaultValue)) return optsOrDefaultValue.defaultValue;
if (typeof k === 'function') return '';
if (Array.isArray(k)) {
const last = k[k.length - 1];
return typeof last === 'function' ? '' : last;
}
return k;
};
const notReadySnapshot = {
t: notReadyT,
ready: false
};
const dummySubscribe = () => () => {};
const useTranslation = (ns, props = {}) => {
const {
i18n: i18nFromProps
} = props;
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = (0, _react.useContext)(_context.I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || (0, _context.getI18n)();
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new _context.ReportNamespaces();
if (!i18n) {
(0, _utils.warnOnce)(i18n, 'NO_I18NEXT_INSTANCE', 'useTranslation: You will need to pass in an i18next instance by using initReactI18next');
}
const i18nOptions = (0, _react.useMemo)(() => ({
...(0, _context.getDefaults)(),
...i18n?.options?.react,
...props
}), [i18n, props]);
const {
useSuspense,
keyPrefix
} = i18nOptions;
const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS;
const unstableNamespaces = (0, _utils.isString)(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation'];
const namespaces = (0, _react.useMemo)(() => unstableNamespaces, unstableNamespaces);
i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
const revisionRef = (0, _react.useRef)(0);
const subscribe = (0, _react.useCallback)(callback => {
if (!i18n) return dummySubscribe;
const {
bindI18n,
bindI18nStore
} = i18nOptions;
const wrappedCallback = () => {
revisionRef.current += 1;
callback();
};
if (bindI18n) i18n.on(bindI18n, wrappedCallback);
if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
return () => {
if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, wrappedCallback));
if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, wrappedCallback));
};
}, [i18n, i18nOptions]);
const snapshotRef = (0, _react.useRef)();
const getSnapshot = (0, _react.useCallback)(() => {
if (!i18n) {
return notReadySnapshot;
}
const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every(n => (0, _utils.hasLoadedNamespace)(n, i18n, i18nOptions));
const currentLng = props.lng || i18n.language;
const currentRevision = revisionRef.current;
const lastSnapshot = snapshotRef.current;
if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) {
return lastSnapshot;
}
const calculatedT = i18n.getFixedT(currentLng, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix);
const newSnapshot = {
t: calculatedT,
ready: calculatedReady,
lng: currentLng,
keyPrefix,
revision: currentRevision
};
snapshotRef.current = newSnapshot;
return newSnapshot;
}, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]);
const [loadCount, setLoadCount] = (0, _react.useState)(0);
const {
t,
ready
} = (0, _shim.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
(0, _react.useEffect)(() => {
if (i18n && !ready && !useSuspense) {
const onLoaded = () => setLoadCount(c => c + 1);
if (props.lng) {
(0, _utils.loadLanguages)(i18n, props.lng, namespaces, onLoaded);
} else {
(0, _utils.loadNamespaces)(i18n, namespaces, onLoaded);
}
}
}, [i18n, props.lng, namespaces, ready, useSuspense, loadCount]);
const finalI18n = i18n || {};
const wrapperRef = (0, _react.useRef)(null);
const wrapperLangRef = (0, _react.useRef)();
const createI18nWrapper = original => {
const descriptors = Object.getOwnPropertyDescriptors(original);
if (descriptors.__original) delete descriptors.__original;
const wrapper = Object.create(Object.getPrototypeOf(original), descriptors);
if (!Object.prototype.hasOwnProperty.call(wrapper, '__original')) {
try {
Object.defineProperty(wrapper, '__original', {
value: original,
writable: false,
enumerable: false,
configurable: false
});
} catch (_) {}
}
return wrapper;
};
const ret = (0, _react.useMemo)(() => {
const original = finalI18n;
const lang = original?.language;
let i18nWrapper = original;
if (original) {
if (wrapperRef.current && wrapperRef.current.__original === original) {
if (wrapperLangRef.current !== lang) {
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
} else {
i18nWrapper = wrapperRef.current;
}
} else {
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
}
}
const effectiveT = !ready && !useSuspense ? (...args) => {
(0, _utils.warnOnce)(i18n, 'USE_T_BEFORE_READY', 'useTranslation: t was called before ready. When using useSuspense: false, make sure to check the ready flag before using t.');
return t(...args);
} : t;
const arr = [effectiveT, i18nWrapper, ready];
arr.t = effectiveT;
arr.i18n = i18nWrapper;
arr.ready = ready;
return arr;
}, [t, finalI18n, ready, finalI18n.resolvedLanguage, finalI18n.language, finalI18n.languages]);
if (i18n && useSuspense && !ready) {
throw new Promise(resolve => {
const onLoaded = () => resolve();
if (props.lng) {
(0, _utils.loadLanguages)(i18n, props.lng, namespaces, onLoaded);
} else {
(0, _utils.loadNamespaces)(i18n, namespaces, onLoaded);
}
});
}
return ret;
};
exports.useTranslation = useTranslation;
+76
View File
@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.warnOnce = exports.warn = exports.loadNamespaces = exports.loadLanguages = exports.isString = exports.isObject = exports.hasLoadedNamespace = exports.getDisplayName = void 0;
const warn = (i18n, code, msg, rest) => {
const args = [msg, {
code,
...(rest || {})
}];
if (i18n?.services?.logger?.forward) {
return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
}
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
if (i18n?.services?.logger?.warn) {
i18n.services.logger.warn(...args);
} else if (console?.warn) {
console.warn(...args);
}
};
exports.warn = warn;
const alreadyWarned = {};
const warnOnce = (i18n, code, msg, rest) => {
if (isString(msg) && alreadyWarned[msg]) return;
if (isString(msg)) alreadyWarned[msg] = new Date();
warn(i18n, code, msg, rest);
};
exports.warnOnce = warnOnce;
const loadedClb = (i18n, cb) => () => {
if (i18n.isInitialized) {
cb();
} else {
const initialized = () => {
setTimeout(() => {
i18n.off('initialized', initialized);
}, 0);
cb();
};
i18n.on('initialized', initialized);
}
};
const loadNamespaces = (i18n, ns, cb) => {
i18n.loadNamespaces(ns, loadedClb(i18n, cb));
};
exports.loadNamespaces = loadNamespaces;
const loadLanguages = (i18n, lng, ns, cb) => {
if (isString(ns)) ns = [ns];
if (i18n.options.preload && i18n.options.preload.indexOf(lng) > -1) return loadNamespaces(i18n, ns, cb);
ns.forEach(n => {
if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
});
i18n.loadLanguages(lng, loadedClb(i18n, cb));
};
exports.loadLanguages = loadLanguages;
const hasLoadedNamespace = (ns, i18n, options = {}) => {
if (!i18n.languages || !i18n.languages.length) {
warnOnce(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
languages: i18n.languages
});
return true;
}
return i18n.hasLoadedNamespace(ns, {
lng: options.lng,
precheck: (i18nInstance, loadNotPending) => {
if (options.bindI18n && options.bindI18n.indexOf('languageChanging') > -1 && i18nInstance.services.backendConnector.backend && i18nInstance.isLanguageChangingTo && !loadNotPending(i18nInstance.isLanguageChangingTo, ns)) return false;
}
});
};
exports.hasLoadedNamespace = hasLoadedNamespace;
const getDisplayName = Component => Component.displayName || Component.name || (isString(Component) && Component.length > 0 ? Component : 'Unknown');
exports.getDisplayName = getDisplayName;
const isString = obj => typeof obj === 'string';
exports.isString = isString;
const isObject = obj => typeof obj === 'object' && obj !== null;
exports.isObject = isObject;
+27
View File
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.withSSR = void 0;
var _react = require("react");
var _useSSR = require("./useSSR.js");
var _context = require("./context.js");
var _utils = require("./utils.js");
const withSSR = () => function Extend(WrappedComponent) {
function I18nextWithSSR({
initialI18nStore,
initialLanguage,
...rest
}) {
(0, _useSSR.useSSR)(initialI18nStore, initialLanguage);
return (0, _react.createElement)(WrappedComponent, {
...rest
});
}
I18nextWithSSR.getInitialProps = (0, _context.composeInitialProps)(WrappedComponent);
I18nextWithSSR.displayName = `withI18nextSSR(${(0, _utils.getDisplayName)(WrappedComponent)})`;
I18nextWithSSR.WrappedComponent = WrappedComponent;
return I18nextWithSSR;
};
exports.withSSR = withSSR;
+39
View File
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.withTranslation = void 0;
var _react = require("react");
var _useTranslation = require("./useTranslation.js");
var _utils = require("./utils.js");
const withTranslation = (ns, options = {}) => function Extend(WrappedComponent) {
function I18nextWithTranslation({
forwardedRef,
...rest
}) {
const [t, i18n, ready] = (0, _useTranslation.useTranslation)(ns, {
...rest,
keyPrefix: options.keyPrefix
});
const passDownProps = {
...rest,
t,
i18n,
tReady: ready
};
if (options.withRef && forwardedRef) {
passDownProps.ref = forwardedRef;
} else if (!options.withRef && forwardedRef) {
passDownProps.forwardedRef = forwardedRef;
}
return (0, _react.createElement)(WrappedComponent, passDownProps);
}
I18nextWithTranslation.displayName = `withI18nextTranslation(${(0, _utils.getDisplayName)(WrappedComponent)})`;
I18nextWithTranslation.WrappedComponent = WrappedComponent;
const forwardRef = (props, ref) => (0, _react.createElement)(I18nextWithTranslation, Object.assign({}, props, {
forwardedRef: ref
}));
return options.withRef ? (0, _react.forwardRef)(forwardRef) : I18nextWithTranslation;
};
exports.withTranslation = withTranslation;
+15
View File
@@ -0,0 +1,15 @@
import { createElement, useMemo } from 'react';
import { I18nContext } from './context.js';
export function I18nextProvider({
i18n,
defaultNS,
children
}) {
const value = useMemo(() => ({
i18n,
defaultNS
}), [i18n, defaultNS]);
return createElement(I18nContext.Provider, {
value
}, children);
}
+29
View File
@@ -0,0 +1,29 @@
import { useContext } from 'react';
import { IcuTransWithoutContext } from './IcuTransWithoutContext.js';
import { getI18n, I18nContext } from './context.js';
export function IcuTrans({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps
}) {
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
const t = tFromProps || i18n?.t.bind(i18n);
return IcuTransWithoutContext({
i18nKey,
defaultTranslation,
content,
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
values,
i18n,
t: tFromProps
});
}
IcuTrans.displayName = 'IcuTrans';
@@ -0,0 +1,11 @@
export class TranslationParserError extends Error {
constructor(message, position, translationString) {
super(message);
this.name = 'TranslationParserError';
this.position = position;
this.translationString = translationString;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TranslationParserError);
}
}
}
+211
View File
@@ -0,0 +1,211 @@
const commonEntities = {
'&nbsp;': '\u00A0',
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&copy;': '©',
'&reg;': '®',
'&trade;': '™',
'&hellip;': '…',
'&ndash;': '–',
'&mdash;': '—',
'&lsquo;': '\u2018',
'&rsquo;': '\u2019',
'&sbquo;': '\u201A',
'&ldquo;': '\u201C',
'&rdquo;': '\u201D',
'&bdquo;': '\u201E',
'&dagger;': '†',
'&Dagger;': '‡',
'&bull;': '•',
'&prime;': '′',
'&Prime;': '″',
'&lsaquo;': '‹',
'&rsaquo;': '›',
'&sect;': '§',
'&para;': '¶',
'&middot;': '·',
'&ensp;': '\u2002',
'&emsp;': '\u2003',
'&thinsp;': '\u2009',
'&euro;': '€',
'&pound;': '£',
'&yen;': '¥',
'&cent;': '¢',
'&curren;': '¤',
'&times;': '×',
'&divide;': '÷',
'&minus;': '−',
'&plusmn;': '±',
'&ne;': '≠',
'&le;': '≤',
'&ge;': '≥',
'&asymp;': '≈',
'&equiv;': '≡',
'&infin;': '∞',
'&int;': '∫',
'&sum;': '∑',
'&prod;': '∏',
'&radic;': '√',
'&part;': '∂',
'&permil;': '‰',
'&deg;': '°',
'&micro;': 'µ',
'&larr;': '←',
'&uarr;': '↑',
'&rarr;': '→',
'&darr;': '↓',
'&harr;': '↔',
'&crarr;': '↵',
'&lArr;': '⇐',
'&uArr;': '⇑',
'&rArr;': '⇒',
'&dArr;': '⇓',
'&hArr;': '⇔',
'&alpha;': 'α',
'&beta;': 'β',
'&gamma;': 'γ',
'&delta;': 'δ',
'&epsilon;': 'ε',
'&zeta;': 'ζ',
'&eta;': 'η',
'&theta;': 'θ',
'&iota;': 'ι',
'&kappa;': 'κ',
'&lambda;': 'λ',
'&mu;': 'μ',
'&nu;': 'ν',
'&xi;': 'ξ',
'&omicron;': 'ο',
'&pi;': 'π',
'&rho;': 'ρ',
'&sigma;': 'σ',
'&tau;': 'τ',
'&upsilon;': 'υ',
'&phi;': 'φ',
'&chi;': 'χ',
'&psi;': 'ψ',
'&omega;': 'ω',
'&Alpha;': 'Α',
'&Beta;': 'Β',
'&Gamma;': 'Γ',
'&Delta;': 'Δ',
'&Epsilon;': 'Ε',
'&Zeta;': 'Ζ',
'&Eta;': 'Η',
'&Theta;': 'Θ',
'&Iota;': 'Ι',
'&Kappa;': 'Κ',
'&Lambda;': 'Λ',
'&Mu;': 'Μ',
'&Nu;': 'Ν',
'&Xi;': 'Ξ',
'&Omicron;': 'Ο',
'&Pi;': 'Π',
'&Rho;': 'Ρ',
'&Sigma;': 'Σ',
'&Tau;': 'Τ',
'&Upsilon;': 'Υ',
'&Phi;': 'Φ',
'&Chi;': 'Χ',
'&Psi;': 'Ψ',
'&Omega;': 'Ω',
'&Agrave;': 'À',
'&Aacute;': 'Á',
'&Acirc;': 'Â',
'&Atilde;': 'Ã',
'&Auml;': 'Ä',
'&Aring;': 'Å',
'&AElig;': 'Æ',
'&Ccedil;': 'Ç',
'&Egrave;': 'È',
'&Eacute;': 'É',
'&Ecirc;': 'Ê',
'&Euml;': 'Ë',
'&Igrave;': 'Ì',
'&Iacute;': 'Í',
'&Icirc;': 'Î',
'&Iuml;': 'Ï',
'&ETH;': 'Ð',
'&Ntilde;': 'Ñ',
'&Ograve;': 'Ò',
'&Oacute;': 'Ó',
'&Ocirc;': 'Ô',
'&Otilde;': 'Õ',
'&Ouml;': 'Ö',
'&Oslash;': 'Ø',
'&Ugrave;': 'Ù',
'&Uacute;': 'Ú',
'&Ucirc;': 'Û',
'&Uuml;': 'Ü',
'&Yacute;': 'Ý',
'&THORN;': 'Þ',
'&szlig;': 'ß',
'&agrave;': 'à',
'&aacute;': 'á',
'&acirc;': 'â',
'&atilde;': 'ã',
'&auml;': 'ä',
'&aring;': 'å',
'&aelig;': 'æ',
'&ccedil;': 'ç',
'&egrave;': 'è',
'&eacute;': 'é',
'&ecirc;': 'ê',
'&euml;': 'ë',
'&igrave;': 'ì',
'&iacute;': 'í',
'&icirc;': 'î',
'&iuml;': 'ï',
'&eth;': 'ð',
'&ntilde;': 'ñ',
'&ograve;': 'ò',
'&oacute;': 'ó',
'&ocirc;': 'ô',
'&otilde;': 'õ',
'&ouml;': 'ö',
'&oslash;': 'ø',
'&ugrave;': 'ù',
'&uacute;': 'ú',
'&ucirc;': 'û',
'&uuml;': 'ü',
'&yacute;': 'ý',
'&thorn;': 'þ',
'&yuml;': 'ÿ',
'&iexcl;': '¡',
'&iquest;': '¿',
'&fnof;': 'ƒ',
'&circ;': 'ˆ',
'&tilde;': '˜',
'&OElig;': 'Œ',
'&oelig;': 'œ',
'&Scaron;': 'Š',
'&scaron;': 'š',
'&Yuml;': 'Ÿ',
'&ordf;': 'ª',
'&ordm;': 'º',
'&macr;': '¯',
'&acute;': '´',
'&cedil;': '¸',
'&sup1;': '¹',
'&sup2;': '²',
'&sup3;': '³',
'&frac14;': '¼',
'&frac12;': '½',
'&frac34;': '¾',
'&spades;': '♠',
'&clubs;': '♣',
'&hearts;': '♥',
'&diams;': '♦',
'&loz;': '◊',
'&oline;': '‾',
'&frasl;': '⁄',
'&weierp;': '℘',
'&image;': 'ℑ',
'&real;': 'ℜ',
'&alefsym;': 'ℵ'
};
const entityPattern = new RegExp(Object.keys(commonEntities).map(entity => entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
export const decodeHtmlEntities = text => text.replace(entityPattern, match => commonEntities[match]).replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10))).replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
+4
View File
@@ -0,0 +1,4 @@
export * from './TranslationParserError.js';
export * from './htmlEntityDecoder.js';
export * from './tokenizer.js';
export * from './renderTranslation.js';
+106
View File
@@ -0,0 +1,106 @@
import React from 'react';
import { TranslationParserError } from './TranslationParserError.js';
import { tokenize } from './tokenizer.js';
import { decodeHtmlEntities } from './htmlEntityDecoder.js';
const renderDeclarationNode = (declaration, children, childDeclarations) => {
const {
type,
props = {}
} = declaration;
if (props.children && Array.isArray(props.children) && childDeclarations) {
const {
children: _childrenToRemove,
...propsWithoutChildren
} = props;
return React.createElement(type, propsWithoutChildren, ...children);
}
if (children.length === 0) {
return React.createElement(type, props);
}
if (children.length === 1) {
return React.createElement(type, props, children[0]);
}
return React.createElement(type, props, ...children);
};
export const renderTranslation = (translation, declarations = []) => {
if (!translation) {
return [];
}
const tokens = tokenize(translation);
const result = [];
const stack = [];
const literalTagNumbers = new Set();
const getCurrentDeclarations = () => {
if (stack.length === 0) {
return declarations;
}
const parentFrame = stack[stack.length - 1];
if (parentFrame.declaration.props?.children && Array.isArray(parentFrame.declaration.props.children)) {
return parentFrame.declaration.props.children;
}
return parentFrame.declarations;
};
tokens.forEach(token => {
switch (token.type) {
case 'Text':
{
const decoded = decodeHtmlEntities(token.value);
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(decoded);
}
break;
case 'TagOpen':
{
const {
tagNumber
} = token;
const currentDeclarations = getCurrentDeclarations();
const declaration = currentDeclarations[tagNumber];
if (!declaration) {
literalTagNumbers.add(tagNumber);
const literalText = `<${tagNumber}>`;
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(literalText);
break;
}
stack.push({
tagNumber,
children: [],
position: token.position,
declaration,
declarations: currentDeclarations
});
}
break;
case 'TagClose':
{
const {
tagNumber
} = token;
if (literalTagNumbers.has(tagNumber)) {
const literalText = `</${tagNumber}>`;
const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
literalTargetArray.push(literalText);
literalTagNumbers.delete(tagNumber);
break;
}
if (stack.length === 0) {
throw new TranslationParserError(`Unexpected closing tag </${tagNumber}> at position ${token.position}`, token.position, translation);
}
const frame = stack.pop();
if (frame.tagNumber !== tagNumber) {
throw new TranslationParserError(`Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`, token.position, translation);
}
const element = renderDeclarationNode(frame.declaration, frame.children, frame.declarations);
const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
elementTargetArray.push(element);
}
break;
}
});
if (stack.length > 0) {
const unclosed = stack[stack.length - 1];
throw new TranslationParserError(`Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`, unclosed.position, translation);
}
return result;
};
+51
View File
@@ -0,0 +1,51 @@
export const tokenize = translation => {
const tokens = [];
let position = 0;
let currentText = '';
const flushText = () => {
if (currentText) {
tokens.push({
type: 'Text',
value: currentText,
position: position - currentText.length
});
currentText = '';
}
};
while (position < translation.length) {
const char = translation[position];
if (char === '<') {
const tagMatch = translation.slice(position).match(/^<(\d+)>/);
if (tagMatch) {
flushText();
tokens.push({
type: 'TagOpen',
value: tagMatch[0],
position,
tagNumber: parseInt(tagMatch[1], 10)
});
position += tagMatch[0].length;
} else {
const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
if (closeTagMatch) {
flushText();
tokens.push({
type: 'TagClose',
value: closeTagMatch[0],
position,
tagNumber: parseInt(closeTagMatch[1], 10)
});
position += closeTagMatch[0].length;
} else {
currentText += char;
position += 1;
}
}
} else {
currentText += char;
position += 1;
}
}
flushText();
return tokens;
};
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import { warn, warnOnce, isString } from './utils.js';
import { getI18n } from './i18nInstance.js';
import { renderTranslation } from './IcuTransUtils/index.js';
export function IcuTransWithoutContext({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps
}) {
const i18n = i18nFromProps || getI18n();
if (!i18n) {
warnOnce(i18n, 'NO_I18NEXT_INSTANCE', `IcuTrans: You need to pass in an i18next instance using i18nextReactModule`, {
i18nKey
});
return React.createElement(React.Fragment, {}, defaultTranslation);
}
const t = tFromProps || i18n.t?.bind(i18n) || (k => k);
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
let mergedValues = values;
if (i18n.options?.interpolation?.defaultVariables) {
mergedValues = values && Object.keys(values).length > 0 ? {
...values,
...i18n.options.interpolation.defaultVariables
} : {
...i18n.options.interpolation.defaultVariables
};
}
const translation = t(i18nKey, {
defaultValue: defaultTranslation,
...mergedValues,
ns: namespaces
});
try {
const rendered = renderTranslation(translation, content);
return React.createElement(React.Fragment, {}, ...rendered);
} catch (error) {
warn(i18n, 'ICU_TRANS_RENDER_ERROR', `IcuTrans component error for key "${i18nKey}": ${error.message}`, {
i18nKey,
error
});
return React.createElement(React.Fragment, {}, translation);
}
}
IcuTransWithoutContext.displayName = 'IcuTransWithoutContext';
+43
View File
@@ -0,0 +1,43 @@
import { useContext } from 'react';
import { nodesToString, Trans as TransWithoutContext } from './TransWithoutContext.js';
import { getI18n, I18nContext } from './context.js';
export { nodesToString };
export function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
const t = tFromProps || i18n?.t.bind(i18n);
return TransWithoutContext({
children,
count,
parent,
i18nKey,
context,
tOptions,
values,
defaults,
components,
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
i18n,
t: tFromProps,
shouldUnescape,
...additionalProps
});
}
+419
View File
@@ -0,0 +1,419 @@
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react';
import { keyFromSelector } from 'i18next';
import HTML from 'html-parse-stringify';
import { isObject, isString, warn, warnOnce } from './utils.js';
import { getDefaults } from './defaults.js';
import { getI18n } from './i18nInstance.js';
import { unescape } from './unescape.js';
const hasChildren = (node, checkLength) => {
if (!node) return false;
const base = node.props?.children ?? node.children;
if (checkLength) return base.length > 0;
return !!base;
};
const getChildren = node => {
if (!node) return [];
const children = node.props?.children ?? node.children;
return node.props?.i18nIsDynamicList ? getAsArray(children) : children;
};
const hasValidReactChildren = children => Array.isArray(children) && children.every(isValidElement);
const getAsArray = data => Array.isArray(data) ? data : [data];
const mergeProps = (source, target) => {
const newTarget = {
...target
};
newTarget.props = {
...target.props,
...source.props
};
return newTarget;
};
const getValuesFromChildren = children => {
const values = {};
if (!children) return values;
const getData = childs => {
const childrenArray = getAsArray(childs);
childrenArray.forEach(child => {
if (isString(child)) return;
if (hasChildren(child)) getData(getChildren(child));else if (isObject(child) && !isValidElement(child)) Object.assign(values, child);
});
};
getData(children);
return values;
};
export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
if (!children) return '';
let stringNode = '';
const childrenArray = getAsArray(children);
const keepArray = i18nOptions?.transSupportBasicHtmlNodes ? i18nOptions.transKeepBasicHtmlNodesFor ?? [] : [];
childrenArray.forEach((child, childIndex) => {
if (isString(child)) {
stringNode += `${child}`;
return;
}
if (isValidElement(child)) {
const {
props,
type
} = child;
const childPropsCount = Object.keys(props).length;
const shouldKeepChild = keepArray.indexOf(type) > -1;
const childChildren = props.children;
if (!childChildren && shouldKeepChild && !childPropsCount) {
stringNode += `<${type}/>`;
return;
}
if (!childChildren && (!shouldKeepChild || childPropsCount) || props.i18nIsDynamicList) {
stringNode += `<${childIndex}></${childIndex}>`;
return;
}
if (shouldKeepChild && childPropsCount <= 1) {
const cnt = isString(childChildren) ? childChildren : nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${type}>${cnt}</${type}>`;
return;
}
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
return;
}
if (child === null) {
warn(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, {
i18nKey
});
return;
}
if (isObject(child)) {
const {
format,
...clone
} = child;
const keys = Object.keys(clone);
if (keys.length === 1) {
const value = format ? `${keys[0]}, ${format}` : keys[0];
stringNode += `{{${value}}}`;
return;
}
warn(i18n, 'TRANS_INVALID_OBJ', `Invalid child - Object should only have keys {{ value, format }} (format is optional).`, {
i18nKey,
child
});
return;
}
warn(i18n, 'TRANS_INVALID_VAR', `Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`, {
i18nKey,
child
});
});
return stringNode;
};
const escapeLiteralLessThan = (str, keepArray = [], knownComponentsMap = {}) => {
if (!str) return str;
const knownNames = Object.keys(knownComponentsMap);
const allValidNames = [...keepArray, ...knownNames];
let result = '';
let i = 0;
while (i < str.length) {
if (str[i] === '<') {
let isValidTag = false;
const closingMatch = str.slice(i).match(/^<\/(\d+|[a-zA-Z][a-zA-Z0-9_-]*)>/);
if (closingMatch) {
const tagName = closingMatch[1];
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += closingMatch[0];
i += closingMatch[0].length;
}
}
if (!isValidTag) {
const openingMatch = str.slice(i).match(/^<(\d+|[a-zA-Z][a-zA-Z0-9_-]*)(\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*(\/)?>/);
if (openingMatch) {
const tagName = openingMatch[1];
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += openingMatch[0];
i += openingMatch[0].length;
}
}
}
if (!isValidTag) {
result += '&lt;';
i += 1;
}
} else {
result += str[i];
i += 1;
}
}
return result;
};
const renderNodes = (children, knownComponentsMap, targetString, i18n, i18nOptions, combinedTOpts, shouldUnescape) => {
if (targetString === '') return [];
const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || [];
const emptyChildrenButNeedsHandling = targetString && new RegExp(keepArray.map(keep => `<${keep}`).join('|')).test(targetString);
if (!children && !knownComponentsMap && !emptyChildrenButNeedsHandling && !shouldUnescape) return [targetString];
const data = knownComponentsMap ?? {};
const getData = childs => {
const childrenArray = getAsArray(childs);
childrenArray.forEach(child => {
if (isString(child)) return;
if (hasChildren(child)) getData(getChildren(child));else if (isObject(child) && !isValidElement(child)) Object.assign(data, child);
});
};
getData(children);
const escapedString = escapeLiteralLessThan(targetString, keepArray, data);
const ast = HTML.parse(`<0>${escapedString}</0>`);
const opts = {
...data,
...combinedTOpts
};
const renderInner = (child, node, rootReactNode) => {
const childs = getChildren(child);
const mappedChildren = mapAST(childs, node.children, rootReactNode);
return hasValidReactChildren(childs) && mappedChildren.length === 0 || child.props?.i18nIsDynamicList ? childs : mappedChildren;
};
const pushTranslatedJSX = (child, inner, mem, i, isVoid) => {
if (child.dummy) {
child.children = inner;
mem.push(cloneElement(child, {
key: i
}, isVoid ? undefined : inner));
} else {
mem.push(...Children.map([child], c => {
const INTERNAL_DYNAMIC_MARKER = 'data-i18n-is-dynamic-list';
const override = {
key: i,
[INTERNAL_DYNAMIC_MARKER]: undefined
};
if (c && c.props) {
Object.keys(c.props).forEach(k => {
if (k === 'ref' || k === 'children' || k === 'i18nIsDynamicList' || k === INTERNAL_DYNAMIC_MARKER) return;
override[k] = c.props[k];
});
}
return cloneElement(c, override, isVoid ? null : inner);
}));
}
};
const mapAST = (reactNode, astNode, rootReactNode) => {
const reactNodes = getAsArray(reactNode);
const astNodes = getAsArray(astNode);
return astNodes.reduce((mem, node, i) => {
const translationContent = node.children?.[0]?.content && i18n.services.interpolator.interpolate(node.children[0].content, opts, i18n.language);
if (node.type === 'tag') {
let tmp = reactNodes[parseInt(node.name, 10)];
if (!tmp && knownComponentsMap) tmp = knownComponentsMap[node.name];
if (rootReactNode.length === 1 && !tmp) tmp = rootReactNode[0][node.name];
if (!tmp) tmp = {};
const props = {
...node.attrs
};
if (shouldUnescape) {
Object.keys(props).forEach(p => {
const val = props[p];
if (isString(val)) {
props[p] = unescape(val);
}
});
}
const child = Object.keys(props).length !== 0 ? mergeProps({
props
}, tmp) : tmp;
const isElement = isValidElement(child);
const isValidTranslationWithChildren = isElement && hasChildren(node, true) && !node.voidElement;
const isEmptyTransWithHTML = emptyChildrenButNeedsHandling && isObject(child) && child.dummy && !isElement;
const isKnownComponent = isObject(knownComponentsMap) && Object.hasOwnProperty.call(knownComponentsMap, node.name);
if (isString(child)) {
const value = i18n.services.interpolator.interpolate(child, opts, i18n.language);
mem.push(value);
} else if (hasChildren(child) || isValidTranslationWithChildren) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i);
} else if (isEmptyTransWithHTML) {
const inner = mapAST(reactNodes, node.children, rootReactNode);
pushTranslatedJSX(child, inner, mem, i);
} else if (Number.isNaN(parseFloat(node.name))) {
if (isKnownComponent) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i, node.voidElement);
} else if (i18nOptions.transSupportBasicHtmlNodes && keepArray.indexOf(node.name) > -1) {
if (node.voidElement) {
mem.push(createElement(node.name, {
key: `${node.name}-${i}`
}));
} else {
const inner = mapAST(reactNodes, node.children, rootReactNode);
mem.push(createElement(node.name, {
key: `${node.name}-${i}`
}, inner));
}
} else if (node.voidElement) {
mem.push(`<${node.name} />`);
} else {
const inner = mapAST(reactNodes, node.children, rootReactNode);
mem.push(`<${node.name}>${inner}</${node.name}>`);
}
} else if (isObject(child) && !isElement) {
const content = node.children[0] ? translationContent : null;
if (content) mem.push(content);
} else {
pushTranslatedJSX(child, translationContent, mem, i, node.children.length !== 1 || !translationContent);
}
} else if (node.type === 'text') {
const wrapTextNodes = i18nOptions.transWrapTextNodes;
const unescapeFn = typeof i18nOptions.unescape === 'function' ? i18nOptions.unescape : getDefaults().unescape;
const content = shouldUnescape ? unescapeFn(i18n.services.interpolator.interpolate(node.content, opts, i18n.language)) : i18n.services.interpolator.interpolate(node.content, opts, i18n.language);
if (wrapTextNodes) {
mem.push(createElement(wrapTextNodes, {
key: `${node.name}-${i}`
}, content));
} else {
mem.push(content);
}
}
return mem;
}, []);
};
const result = mapAST([{
dummy: true,
children: children || []
}], ast, getAsArray(children || []));
return getChildren(result[0]);
};
const fixComponentProps = (component, index, translation) => {
const componentKey = component.key || index;
const comp = cloneElement(component, {
key: componentKey
});
if (!comp.props || !comp.props.children || translation.indexOf(`${index}/>`) < 0 && translation.indexOf(`${index} />`) < 0) {
return comp;
}
function Componentized() {
return createElement(Fragment, null, comp);
}
return createElement(Componentized, {
key: componentKey
});
};
const generateArrayComponents = (components, translation) => components.map((c, index) => fixComponentProps(c, index, translation));
const generateObjectComponents = (components, translation) => {
const componentMap = {};
Object.keys(components).forEach(c => {
Object.assign(componentMap, {
[c]: fixComponentProps(components[c], c, translation)
});
});
return componentMap;
};
const generateComponents = (components, translation, i18n, i18nKey) => {
if (!components) return null;
if (Array.isArray(components)) {
return generateArrayComponents(components, translation);
}
if (isObject(components)) {
return generateObjectComponents(components, translation);
}
warnOnce(i18n, 'TRANS_INVALID_COMPONENTS', `<Trans /> "components" prop expects an object or array`, {
i18nKey
});
return null;
};
const isComponentsMap = object => {
if (!isObject(object)) return false;
if (Array.isArray(object)) return false;
return Object.keys(object).reduce((acc, key) => acc && Number.isNaN(Number.parseFloat(key)), true);
};
export function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const i18n = i18nFromProps || getI18n();
if (!i18n) {
warnOnce(i18n, 'NO_I18NEXT_INSTANCE', `Trans: You need to pass in an i18next instance using i18nextReactModule`, {
i18nKey
});
return children;
}
const t = tFromProps || i18n.t.bind(i18n) || (k => k);
const reactI18nextOptions = {
...getDefaults(),
...i18n.options?.react
};
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
const {
transDefaultProps
} = reactI18nextOptions;
const mergedTOptions = transDefaultProps?.tOptions ? {
...transDefaultProps.tOptions,
...tOptions
} : tOptions;
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
const mergedValues = transDefaultProps?.values ? {
...transDefaultProps.values,
...values
} : values;
const mergedComponents = transDefaultProps?.components ? {
...transDefaultProps.components,
...components
} : components;
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
const defaultValue = defaults || mergedTOptions?.defaultValue || nodeAsString || reactI18nextOptions.transEmptyNodeValue || (typeof i18nKey === 'function' ? keyFromSelector(i18nKey) : i18nKey);
const {
hashTransKey
} = reactI18nextOptions;
const key = i18nKey || (hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue);
if (i18n.options?.interpolation?.defaultVariables) {
values = mergedValues && Object.keys(mergedValues).length > 0 ? {
...mergedValues,
...i18n.options.interpolation.defaultVariables
} : {
...i18n.options.interpolation.defaultVariables
};
} else {
values = mergedValues;
}
const valuesFromChildren = getValuesFromChildren(children);
if (valuesFromChildren && typeof valuesFromChildren.count === 'number' && count === undefined) {
count = valuesFromChildren.count;
}
const interpolationOverride = values || count !== undefined && !i18n.options?.interpolation?.alwaysFormat || !children ? mergedTOptions.interpolation : {
interpolation: {
...mergedTOptions.interpolation,
prefix: '#$?',
suffix: '?$#'
}
};
const combinedTOpts = {
...mergedTOptions,
context: context || mergedTOptions.context,
count,
...values,
...interpolationOverride,
defaultValue,
ns: namespaces
};
let translation = key ? t(key, combinedTOpts) : defaultValue;
if (translation === key && defaultValue) translation = defaultValue;
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
let indexedChildren = generatedComponents || children;
let componentsMap = null;
if (isComponentsMap(generatedComponents)) {
componentsMap = generatedComponents;
indexedChildren = children;
}
const content = renderNodes(indexedChildren, componentsMap, translation, i18n, reactI18nextOptions, combinedTOpts, mergedShouldUnescape);
const useAsParent = parent ?? reactI18nextOptions.defaultTransParent;
return useAsParent ? createElement(useAsParent, additionalProps, content) : content;
}
+12
View File
@@ -0,0 +1,12 @@
import { useTranslation } from './useTranslation.js';
export const Translation = ({
ns,
children,
...options
}) => {
const [t, i18n, ready] = useTranslation(ns, options);
return children(t, {
i18n,
lng: i18n?.language
}, ready);
};
+46
View File
@@ -0,0 +1,46 @@
import { createContext } from 'react';
import { getDefaults, setDefaults } from './defaults.js';
import { getI18n, setI18n } from './i18nInstance.js';
import { initReactI18next } from './initReactI18next.js';
export { getDefaults, setDefaults, getI18n, setI18n, initReactI18next };
export const I18nContext = createContext();
export class ReportNamespaces {
constructor() {
this.usedNamespaces = {};
}
addUsedNamespaces(namespaces) {
namespaces.forEach(ns => {
if (!this.usedNamespaces[ns]) this.usedNamespaces[ns] = true;
});
}
getUsedNamespaces() {
return Object.keys(this.usedNamespaces);
}
}
export const composeInitialProps = ForComponent => async ctx => {
const componentsInitialProps = (await ForComponent.getInitialProps?.(ctx)) ?? {};
const i18nInitialProps = getInitialProps();
return {
...componentsInitialProps,
...i18nInitialProps
};
};
export const getInitialProps = () => {
const i18n = getI18n();
if (!i18n) {
console.warn('react-i18next:: getInitialProps: You will need to pass in an i18next instance by using initReactI18next');
return {};
}
const namespaces = i18n.reportNamespaces?.getUsedNamespaces() ?? [];
const ret = {};
const initialI18nStore = {};
i18n.languages.forEach(l => {
initialI18nStore[l] = {};
namespaces.forEach(ns => {
initialI18nStore[l][ns] = i18n.getResourceBundle(l, ns) || {};
});
});
ret.initialI18nStore = initialI18nStore;
ret.initialLanguage = i18n.language;
return ret;
};
+19
View File
@@ -0,0 +1,19 @@
import { unescape } from './unescape.js';
let defaultOptions = {
bindI18n: 'languageChanged',
bindI18nStore: '',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transWrapTextNodes: '',
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
useSuspense: true,
unescape,
transDefaultProps: undefined
};
export const setDefaults = (options = {}) => {
defaultOptions = {
...defaultOptions,
...options
};
};
export const getDefaults = () => defaultOptions;
+5
View File
@@ -0,0 +1,5 @@
let i18nInstance;
export const setI18n = instance => {
i18nInstance = instance;
};
export const getI18n = () => i18nInstance;
+21
View File
@@ -0,0 +1,21 @@
export { Trans } from './Trans.js';
export { Trans as TransWithoutContext } from './TransWithoutContext.js';
export { IcuTrans } from './IcuTrans.js';
export { IcuTransWithoutContext } from './IcuTransWithoutContext.js';
export { useTranslation } from './useTranslation.js';
export { withTranslation } from './withTranslation.js';
export { Translation } from './Translation.js';
export { I18nextProvider } from './I18nextProvider.js';
export { withSSR } from './withSSR.js';
export { useSSR } from './useSSR.js';
export { initReactI18next } from './initReactI18next.js';
export { setDefaults, getDefaults } from './defaults.js';
export { setI18n, getI18n } from './i18nInstance.js';
export { nodesToString } from './Trans.js';
export { I18nContext, composeInitialProps, getInitialProps } from './context.js';
export const date = () => '';
export const time = () => '';
export const number = () => '';
export const select = () => '';
export const plural = () => '';
export const selectOrdinal = () => '';
+9
View File
@@ -0,0 +1,9 @@
import { setDefaults } from './defaults.js';
import { setI18n } from './i18nInstance.js';
export const initReactI18next = {
type: '3rdParty',
init(instance) {
setDefaults(instance.options.react);
setI18n(instance);
}
};
+1
View File
@@ -0,0 +1 @@
{"type":"module","version":"17.0.2"}
+25
View File
@@ -0,0 +1,25 @@
const matchHtmlEntity = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
const htmlEntities = {
'&amp;': '&',
'&#38;': '&',
'&lt;': '<',
'&#60;': '<',
'&gt;': '>',
'&#62;': '>',
'&apos;': "'",
'&#39;': "'",
'&quot;': '"',
'&#34;': '"',
'&nbsp;': ' ',
'&#160;': ' ',
'&copy;': '©',
'&#169;': '©',
'&reg;': '®',
'&#174;': '®',
'&hellip;': '…',
'&#8230;': '…',
'&#x2F;': '/',
'&#47;': '/'
};
const unescapeHtmlEntity = m => htmlEntities[m];
export const unescape = text => text.replace(matchHtmlEntity, unescapeHtmlEntity);
+36
View File
@@ -0,0 +1,36 @@
import { useContext } from 'react';
import { getI18n, I18nContext } from './context.js';
import { warnOnce } from './utils.js';
export const useSSR = (initialI18nStore, initialLanguage, props = {}) => {
const {
i18n: i18nFromProps
} = props;
const {
i18n: i18nFromContext
} = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (!i18n) {
warnOnce(i18n, 'NO_I18NEXT_INSTANCE', 'useSSR: You will need to pass in an i18next instance by using initReactI18next or by passing it via props or context. In monorepo setups, make sure there is only one instance of react-i18next.');
return;
}
if (i18n.options?.isClone) return;
if (initialI18nStore && !i18n.initializedStoreOnce) {
if (!i18n.services?.resourceStore) {
warnOnce(i18n, 'I18N_NOT_INITIALIZED', 'useSSR: i18n instance was found but not initialized (services.resourceStore is missing). Make sure you call i18next.init() before using useSSR — e.g. at module level, not only in getStaticProps/getServerSideProps.');
return;
}
i18n.services.resourceStore.data = initialI18nStore;
i18n.options.ns = Object.values(initialI18nStore).reduce((mem, lngResources) => {
Object.keys(lngResources).forEach(ns => {
if (mem.indexOf(ns) < 0) mem.push(ns);
});
return mem;
}, i18n.options.ns);
i18n.initializedStoreOnce = true;
i18n.isInitialized = true;
}
if (initialLanguage && !i18n.initializedLanguageOnce) {
i18n.changeLanguage(initialLanguage);
i18n.initializedLanguageOnce = true;
}
};
+161
View File
@@ -0,0 +1,161 @@
import { useContext, useCallback, useMemo, useEffect, useRef, useState } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { getI18n, getDefaults, ReportNamespaces, I18nContext } from './context.js';
import { warnOnce, loadNamespaces, loadLanguages, hasLoadedNamespace, isString, isObject } from './utils.js';
const notReadyT = (k, optsOrDefaultValue) => {
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue)) return optsOrDefaultValue.defaultValue;
if (typeof k === 'function') return '';
if (Array.isArray(k)) {
const last = k[k.length - 1];
return typeof last === 'function' ? '' : last;
}
return k;
};
const notReadySnapshot = {
t: notReadyT,
ready: false
};
const dummySubscribe = () => () => {};
export const useTranslation = (ns, props = {}) => {
const {
i18n: i18nFromProps
} = props;
const {
i18n: i18nFromContext,
defaultNS: defaultNSFromContext
} = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
if (!i18n) {
warnOnce(i18n, 'NO_I18NEXT_INSTANCE', 'useTranslation: You will need to pass in an i18next instance by using initReactI18next');
}
const i18nOptions = useMemo(() => ({
...getDefaults(),
...i18n?.options?.react,
...props
}), [i18n, props]);
const {
useSuspense,
keyPrefix
} = i18nOptions;
const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS;
const unstableNamespaces = isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation'];
const namespaces = useMemo(() => unstableNamespaces, unstableNamespaces);
i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
const revisionRef = useRef(0);
const subscribe = useCallback(callback => {
if (!i18n) return dummySubscribe;
const {
bindI18n,
bindI18nStore
} = i18nOptions;
const wrappedCallback = () => {
revisionRef.current += 1;
callback();
};
if (bindI18n) i18n.on(bindI18n, wrappedCallback);
if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
return () => {
if (bindI18n) bindI18n.split(' ').forEach(e => i18n.off(e, wrappedCallback));
if (bindI18nStore) bindI18nStore.split(' ').forEach(e => i18n.store.off(e, wrappedCallback));
};
}, [i18n, i18nOptions]);
const snapshotRef = useRef();
const getSnapshot = useCallback(() => {
if (!i18n) {
return notReadySnapshot;
}
const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every(n => hasLoadedNamespace(n, i18n, i18nOptions));
const currentLng = props.lng || i18n.language;
const currentRevision = revisionRef.current;
const lastSnapshot = snapshotRef.current;
if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) {
return lastSnapshot;
}
const calculatedT = i18n.getFixedT(currentLng, i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0], keyPrefix);
const newSnapshot = {
t: calculatedT,
ready: calculatedReady,
lng: currentLng,
keyPrefix,
revision: currentRevision
};
snapshotRef.current = newSnapshot;
return newSnapshot;
}, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]);
const [loadCount, setLoadCount] = useState(0);
const {
t,
ready
} = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
useEffect(() => {
if (i18n && !ready && !useSuspense) {
const onLoaded = () => setLoadCount(c => c + 1);
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, onLoaded);
} else {
loadNamespaces(i18n, namespaces, onLoaded);
}
}
}, [i18n, props.lng, namespaces, ready, useSuspense, loadCount]);
const finalI18n = i18n || {};
const wrapperRef = useRef(null);
const wrapperLangRef = useRef();
const createI18nWrapper = original => {
const descriptors = Object.getOwnPropertyDescriptors(original);
if (descriptors.__original) delete descriptors.__original;
const wrapper = Object.create(Object.getPrototypeOf(original), descriptors);
if (!Object.prototype.hasOwnProperty.call(wrapper, '__original')) {
try {
Object.defineProperty(wrapper, '__original', {
value: original,
writable: false,
enumerable: false,
configurable: false
});
} catch (_) {}
}
return wrapper;
};
const ret = useMemo(() => {
const original = finalI18n;
const lang = original?.language;
let i18nWrapper = original;
if (original) {
if (wrapperRef.current && wrapperRef.current.__original === original) {
if (wrapperLangRef.current !== lang) {
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
} else {
i18nWrapper = wrapperRef.current;
}
} else {
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
}
}
const effectiveT = !ready && !useSuspense ? (...args) => {
warnOnce(i18n, 'USE_T_BEFORE_READY', 'useTranslation: t was called before ready. When using useSuspense: false, make sure to check the ready flag before using t.');
return t(...args);
} : t;
const arr = [effectiveT, i18nWrapper, ready];
arr.t = effectiveT;
arr.i18n = i18nWrapper;
arr.ready = ready;
return arr;
}, [t, finalI18n, ready, finalI18n.resolvedLanguage, finalI18n.language, finalI18n.languages]);
if (i18n && useSuspense && !ready) {
throw new Promise(resolve => {
const onLoaded = () => resolve();
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, onLoaded);
} else {
loadNamespaces(i18n, namespaces, onLoaded);
}
});
}
return ret;
};
+62
View File
@@ -0,0 +1,62 @@
export const warn = (i18n, code, msg, rest) => {
const args = [msg, {
code,
...(rest || {})
}];
if (i18n?.services?.logger?.forward) {
return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
}
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
if (i18n?.services?.logger?.warn) {
i18n.services.logger.warn(...args);
} else if (console?.warn) {
console.warn(...args);
}
};
const alreadyWarned = {};
export const warnOnce = (i18n, code, msg, rest) => {
if (isString(msg) && alreadyWarned[msg]) return;
if (isString(msg)) alreadyWarned[msg] = new Date();
warn(i18n, code, msg, rest);
};
const loadedClb = (i18n, cb) => () => {
if (i18n.isInitialized) {
cb();
} else {
const initialized = () => {
setTimeout(() => {
i18n.off('initialized', initialized);
}, 0);
cb();
};
i18n.on('initialized', initialized);
}
};
export const loadNamespaces = (i18n, ns, cb) => {
i18n.loadNamespaces(ns, loadedClb(i18n, cb));
};
export const loadLanguages = (i18n, lng, ns, cb) => {
if (isString(ns)) ns = [ns];
if (i18n.options.preload && i18n.options.preload.indexOf(lng) > -1) return loadNamespaces(i18n, ns, cb);
ns.forEach(n => {
if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
});
i18n.loadLanguages(lng, loadedClb(i18n, cb));
};
export const hasLoadedNamespace = (ns, i18n, options = {}) => {
if (!i18n.languages || !i18n.languages.length) {
warnOnce(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
languages: i18n.languages
});
return true;
}
return i18n.hasLoadedNamespace(ns, {
lng: options.lng,
precheck: (i18nInstance, loadNotPending) => {
if (options.bindI18n && options.bindI18n.indexOf('languageChanging') > -1 && i18nInstance.services.backendConnector.backend && i18nInstance.isLanguageChangingTo && !loadNotPending(i18nInstance.isLanguageChangingTo, ns)) return false;
}
});
};
export const getDisplayName = Component => Component.displayName || Component.name || (isString(Component) && Component.length > 0 ? Component : 'Unknown');
export const isString = obj => typeof obj === 'string';
export const isObject = obj => typeof obj === 'object' && obj !== null;
+20
View File
@@ -0,0 +1,20 @@
import { createElement } from 'react';
import { useSSR } from './useSSR.js';
import { composeInitialProps } from './context.js';
import { getDisplayName } from './utils.js';
export const withSSR = () => function Extend(WrappedComponent) {
function I18nextWithSSR({
initialI18nStore,
initialLanguage,
...rest
}) {
useSSR(initialI18nStore, initialLanguage);
return createElement(WrappedComponent, {
...rest
});
}
I18nextWithSSR.getInitialProps = composeInitialProps(WrappedComponent);
I18nextWithSSR.displayName = `withI18nextSSR(${getDisplayName(WrappedComponent)})`;
I18nextWithSSR.WrappedComponent = WrappedComponent;
return I18nextWithSSR;
};
+32
View File
@@ -0,0 +1,32 @@
import { createElement, forwardRef as forwardRefReact } from 'react';
import { useTranslation } from './useTranslation.js';
import { getDisplayName } from './utils.js';
export const withTranslation = (ns, options = {}) => function Extend(WrappedComponent) {
function I18nextWithTranslation({
forwardedRef,
...rest
}) {
const [t, i18n, ready] = useTranslation(ns, {
...rest,
keyPrefix: options.keyPrefix
});
const passDownProps = {
...rest,
t,
i18n,
tReady: ready
};
if (options.withRef && forwardedRef) {
passDownProps.ref = forwardedRef;
} else if (!options.withRef && forwardedRef) {
passDownProps.forwardedRef = forwardedRef;
}
return createElement(WrappedComponent, passDownProps);
}
I18nextWithTranslation.displayName = `withI18nextTranslation(${getDisplayName(WrappedComponent)})`;
I18nextWithTranslation.WrappedComponent = WrappedComponent;
const forwardRef = (props, ref) => createElement(I18nextWithTranslation, Object.assign({}, props, {
forwardedRef: ref
}));
return options.withRef ? forwardRefReact(forwardRef) : I18nextWithTranslation;
};
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
// Internal Helpers
export type $Tuple<T> = readonly [T?, ...T[]];
export type $Subtract<T extends K, K> = Omit<T, keyof K>;
+1
View File
@@ -0,0 +1 @@
export * from './icu.macro.js';
+103
View File
@@ -0,0 +1,103 @@
import { ReactElement } from 'react';
import { Trans } from './index.js';
import type { Namespace, TypeOptions, i18n, ParseKeys } from 'i18next';
export { Trans };
type _DefaultNamespace = TypeOptions['defaultNS'];
declare module 'react-i18next/icu.macro' {
export interface PluralSubProps<
Key extends ParseKeys<Ns, {}, ''>,
Ns extends Namespace = _DefaultNamespace,
> {
children?: never;
i18nKey?: Key;
i18n?: i18n;
ns?: Ns;
count: number;
values?: {};
zero?: string | ReactElement;
one?: string | ReactElement;
two?: string | ReactElement;
few?: string | ReactElement;
many?: string | ReactElement;
other: string | ReactElement;
}
type PluralProps<
T,
Key extends ParseKeys<Ns, {}, ''>,
Ns extends Namespace = _DefaultNamespace,
> = {
[P in keyof T]: P extends keyof PluralSubProps<Key, Ns>
? // support the standard properties of Plural
PluralSubProps<Key, Ns>[P]
: // this supports infinite $0={..} or $123={..}
// technically it also supports $-1={..} and $2.3={..} but we don't need to
// worry since that's invalid syntax.
P extends `$${number}`
? string | ReactElement
: never;
};
interface SelectSubProps {
[key: string]: string | ReactElement;
}
interface NoChildren {
children?: never;
}
interface SelectRequiredProps<
Key extends ParseKeys<Ns, {}, ''>,
Ns extends Namespace = _DefaultNamespace,
> extends NoChildren {
i18nKey?: Key;
i18n?: i18n;
ns?: Ns;
other: string | ReactElement;
}
// defining it this way ensures that `other` is always defined, but allows
// unlimited other select types.
type SelectProps<
Key extends ParseKeys<Ns, {}, ''>,
Ns extends Namespace = _DefaultNamespace,
> = SelectSubProps & SelectRequiredProps<Key, Ns>;
function Plural<T, Key extends ParseKeys<Ns, {}, ''>, Ns extends Namespace = _DefaultNamespace>(
props: PluralProps<T, Key, Ns> & NoChildren,
): ReactElement;
function SelectOrdinal<
T,
Key extends ParseKeys<Ns, {}, ''>,
Ns extends Namespace = _DefaultNamespace,
>(props: PluralProps<T, Key, Ns> & NoChildren): ReactElement;
function Select<Key extends ParseKeys<Ns, {}, ''>, Ns extends Namespace = _DefaultNamespace>(
props: SelectProps<Key, Ns>,
): ReactElement;
function date(strings: TemplateStringsArray, variable: Date): string;
function time(strings: TemplateStringsArray, variable: Date): string;
function number(strings: TemplateStringsArray, variable: number): string;
type ValidInterpolations = ReactElement | string;
function plural(
strings: TemplateStringsArray,
variable: number,
...args: ValidInterpolations[]
): string;
function selectOrdinal(
strings: TemplateStringsArray,
variable: number,
...args: ValidInterpolations[]
): string;
function select(
strings: TemplateStringsArray,
variable: string,
...args: ValidInterpolations[]
): string;
}
+862
View File
@@ -0,0 +1,862 @@
const { createMacro } = require('babel-plugin-macros');
// copy to:
// https://astexplorer.net/#/gist/642aebbb9e449e959f4ad8907b4adf3a/4a65742e2a3e926eb55eaa3d657d1472b9ac7970
module.exports = createMacro(ICUMacro);
function ICUMacro({ references, state, babel }) {
const {
Trans = [],
Plural = [],
Select = [],
SelectOrdinal = [],
number = [],
date = [],
select = [],
selectOrdinal = [],
plural = [],
time = [],
} = references;
// assert we have the react-i18next IcuTrans component imported
addNeededImports(state, babel, references);
// transform Plural and SelectOrdinal
[...Plural, ...SelectOrdinal].forEach((referencePath) => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
pluralAsJSX(
referencePath.parentPath,
{
attributes: referencePath.parentPath.get('attributes'),
children: referencePath.parentPath.parentPath.get('children'),
},
babel,
);
} else {
// throw a helpful error message or something :)
}
});
// transform Select
Select.forEach((referencePath) => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
selectAsJSX(
referencePath.parentPath,
{
attributes: referencePath.parentPath.get('attributes'),
children: referencePath.parentPath.parentPath.get('children'),
},
babel,
);
} else {
// throw a helpful error message or something :)
}
});
// transform Trans
Trans.forEach((referencePath) => {
if (referencePath.parentPath.type === 'JSXOpeningElement') {
transAsJSX(
referencePath.parentPath,
{
attributes: referencePath.parentPath.get('attributes'),
children: referencePath.parentPath.parentPath.get('children'),
},
babel,
);
} else {
// throw a helpful error message or something :)
}
});
// check for number`` and others outside of <Trans>
Object.entries({
number,
date,
time,
select,
plural,
selectOrdinal,
}).forEach(([name, node]) => {
node.forEach((item) => {
let f = item.parentPath;
while (f) {
if (babel.types.isJSXElement(f.node)) {
const { openingElement } = f.node;
const elementName = openingElement.name;
const isMemberExpression = babel.types.isJSXMemberExpression(elementName);
if (
!isMemberExpression &&
(elementName.name === 'IcuTrans' || elementName.name === 'Trans')
) {
// this is a valid use of number/date/time/etc.
// Check for both IcuTrans (after transformation) and Trans (during transformation)
return;
}
}
f = f.parentPath;
}
const { loc } = item.node;
if (!loc) {
throw new Error(`"${name}\`\`" can only be used inside <Trans> in "unknown file"`);
}
throw new Error(
`"${name}\`\`" can only be used inside <Trans> in "${loc.filename}" on line ${loc.start.line}`,
);
});
});
}
function pluralAsJSX(parentPath, { attributes }, babel) {
const t = babel.types;
const toObjectProperty = (name, value) =>
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
// plural or selectordinal
const nodeName = parentPath.node.name.name.toLocaleLowerCase();
// will need to merge count attribute with existing values attribute in some cases
const existingValuesAttribute = findAttribute('values', attributes);
const existingValues = existingValuesAttribute
? existingValuesAttribute.node.value.expression.properties
: [];
let componentStartIndex = 0;
const extracted = attributes.reduce(
(mem, attr) => {
if (attr.node.name.name === 'i18nKey') {
// copy the i18nKey
mem.attributesToCopy.push(attr.node);
} else if (attr.node.name.name === 'count') {
// take the count for element
let exprName = attr.node.value.expression.name;
if (!exprName) {
exprName = 'count';
}
if (exprName === 'count') {
// if the prop expression name is also "count", copy it instead: <Plural count={count} --> <IcuTrans count={count}
mem.attributesToCopy.push(attr.node);
} else {
mem.values.unshift(toObjectProperty(exprName));
}
mem.defaults = `{${exprName}, ${nodeName}, ${mem.defaults}`;
} else if (attr.node.name.name === 'values') {
// skip the values attribute, as it has already been processed into mem from existingValues
} else if (attr.node.value.type === 'StringLiteral') {
// take any string node as plural option
let pluralForm = attr.node.name.name;
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
mem.defaults = `${mem.defaults} ${pluralForm} {${attr.node.value.value}}`;
} else if (attr.node.value.type === 'JSXExpressionContainer') {
// convert any Trans component to plural option extracting any values and components
const children = attr.node.value.expression.children || [];
const thisTrans = processTrans(children, babel, componentStartIndex);
let pluralForm = attr.node.name.name;
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
mem.defaults = `${mem.defaults} ${pluralForm} {${thisTrans.defaults}}`;
mem.components = mem.components.concat(thisTrans.components);
componentStartIndex += thisTrans.components.length;
}
return mem;
},
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
);
// replace the node with the new IcuTrans
parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
}
function selectAsJSX(parentPath, { attributes }, babel) {
const t = babel.types;
const toObjectProperty = (name, value) =>
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
// will need to merge switch attribute with existing values attribute
const existingValuesAttribute = findAttribute('values', attributes);
const existingValues = existingValuesAttribute
? existingValuesAttribute.node.value.expression.properties
: [];
let componentStartIndex = 0;
const extracted = attributes.reduce(
(mem, attr) => {
if (attr.node.name.name === 'i18nKey') {
// copy the i18nKey
mem.attributesToCopy.push(attr.node);
} else if (attr.node.name.name === 'switch') {
// take the switch for select element
let exprName = attr.node.value.expression.name;
if (!exprName) {
exprName = 'selectKey';
mem.values.unshift(t.objectProperty(t.identifier(exprName), attr.node.value.expression));
} else {
mem.values.unshift(toObjectProperty(exprName));
}
mem.defaults = `{${exprName}, select, ${mem.defaults}`;
} else if (attr.node.name.name === 'values') {
// skip the values attribute, as it has already been processed into mem as existingValues
} else if (attr.node.value.type === 'StringLiteral') {
// take any string node as select option
mem.defaults = `${mem.defaults} ${attr.node.name.name} {${attr.node.value.value}}`;
} else if (attr.node.value.type === 'JSXExpressionContainer') {
// convert any Trans component to select option extracting any values and components
const children = attr.node.value.expression.children || [];
const thisTrans = processTrans(children, babel, componentStartIndex);
mem.defaults = `${mem.defaults} ${attr.node.name.name} {${thisTrans.defaults}}`;
mem.components = mem.components.concat(thisTrans.components);
componentStartIndex += thisTrans.components.length;
}
return mem;
},
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
);
// replace the node with the new IcuTrans
parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
}
function transAsJSX(parentPath, { attributes, children }, babel) {
const defaultsAttr = findAttribute('defaults', attributes);
const contentAttr = findAttribute('content', attributes);
const componentsAttr = findAttribute('components', attributes);
// if there is "defaults" attribute and no "content"/"components" attribute, parse defaults and extract from the parsed defaults instead of children
// if a "content" or "components" attribute has been provided, we assume they have already constructed a valid "defaults" and it does not need to be parsed
const parseDefaults = defaultsAttr && !contentAttr && !componentsAttr;
let extracted;
if (parseDefaults) {
const defaultsExpression = defaultsAttr.node.value.value;
const parsed = babel.parse(`<>${defaultsExpression}</>`, {
presets: ['@babel/react'],
filename: babel.state?.filename || 'unknown',
}).program.body[0].expression.children;
extracted = processTrans(parsed, babel);
} else {
extracted = processTrans(children, babel);
}
let clonedAttributes = cloneExistingAttributes(attributes);
if (parseDefaults) {
// remove existing defaults so it can be replaced later with the new parsed defaults
clonedAttributes = clonedAttributes.filter((node) => node.name.name !== 'defaults');
}
// replace the node with the new IcuTrans
const replacePath = children.length ? children[0].parentPath : parentPath;
replacePath.replaceWith(
buildTransElement(extracted, clonedAttributes, babel.types, false, !!children.length),
);
}
function buildTransElement(
extracted,
finalAttributes,
t,
closeDefaults = false,
wasElementWithChildren = false,
) {
const nodeName = t.jSXIdentifier('IcuTrans');
// plural, select open { but do not close it while reduce
if (closeDefaults) extracted.defaults += '}';
// convert JSX elements to declaration objects: { type: Component, props: {...} }
const contentDeclarations = extracted.components.map((component) =>
jsxElementToDeclaration(component, t),
);
const content = t.arrayExpression(contentDeclarations);
const values = t.objectExpression(extracted.values);
// add generated IcuTrans attributes
if (!attributeExistsAlready('defaultTranslation', finalAttributes)) {
if (extracted.defaults.includes('"')) {
// wrap defaultTranslation that contains double quotes in expression container
finalAttributes.push(
t.jSXAttribute(
t.jSXIdentifier('defaultTranslation'),
t.jSXExpressionContainer(t.stringLiteral(extracted.defaults)),
),
);
} else {
finalAttributes.push(
t.jSXAttribute(t.jSXIdentifier('defaultTranslation'), t.stringLiteral(extracted.defaults)),
);
}
}
if (!attributeExistsAlready('content', finalAttributes))
finalAttributes.push(
t.jSXAttribute(t.jSXIdentifier('content'), t.jSXExpressionContainer(content)),
);
if (!attributeExistsAlready('values', finalAttributes))
finalAttributes.push(
t.jSXAttribute(t.jSXIdentifier('values'), t.jSXExpressionContainer(values)),
);
// create selfclosing IcuTrans component
const openElement = t.jSXOpeningElement(nodeName, finalAttributes, true);
if (!wasElementWithChildren) return openElement;
return t.jSXElement(openElement, null, [], true);
}
/**
* Convert a JSX element to a declaration object: { type: Component, props: {...} }
*/
function jsxElementToDeclaration(jsxElement, t) {
// Handle case where jsxElement is not actually a JSXElement
if (!jsxElement || !jsxElement.openingElement) {
console.error('Invalid JSXElement passed to jsxElementToDeclaration:', jsxElement);
return t.objectExpression([t.objectProperty(t.identifier('type'), t.stringLiteral('div'))]);
}
const elementName = jsxElement.openingElement.name;
// Get the type - either a string for HTML elements or identifier for components
let typeValue;
if (t.isJSXIdentifier(elementName)) {
// For HTML elements like 'div', 'strong', use string literal
if (elementName.name.toLowerCase() === elementName.name) {
typeValue = t.stringLiteral(elementName.name);
} else {
// For React components, use identifier
typeValue = t.identifier(elementName.name);
}
} else if (t.isJSXMemberExpression(elementName)) {
// For member expressions like Icon.Svg
const objectName = t.isJSXIdentifier(elementName.object) ? elementName.object.name : 'unknown';
const propertyName = elementName.property.name;
typeValue = t.memberExpression(t.identifier(objectName), t.identifier(propertyName));
} else {
// Fallback
typeValue = t.stringLiteral('div');
}
const properties = [t.objectProperty(t.identifier('type'), typeValue)];
// Convert JSX attributes to props object
const propsProperties = [];
jsxElement.openingElement.attributes.forEach((attr) => {
if (t.isJSXAttribute(attr)) {
const propName = t.isJSXIdentifier(attr.name) ? attr.name.name : String(attr.name);
let propValue;
if (attr.value === null) {
// Boolean prop like <Component disabled />
propValue = t.booleanLiteral(true);
} else if (t.isStringLiteral(attr.value)) {
propValue = attr.value;
} else if (t.isJSXExpressionContainer(attr.value)) {
propValue = attr.value.expression;
} else {
// fallback
propValue = t.nullLiteral();
}
// Use string literal for keys that aren't valid identifiers (e.g., contain hyphens)
const propKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName)
? t.identifier(propName)
: t.stringLiteral(propName);
propsProperties.push(t.objectProperty(propKey, propValue));
} else if (t.isJSXSpreadAttribute(attr)) {
// Handle spread attributes like {...spreadProps}
propsProperties.push(t.spreadElement(attr.argument));
}
});
// Handle nested children if any
if (jsxElement.children && jsxElement.children.length > 0) {
const childDeclarations = jsxElement.children
.filter((child) =>
// Only include JSXElements in the declaration tree
// JSXText content is already captured in the defaults string
t.isJSXElement(child),
)
.map((child) => jsxElementToDeclaration(child, t));
if (childDeclarations.length > 0) {
propsProperties.push(
t.objectProperty(t.identifier('children'), t.arrayExpression(childDeclarations)),
);
}
}
if (propsProperties.length > 0) {
properties.push(t.objectProperty(t.identifier('props'), t.objectExpression(propsProperties)));
}
return t.objectExpression(properties);
}
function cloneExistingAttributes(attributes) {
return attributes.reduce((mem, attr) => {
const node = attr.node ? attr.node : attr;
// Skip 'defaults' attribute as we're replacing it with 'defaultTranslation'
if (node.type === 'JSXAttribute' && node.name && node.name.name === 'defaults') {
return mem;
}
// Skip 'components' attribute as we're replacing it with 'content'
if (node.type === 'JSXAttribute' && node.name && node.name.name === 'components') {
return mem;
}
mem.push(node);
return mem;
}, []);
}
function findAttribute(name, attributes) {
return attributes.find((child) => {
const ele = child.node ? child.node : child;
return ele.name.name === name;
});
}
function attributeExistsAlready(name, attributes) {
return !!findAttribute(name, attributes);
}
function processTrans(children, babel, componentStartIndex = 0) {
const res = {};
res.defaults = mergeChildren(children, babel, componentStartIndex);
res.components = getComponents(children, babel);
res.values = getValues(children, babel);
return res;
}
const leadingNewLineAndWhitespace = /^\n\s+/g;
const trailingNewLineAndWhitespace = /\n\s+$/g;
function trimIndent(text) {
const newText = text
.replace(leadingNewLineAndWhitespace, '')
.replace(trailingNewLineAndWhitespace, '');
return newText;
}
/**
* add comma-delimited expressions like `{ val, number }`
*/
function mergeCommaExpressions(ele) {
if (ele.expression && ele.expression.expressions) {
return `{${ele.expression.expressions
.reduce((m, i) => {
m.push(i.name || i.value);
return m;
}, [])
.join(', ')}}`;
}
return '';
}
/**
* this is for supporting complex icu type interpolations
* date`${variable}` and number`{${varName}, ::percent}`
* also, plural`{${count}, one { ... } other { ... }}
*/
function mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel) {
if (t.isTaggedTemplateExpression(ele.expression)) {
const [, text, index] = getTextAndInterpolatedVariables(
ele.expression.tag.name,
ele.expression,
componentFoundIndex,
babel,
);
return [text, index];
}
return ['', componentFoundIndex];
}
function mergeChildren(children, babel, componentStartIndex = 0) {
const t = babel.types;
let componentFoundIndex = componentStartIndex;
return children.reduce((mem, child) => {
const ele = child.node ? child.node : child;
let result = mem;
// add text, but trim indentation whitespace
if (t.isJSXText(ele) && ele.value) result += trimIndent(ele.value);
// add ?!? forgot
if (ele.expression && ele.expression.value) result += ele.expression.value;
// add `{ val }`
if (ele.expression && ele.expression.name) result += `{${ele.expression.name}}`;
// add `{ val, number }`
result += mergeCommaExpressions(ele);
const [nextText, newIndex] = mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel);
result += nextText;
componentFoundIndex = newIndex;
// add <strong>...</strong> with replace to <0>inner string</0>
if (t.isJSXElement(ele)) {
result += `<${componentFoundIndex}>${mergeChildren(
ele.children,
babel,
)}</${componentFoundIndex}>`;
componentFoundIndex += 1;
}
return result;
}, '');
}
const extractTaggedTemplateValues = (ele, babel, toObjectProperty) => {
// date`${variable}` and so on
if (ele.expression && ele.expression.type === 'TaggedTemplateExpression') {
const [variables] = getTextAndInterpolatedVariables(
ele.expression.tag.name,
ele.expression,
0,
babel,
);
return variables.map((vari) => toObjectProperty(vari));
}
return [];
};
/**
* Extract the names of interpolated value as object properties to pass to IcuTrans
*/
function getValues(children, babel) {
const t = babel.types;
const toObjectProperty = (name, value) =>
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
return children.reduce((mem, child) => {
const ele = child.node ? child.node : child;
let result = mem;
// add `{ var }` to values
if (ele.expression && ele.expression.name) mem.push(toObjectProperty(ele.expression.name));
// add `{ var, number }` to values
if (ele.expression && ele.expression.expressions)
result.push(
toObjectProperty(ele.expression.expressions[0].name || ele.expression.expressions[0].value),
);
// add `{ var: 'bar' }` to values
if (ele.expression && ele.expression.properties)
result = result.concat(ele.expression.properties);
// date`${variable}` and so on
result = result.concat(extractTaggedTemplateValues(ele, babel, toObjectProperty));
// recursive add inner elements stuff to values
if (t.isJSXElement(ele)) {
result = result.concat(getValues(ele.children, babel));
}
return result;
}, []);
}
/**
* Common logic for adding a child element of Trans to the list of components to hydrate the translation
* @param {JSXElement} jsxElement
* @param {JSXElement[]} mem
*/
const processJSXElement = (jsxElement, mem, t) => {
const clone = t.clone(jsxElement);
clone.children = clone.children.reduce((clonedMem, clonedChild) => {
const clonedEle = clonedChild.node ? clonedChild.node : clonedChild;
// clean out invalid definitions by replacing `{ catchDate, date, short }` with `{ catchDate }`
if (clonedEle.expression && clonedEle.expression.expressions)
clonedEle.expression.expressions = [clonedEle.expression.expressions[0]];
clonedMem.push(clonedChild);
return clonedMem;
}, []);
mem.push(jsxElement);
};
/**
* Extract the React components to pass to IcuTrans as content
*/
function getComponents(children, babel) {
const t = babel.types;
return children.reduce((mem, child) => {
const ele = child.node ? child.node : child;
if (t.isJSXExpressionContainer(ele)) {
// check for date`` and so on
if (t.isTaggedTemplateExpression(ele.expression)) {
ele.expression.quasi.expressions.forEach((expr) => {
// check for sub-expressions. This can happen with plural`` or select`` or selectOrdinal``
// these can have nested components
if (t.isTaggedTemplateExpression(expr) && expr.quasi.expressions.length) {
mem.push(...getComponents(expr.quasi.expressions, babel));
}
if (!t.isJSXElement(expr)) {
// ignore anything that is not a component
return;
}
processJSXElement(expr, mem, t);
});
}
}
if (t.isJSXElement(ele)) {
processJSXElement(ele, mem, t);
}
return mem;
}, []);
}
const icuInterpolators = ['date', 'time', 'number', 'plural', 'select', 'selectOrdinal'];
const importsToAdd = ['IcuTrans'];
/**
* helper split out of addNeededImports to make codeclimate happy
*
* This does the work of amending an existing import from "react-i18next", or
* creating a new one if it doesn't exist
*/
function addImports(state, existingImport, allImportsToAdd, t) {
// append imports to existing or add a new react-i18next import for the IcuTrans and icu tagged template literals
if (existingImport) {
allImportsToAdd.forEach((name) => {
if (
existingImport.specifiers.findIndex(
(specifier) => specifier.imported && specifier.imported.name === name,
) === -1
) {
existingImport.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)));
}
});
} else {
state.file.path.node.body.unshift(
t.importDeclaration(
allImportsToAdd.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name))),
t.stringLiteral('react-i18next'),
),
);
}
}
/**
* Add `import { IcuTrans, number, date, <etc.> } from "react-i18next"` as needed
*/
function addNeededImports(state, babel, references) {
const t = babel.types;
// check if there is an existing react-i18next import
const existingImport = state.file.path.node.body.find(
(importNode) =>
t.isImportDeclaration(importNode) && importNode.source.value === 'react-i18next',
);
// check for any of the tagged template literals that are used in the source, and add them
const usedRefs = Object.keys(references).filter((importName) => {
if (!icuInterpolators.includes(importName)) {
return false;
}
return references[importName].length;
});
// combine IcuTrans + any tagged template literals
const allImportsToAdd = importsToAdd.concat(usedRefs);
addImports(state, existingImport, allImportsToAdd, t);
}
/**
* iterate over a node detected inside a tagged template literal
*
* This is a helper function for `extractVariableNamesFromQuasiNodes` defined below
*
* this is called using reduce as a way of tricking what would be `.map()`
* into passing in the parameters needed to both modify `componentFoundIndex`,
* `stringOutput`, and `interpolatedVariableNames`
* and to pass in the dependencies babel, and type. Type is the template type.
* For "date``" the type will be `date`. for "number``" the type is `number`, etc.
*/
const extractNestedTemplatesAndComponents = (
{ componentFoundIndex: lastIndex, babel, stringOutput, type, interpolatedVariableNames },
node,
) => {
let componentFoundIndex = lastIndex;
if (node.type === 'JSXElement') {
// perform the interpolation of components just as we do in a normal Trans setting
const subText = `<${componentFoundIndex}>${mergeChildren(
node.children,
babel,
)}</${componentFoundIndex}>`;
componentFoundIndex += 1;
stringOutput.push(subText);
} else if (node.type === 'TaggedTemplateExpression') {
// a nested date``/number``/plural`` etc., extract whatever is inside of it
const tagName = babel.types.isIdentifier(node.tag) && node.tag.name ? node.tag.name : 'unknown';
const [variableNames, childText, newIndex] = getTextAndInterpolatedVariables(
tagName,
node,
componentFoundIndex,
babel,
);
interpolatedVariableNames.push(...variableNames);
componentFoundIndex = newIndex;
stringOutput.push(childText);
} else if (node.type === 'Identifier') {
// turn date`${thing}` into `thing, date`
const nodeName = node.name || 'unknown';
stringOutput.push(`${nodeName}, ${type}`);
} else if (node.type === 'TemplateElement') {
// convert all whitespace into a single space for the text in the tagged template literal
const cookedValue = node.value.cooked || '';
stringOutput.push(cookedValue.replace(/\s+/g, ' '));
} else {
// unknown node type, ignore
}
return { componentFoundIndex, babel, stringOutput, type, interpolatedVariableNames };
};
/**
* filter the list of nodes within a tagged template literal to the 4 types we can process,
* and ignore anything else.
*
* this is a helper function for `extractVariableNamesFromQuasiNodes`
*/
const filterNodes = (node) => {
if (node.type === 'Identifier') {
// if the node has a name, keep it
return node.name;
}
if (node.type === 'JSXElement' || node.type === 'TaggedTemplateExpression') {
// always keep interpolated elements or other tagged template literals like a nested date`` inside a plural``
return true;
}
if (node.type === 'TemplateElement') {
// return the "cooked" (escaped) text for the text in the template literal (`, ::percent` in number`${varname}, ::percent`)
return node.value.cooked;
}
// unknown node type, ignore
return false;
};
const errorOnInvalidQuasiNodes = (primaryNode) => {
const noInterpolationError = !primaryNode.quasi.expressions.length;
const wrongOrderError = primaryNode.quasi.quasis[0].value.raw.length;
const tagName = primaryNode.tag.name || 'unknown';
const message = `${tagName} argument must be interpolated ${
noInterpolationError ? 'in' : 'at the beginning of'
} "${tagName}\`\`" in "${primaryNode.loc?.filename}" on line ${primaryNode.loc?.start.line}`;
if (noInterpolationError || wrongOrderError) {
throw new Error(message);
}
};
const extractNodeVariableNames = (varNode, babel) => {
const interpolatedVariableNames = [];
if (varNode.type === 'JSXElement') {
// extract inner interpolated variables and add to the list
interpolatedVariableNames.push(
...getValues(varNode.children, babel).map((value) => {
if (babel.types.isIdentifier(value.value)) {
return value.value.name;
}
return String(value.value);
}),
);
} else if (varNode.type === 'Identifier') {
// the name of the interpolated variable
interpolatedVariableNames.push(varNode.name);
}
return interpolatedVariableNames;
};
const extractVariableNamesFromQuasiNodes = (primaryNode, babel) => {
errorOnInvalidQuasiNodes(primaryNode);
// this will contain all the nodes to convert to the ICU messageformat text
// at first they are unsorted, but will be ordered correctly at the end of the function
const text = [];
// the variable names. These are converted to object references as required for the IcuTrans values
// in getValues() (toObjectProperty helper function)
const interpolatedVariableNames = [];
primaryNode.quasi.expressions.forEach((varNode) => {
if (
!babel.types.isIdentifier(varNode) &&
!babel.types.isTaggedTemplateExpression(varNode) &&
!babel.types.isJSXElement(varNode)
) {
const tagName = primaryNode.tag.name || 'unknown';
throw new Error(
`Must pass a variable, not an expression to "${tagName}\`\`" in "${primaryNode.loc?.filename}" on line ${primaryNode.loc?.start.line}`,
);
}
text.push(varNode);
interpolatedVariableNames.push(...extractNodeVariableNames(varNode, babel));
});
primaryNode.quasi.quasis.forEach((quasiNode) => {
// these are the text surrounding the variable interpolation
// so in date`${varname}, short` it would be `''` and `, short`.
// (the empty string before `${varname}` and the stuff after it)
text.push(quasiNode);
});
return { text, interpolatedVariableNames };
};
const throwOnInvalidType = (type, primaryNode) => {
if (!icuInterpolators.includes(type)) {
throw new Error(
`Unsupported tagged template literal "${type}", must be one of date, time, number, plural, select, selectOrdinal in "${primaryNode.loc?.filename}" on line ${primaryNode.loc?.start.line}`,
);
}
};
/**
* Retrieve the new text to use, and any interpolated variables
*
* This is used to process tagged template literals like date`${variable}` and number`${num}, ::percent`
*
* for the data example, it will return text of `{variable, date}` with a variable of `variable`
* for the number example, it will return text of `{num, number, ::percent}` with a variable of `num`
* @param {string} type the name of the tagged template (`date`, `number`, `plural`, etc. - any valid complex ICU type)
* @param {TaggedTemplateExpression} primaryNode the template expression node
* @param {int} index starting index number of components to be used for interpolations like <0>
* @param {*} babel
*/
function getTextAndInterpolatedVariables(type, primaryNode, index, babel) {
throwOnInvalidType(type, primaryNode);
const componentFoundIndex = index;
const { text, interpolatedVariableNames } = extractVariableNamesFromQuasiNodes(
primaryNode,
babel,
);
const { stringOutput, componentFoundIndex: newIndex } = text
.filter(filterNodes)
// sort by the order they appear in the source code
.sort((a, b) => {
const aStart = a.start != null ? a.start : null;
const bStart = b.start != null ? b.start : null;
if (aStart != null && bStart != null && aStart > bStart) return 1;
return -1;
})
.reduce(extractNestedTemplatesAndComponents, {
babel,
componentFoundIndex,
stringOutput: [],
type,
interpolatedVariableNames,
});
return [
interpolatedVariableNames,
`{${stringOutput.join('')}}`,
// return the new component interpolation index
newIndex,
];
}
+1
View File
@@ -0,0 +1 @@
export * from './index.js';
+335
View File
@@ -0,0 +1,335 @@
import type { $Subtract, $Tuple } from './helpers.js';
import type {
ReactOptions,
i18n,
Resource,
FlatNamespace,
Namespace,
TypeOptions,
TFunction,
KeyPrefix,
KeyPrefixSelector,
NsResource,
ParseKeys,
TOptions,
} from 'i18next';
import * as React from 'react';
import {
Trans,
TransProps,
TransSelectorProps,
ErrorCode,
ErrorArgs,
} from './TransWithoutContext.js';
export { initReactI18next } from './initReactI18next.js';
export function nodesToString(
children: React.ReactNode,
i18nOptions?: ReactOptions,
i18n?: i18n,
i18nKey?: string,
): string;
export const TransWithoutContext: typeof Trans;
export { Trans, TransProps, TransSelectorProps, ErrorArgs, ErrorCode };
/**
* Content declaration for IcuTrans component
* Describes React components as type + props blueprints that will be rendered
*/
export interface IcuTransContentDeclaration {
type: string | React.ComponentType<any>;
props?: {
children?: IcuTransContentDeclaration | IcuTransContentDeclaration[];
[key: string]: any;
};
}
/**
* Props for IcuTransWithoutContext component (no React context)
*/
export type IcuTransWithoutContextProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
> = {
/** The i18n key to look up the translation */
i18nKey: Key;
/** The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>") */
defaultTranslation: string;
/** Declaration tree describing React components and their props */
content: IcuTransContentDeclaration[];
/** Optional namespace(s) for the translation */
ns?: Ns;
/** Optional values for ICU variable interpolation */
values?: Record<string, any>;
/** i18next instance. If not provided, uses global instance */
i18n?: i18n;
/** Custom translation function */
t?: TFunction<Ns, KPrefix>;
};
/**
* Props for IcuTrans component (with React context support)
*/
export type IcuTransProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
> = IcuTransWithoutContextProps<Key, Ns, KPrefix, TContext, TOpt>;
/**
* IcuTrans component for rendering ICU MessageFormat translations
* This is the context-aware version that works with I18nextProvider
*
* @example
* ```tsx
* // Type-safe usage with namespace
* <IcuTrans<'welcome.message', 'common'>
* i18nKey="welcome.message"
* defaultTranslation="Welcome <0>friend</0>!"
* content={[{ type: 'strong', props: {} }]}
* />
* ```
*/
export interface IcuTransComponent {
<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
>(
props: IcuTransProps<Key, Ns, KPrefix, TContext, TOpt>,
): React.ReactElement;
}
/**
* IcuTransWithoutContext component for rendering ICU MessageFormat translations
* This version does not use React context and requires explicit i18n instance
*
* @example
* ```tsx
* // Type-safe usage with namespace
* <IcuTransWithoutContext<'welcome.message', 'common'>
* i18nKey="welcome.message"
* defaultTranslation="Welcome <0>back</0>!"
* content={[{ type: 'strong', props: {} }]}
* i18n={i18nInstance}
* />
* ```
*/
export interface IcuTransWithoutContextComponent {
<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
>(
props: IcuTransWithoutContextProps<Key, Ns, KPrefix, TContext, TOpt>,
): React.ReactElement;
}
export const IcuTrans: IcuTransComponent;
export const IcuTransWithoutContext: IcuTransWithoutContextComponent;
export function setDefaults(options: ReactOptions): void;
export function getDefaults(): ReactOptions;
export function setI18n(instance: i18n): void;
export function getI18n(): i18n;
export function composeInitialProps(ForComponent: any): (ctx: unknown) => Promise<any>;
export function getInitialProps(): {
initialI18nStore: {
[ns: string]: {};
};
initialLanguage: string;
};
export interface ReportNamespaces {
addUsedNamespaces(namespaces: Namespace): void;
getUsedNamespaces(): string[];
}
declare module 'i18next' {
// interface i18n {
// reportNamespaces?: ReportNamespaces;
// }
interface CustomInstanceExtensions {
reportNamespaces?: ReportNamespaces;
}
}
type ObjectOrNever = TypeOptions['allowObjectInHTMLChildren'] extends true
? Record<string, unknown>
: never;
type ReactI18NextChildren = React.ReactNode | ObjectOrNever;
declare module 'react' {
namespace JSX {
interface IntrinsicAttributes {
i18nIsDynamicList?: boolean;
}
}
interface HTMLAttributes<T> {
// This union is inspired by the typings for React.ReactNode. We do this to fix "This JSX tag's 'children' prop
// expects a single child of type 'ReactI18NextChildren', but multiple children were provided":
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5a1e9f91ed0143adede394adb3f540e650455f71/types/react/index.d.ts#L268
children?: ReactI18NextChildren | Iterable<ReactI18NextChildren>;
}
}
type _DefaultNamespace = TypeOptions['defaultNS'];
export function useSSR(initialI18nStore: Resource, initialLanguage: string): void;
// If the version is earlier than i18next v25.4.0, enableSelector does not exist in TypeOptions, so a conditional type is used to maintain type compatibility.
type _EnableSelector = TypeOptions extends { enableSelector: infer U } ? U : false;
export type UseTranslationOptions<KPrefix> = {
i18n?: i18n;
useSuspense?: boolean;
keyPrefix?: KPrefix;
bindI18n?: string | false;
nsMode?: 'fallback' | 'default';
lng?: string;
// other of these options might also work: https://github.com/i18next/i18next/blob/master/index.d.ts#L127
};
export type UseTranslationResponse<Ns extends Namespace, KPrefix> = [
t: TFunction<Ns, KPrefix>,
i18n: i18n,
ready: boolean,
] & {
t: TFunction<Ns, KPrefix>;
i18n: i18n;
ready: boolean;
};
// Workaround to make code completion to work when suggesting namespaces.
// This is a typescript limitation when using generics with default values,
// it'll be addressed in this issue: https://github.com/microsoft/TypeScript/issues/52516
export type FallbackNs<Ns> = Ns extends undefined
? _DefaultNamespace
: Ns extends Namespace
? Ns
: _DefaultNamespace;
export const useTranslation: _EnableSelector extends true | 'optimize'
? UseTranslationSelector
: UseTranslationLegacy;
interface UseTranslationLegacy {
<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
const KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
}
interface UseTranslationSelector {
// Overload: selector function as keyPrefix — provides type-safe key narrowing
<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined,
const KPrefix extends KeyPrefixSelector<FallbackNs<Ns>>,
>(
ns: Ns,
options: UseTranslationOptions<KPrefix> & { keyPrefix: KPrefix },
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
// Overload: string or undefined keyPrefix (original behavior)
<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
const KPrefix = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
}
// Need to see usage to improve this
export function withSSR(): <Props>(WrappedComponent: React.ComponentType<Props>) => {
({
initialI18nStore,
initialLanguage,
...rest
}: {
initialI18nStore: Resource;
initialLanguage: string;
} & Props): React.FunctionComponentElement<Props>;
getInitialProps: (ctx: unknown) => Promise<any>;
};
export interface WithTranslation<
Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
> {
t: TFunction<FallbackNs<Ns>, KPrefix>;
i18n: i18n;
tReady: boolean;
}
export interface WithTranslationProps {
i18n?: i18n;
useSuspense?: boolean;
}
export function withTranslation<
Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(
ns?: Ns,
options?: {
withRef?: boolean;
keyPrefix?: KPrefix;
},
): <
C extends React.ComponentType<React.ComponentProps<any> & WithTranslationProps>,
ResolvedProps = React.JSX.LibraryManagedAttributes<
C,
$Subtract<React.ComponentProps<C>, WithTranslationProps>
>,
>(
component: C,
) => React.ComponentType<Omit<ResolvedProps, keyof WithTranslation<Ns>> & WithTranslationProps>;
export interface I18nextProviderProps {
children?: React.ReactNode;
i18n: i18n;
defaultNS?: string | string[];
}
export const I18nextProvider: React.FunctionComponent<I18nextProviderProps>;
export const I18nContext: React.Context<{ i18n: i18n }>;
export interface TranslationProps<
Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
> {
children: (
t: TFunction<FallbackNs<Ns>, KPrefix>,
options: {
i18n: i18n;
lng: string;
},
ready: boolean,
) => React.ReactNode;
ns?: Ns;
i18n?: i18n;
useSuspense?: boolean;
keyPrefix?: KPrefix;
nsMode?: 'fallback' | 'default';
}
export function Translation<
Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(props: TranslationProps<Ns, KPrefix>): any;
+1
View File
@@ -0,0 +1 @@
export * from './initReactI18next.js';
+3
View File
@@ -0,0 +1,3 @@
import type { ThirdPartyModule } from 'i18next';
export const initReactI18next: ThirdPartyModule;
+4
View File
@@ -0,0 +1,4 @@
export default {
'!*.?(c|m){js,ts}?(x)': ['prettier --write --ignore-unknown'],
'*.?(c|m){js,ts}?(x)': ['prettier --write --ignore-unknown', 'eslint --cache --fix --env-info'],
};
+172
View File
@@ -0,0 +1,172 @@
{
"name": "react-i18next",
"version": "17.0.2",
"description": "Internationalization for react done right. Using the i18next i18n ecosystem.",
"main": "dist/commonjs/index.js",
"types": "./index.d.mts",
"jsnext:main": "dist/es/index.js",
"module": "dist/es/index.js",
"keywords": [
"i18next",
"internationalization",
"i18n",
"translation",
"localization",
"l10n",
"globalization",
"react",
"reactjs"
],
"exports": {
"./package.json": "./package.json",
".": {
"types": {
"require": "./index.d.ts",
"import": "./index.d.mts"
},
"module": "./dist/es/index.js",
"import": "./dist/es/index.js",
"require": "./dist/commonjs/index.js",
"default": "./dist/es/index.js"
},
"./TransWithoutContext": {
"types": {
"require": "./TransWithoutContext.d.ts",
"import": "./TransWithoutContext.d.mts"
},
"module": "./dist/es/TransWithoutContext.js",
"import": "./dist/es/TransWithoutContext.js",
"require": "./dist/commonjs/TransWithoutContext.js",
"default": "./dist/es/TransWithoutContext.js"
},
"./initReactI18next": {
"types": {
"require": "./initReactI18next.d.ts",
"import": "./initReactI18next.d.mts"
},
"module": "./dist/es/initReactI18next.js",
"import": "./dist/es/initReactI18next.js",
"require": "./dist/commonjs/initReactI18next.js",
"default": "./dist/es/initReactI18next.js"
},
"./icu.macro": {
"types": {
"require": "./icu.macro.d.ts",
"import": "./icu.macro.d.mts"
},
"module": "./icu.macro.js",
"import": "./icu.macro.js",
"require": "./icu.macro.js",
"default": "./icu.macro.js"
}
},
"homepage": "https://github.com/i18next/react-i18next",
"bugs": "https://github.com/i18next/react-i18next/issues",
"repository": {
"type": "git",
"url": "https://github.com/i18next/react-i18next.git"
},
"dependencies": {
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 26.0.1",
"react": ">= 16.8.0",
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/eslint-parser": "^7.28.6",
"@babel/plugin-proposal-async-generator-functions": "^7.20.7",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.29.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.29.2",
"@babel/preset-react": "^7.28.5",
"@babel/register": "^7.28.6",
"@rollup/plugin-babel": "^7.0.0",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "1.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0",
"@types/react": "^19.2.14",
"@vitest/coverage-v8": "^4.1.2",
"all-contributors-cli": "^6.26.1",
"babel-core": "^7.0.0-bridge.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tester": "^11.0.4",
"coveralls": "^3.1.1",
"cpy-cli": "^7.0.0",
"cross-env": "^10.1.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-testing-library": "^6.5.0",
"happy-dom": "^20.8.9",
"husky": "^9.1.7",
"i18next": "^26.0.3",
"lint-staged": "^16.4.0",
"mkdirp": "^3.0.1",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-test-renderer": "^19.2.4",
"rimraf": "^6.1.3",
"rollup": "^4.60.0",
"typescript": "~6.0.2",
"vitest": "^4.1.2",
"yargs": "^18.0.0"
},
"scripts": {
"clean": "rimraf dist && mkdirp dist",
"lint": "eslint --cache .",
"lint:fix": "eslint --cache --fix .",
"format": "prettier . --check",
"format:fix": "prettier . --write --list-different",
"copy": "cpy ./dist/umd/react-i18next.min.js ./dist/umd/react-i18next.js . --flat && echo '{\"type\":\"module\"}' > dist/es/package.json",
"build:es": "cross-env BABEL_ENV=ESNext babel src --out-dir dist/es",
"build:cjs": "babel src --out-dir dist/commonjs",
"build:umd": "rollup -c rollup.config.mjs --format umd && rollup -c rollup.config.mjs --format umd --uglify",
"build:amd": "rollup -c rollup.config.mjs --format amd && rollup -c rollup.config.mjs --format amd --uglify",
"build:iife": "rollup -c rollup.config.mjs --format iife && rollup -c rollup.config.mjs --format iife --uglify",
"build": "npm run clean && npm run build:cjs && npm run build:es && npm run build:umd && npm run build:amd && npm run copy",
"fix_dist_package": "node -e 'console.log(`{\"type\":\"module\",\"version\":\"${process.env.npm_package_version}\"}`)' > dist/es/package.json",
"preversion": "npm run build && git push",
"postversion": "npm run fix_dist_package && git push && git push --tags",
"test": "vitest",
"test:coverage": "npm run test -- --coverage --run",
"test:typescript": "npm run test:typescript:issue-1899 && vitest --config vitest.workspace.typescript.mts",
"test:typescript:issue-1899": "bash test/typescript/issue-1899/check-types.sh",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"prepare": "husky"
},
"author": "Jan Mühlemann <jan.muehlemann@gmail.com> (https://github.com/jamuhl)",
"license": "MIT",
"lock": false,
"sideEffects": false,
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+3821
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
import { createElement, useMemo } from 'react';
import { I18nContext } from './context.js';
export function I18nextProvider({ i18n, defaultNS, children }) {
const value = useMemo(() => ({ i18n, defaultNS }), [i18n, defaultNS]);
return createElement(I18nContext.Provider, { value }, children);
}
+103
View File
@@ -0,0 +1,103 @@
import { useContext } from 'react';
import { IcuTransWithoutContext } from './IcuTransWithoutContext.js';
import { getI18n, I18nContext } from './context.js';
/**
* IcuTrans component for rendering ICU MessageFormat translations with React components
*
* This component provides a context-aware wrapper around IcuTransWithoutContext,
* automatically retrieving the i18n instance from React context when used within
* an I18nextProvider. It uses a declaration tree approach where components are
* defined as type + props blueprints, fetches the translated string, and reconstructs
* the React element tree by replacing numbered tags with actual components.
*
* Key features:
* - Supports React Context (I18nextProvider)
* - Falls back to global i18n instance
* - ICU MessageFormat compatible
* - Type-safe component declarations
* - Automatic HTML entity decoding
*
* @param {Object} props - Component props
* @param {string} props.i18nKey - The i18n key to look up the translation
* @param {string} props.defaultTranslation - The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>")
* @param {Array<{type: string|React.ComponentType, props?: Object}>} props.content - Declaration tree describing React components and their props
* @param {string|string[]} [props.ns] - Optional namespace(s) for the translation
* @param {Object} [props.values] - Optional values for ICU variable interpolation
* @param {Object} [props.i18n] - Optional i18next instance (overrides context)
* @param {Function} [props.t] - Optional translation function (overrides context)
* @returns {React.ReactElement} React fragment containing the rendered translation
*
* @example
* ```jsx
* // Basic usage with context
* <I18nextProvider i18n={i18n}>
* <IcuTrans
* i18nKey="welcome.message"
* defaultTranslation="Welcome <0>friend</0>!"
* content={[
* { type: 'strong', props: { className: 'highlight' } }
* ]}
* />
* </I18nextProvider>
* ```
*
* @example
* ```jsx
* // With custom components and nested structure
* <IcuTrans
* i18nKey="docs.link"
* defaultTranslation="Read the <0>documentation <1></1></0> for more info"
* content={[
* { type: 'a', props: { href: '/docs' } },
* { type: Icon, props: { name: 'external' } }
* ]}
* />
* ```
*
* @example
* ```jsx
* // With nested children in declarations
* <IcuTrans
* i18nKey="list.items"
* defaultTranslation="<0><0>First item</0><1>Second item</1></0>"
* content={[
* {
* type: 'ul',
* props: {
* children: [
* { type: 'li', props: {} },
* { type: 'li', props: {} }
* ]
* }
* }
* ]}
* />
* ```
*/
export function IcuTrans({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps,
}) {
const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
const t = tFromProps || i18n?.t.bind(i18n);
return IcuTransWithoutContext({
i18nKey,
defaultTranslation,
content,
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
values,
i18n,
t: tFromProps,
});
}
IcuTrans.displayName = 'IcuTrans';
+24
View File
@@ -0,0 +1,24 @@
/**
* Error thrown during translation parsing
*/
export class TranslationParserError extends Error {
/**
* @param {string} message - Error message
* @param {number} [position] - Position in the translation string where the error occurred
* @param {string} [translationString] - The full translation string being parsed
*/
constructor(message, position, translationString) {
super(message);
this.name = 'TranslationParserError';
this.position = position;
this.translationString = translationString;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TranslationParserError);
}
}
}
+264
View File
@@ -0,0 +1,264 @@
/**
* Common HTML entities map for fast lookup
*/
const commonEntities = {
// Basic entities
'&nbsp;': '\u00A0', // Non-breaking space
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
// Copyright, trademark, and registration
'&copy;': '©',
'&reg;': '®',
'&trade;': '™',
// Punctuation
'&hellip;': '…',
'&ndash;': '–',
'&mdash;': '—',
'&lsquo;': '\u2018',
'&rsquo;': '\u2019',
'&sbquo;': '\u201A',
'&ldquo;': '\u201C',
'&rdquo;': '\u201D',
'&bdquo;': '\u201E',
'&dagger;': '†',
'&Dagger;': '‡',
'&bull;': '•',
'&prime;': '′',
'&Prime;': '″',
'&lsaquo;': '‹',
'&rsaquo;': '›',
'&sect;': '§',
'&para;': '¶',
'&middot;': '·',
// Spaces
'&ensp;': '\u2002',
'&emsp;': '\u2003',
'&thinsp;': '\u2009',
// Currency
'&euro;': '€',
'&pound;': '£',
'&yen;': '¥',
'&cent;': '¢',
'&curren;': '¤',
// Math symbols
'&times;': '×',
'&divide;': '÷',
'&minus;': '−',
'&plusmn;': '±',
'&ne;': '≠',
'&le;': '≤',
'&ge;': '≥',
'&asymp;': '≈',
'&equiv;': '≡',
'&infin;': '∞',
'&int;': '∫',
'&sum;': '∑',
'&prod;': '∏',
'&radic;': '√',
'&part;': '∂',
'&permil;': '‰',
'&deg;': '°',
'&micro;': 'µ',
// Arrows
'&larr;': '←',
'&uarr;': '↑',
'&rarr;': '→',
'&darr;': '↓',
'&harr;': '↔',
'&crarr;': '↵',
'&lArr;': '⇐',
'&uArr;': '⇑',
'&rArr;': '⇒',
'&dArr;': '⇓',
'&hArr;': '⇔',
// Greek letters (lowercase)
'&alpha;': 'α',
'&beta;': 'β',
'&gamma;': 'γ',
'&delta;': 'δ',
'&epsilon;': 'ε',
'&zeta;': 'ζ',
'&eta;': 'η',
'&theta;': 'θ',
'&iota;': 'ι',
'&kappa;': 'κ',
'&lambda;': 'λ',
'&mu;': 'μ',
'&nu;': 'ν',
'&xi;': 'ξ',
'&omicron;': 'ο',
'&pi;': 'π',
'&rho;': 'ρ',
'&sigma;': 'σ',
'&tau;': 'τ',
'&upsilon;': 'υ',
'&phi;': 'φ',
'&chi;': 'χ',
'&psi;': 'ψ',
'&omega;': 'ω',
// Greek letters (uppercase)
'&Alpha;': 'Α',
'&Beta;': 'Β',
'&Gamma;': 'Γ',
'&Delta;': 'Δ',
'&Epsilon;': 'Ε',
'&Zeta;': 'Ζ',
'&Eta;': 'Η',
'&Theta;': 'Θ',
'&Iota;': 'Ι',
'&Kappa;': 'Κ',
'&Lambda;': 'Λ',
'&Mu;': 'Μ',
'&Nu;': 'Ν',
'&Xi;': 'Ξ',
'&Omicron;': 'Ο',
'&Pi;': 'Π',
'&Rho;': 'Ρ',
'&Sigma;': 'Σ',
'&Tau;': 'Τ',
'&Upsilon;': 'Υ',
'&Phi;': 'Φ',
'&Chi;': 'Χ',
'&Psi;': 'Ψ',
'&Omega;': 'Ω',
// Latin extended
'&Agrave;': 'À',
'&Aacute;': 'Á',
'&Acirc;': 'Â',
'&Atilde;': 'Ã',
'&Auml;': 'Ä',
'&Aring;': 'Å',
'&AElig;': 'Æ',
'&Ccedil;': 'Ç',
'&Egrave;': 'È',
'&Eacute;': 'É',
'&Ecirc;': 'Ê',
'&Euml;': 'Ë',
'&Igrave;': 'Ì',
'&Iacute;': 'Í',
'&Icirc;': 'Î',
'&Iuml;': 'Ï',
'&ETH;': 'Ð',
'&Ntilde;': 'Ñ',
'&Ograve;': 'Ò',
'&Oacute;': 'Ó',
'&Ocirc;': 'Ô',
'&Otilde;': 'Õ',
'&Ouml;': 'Ö',
'&Oslash;': 'Ø',
'&Ugrave;': 'Ù',
'&Uacute;': 'Ú',
'&Ucirc;': 'Û',
'&Uuml;': 'Ü',
'&Yacute;': 'Ý',
'&THORN;': 'Þ',
'&szlig;': 'ß',
'&agrave;': 'à',
'&aacute;': 'á',
'&acirc;': 'â',
'&atilde;': 'ã',
'&auml;': 'ä',
'&aring;': 'å',
'&aelig;': 'æ',
'&ccedil;': 'ç',
'&egrave;': 'è',
'&eacute;': 'é',
'&ecirc;': 'ê',
'&euml;': 'ë',
'&igrave;': 'ì',
'&iacute;': 'í',
'&icirc;': 'î',
'&iuml;': 'ï',
'&eth;': 'ð',
'&ntilde;': 'ñ',
'&ograve;': 'ò',
'&oacute;': 'ó',
'&ocirc;': 'ô',
'&otilde;': 'õ',
'&ouml;': 'ö',
'&oslash;': 'ø',
'&ugrave;': 'ù',
'&uacute;': 'ú',
'&ucirc;': 'û',
'&uuml;': 'ü',
'&yacute;': 'ý',
'&thorn;': 'þ',
'&yuml;': 'ÿ',
// Special characters
'&iexcl;': '¡',
'&iquest;': '¿',
'&fnof;': 'ƒ',
'&circ;': 'ˆ',
'&tilde;': '˜',
'&OElig;': 'Œ',
'&oelig;': 'œ',
'&Scaron;': 'Š',
'&scaron;': 'š',
'&Yuml;': 'Ÿ',
'&ordf;': 'ª',
'&ordm;': 'º',
'&macr;': '¯',
'&acute;': '´',
'&cedil;': '¸',
'&sup1;': '¹',
'&sup2;': '²',
'&sup3;': '³',
'&frac14;': '¼',
'&frac12;': '½',
'&frac34;': '¾',
// Card suits
'&spades;': '♠',
'&clubs;': '♣',
'&hearts;': '♥',
'&diams;': '♦',
// Miscellaneous
'&loz;': '◊',
'&oline;': '‾',
'&frasl;': '⁄',
'&weierp;': '℘',
'&image;': 'ℑ',
'&real;': 'ℜ',
'&alefsym;': 'ℵ',
};
// Create regex pattern for all entities
const entityPattern = new RegExp(
Object.keys(commonEntities)
.map((entity) => entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('|'),
'g',
);
/**
* Decode HTML entities in text
*
* Uses a hybrid approach:
* 1. First pass: decode common named entities using a map
* 2. Second pass: decode numeric entities (decimal and hexadecimal)
*
* @param {string} text - Text with HTML entities
* @returns {string} Decoded text
*/
export const decodeHtmlEntities = (text) =>
text
// First pass: common named entities
.replace(entityPattern, (match) => commonEntities[match])
// Second pass: numeric entities (decimal)
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))
// Third pass: numeric entities (hexadecimal)
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
+4
View File
@@ -0,0 +1,4 @@
export * from './TranslationParserError.js';
export * from './htmlEntityDecoder.js';
export * from './tokenizer.js';
export * from './renderTranslation.js';
+215
View File
@@ -0,0 +1,215 @@
import React from 'react';
import { TranslationParserError } from './TranslationParserError.js';
import { tokenize } from './tokenizer.js';
import { decodeHtmlEntities } from './htmlEntityDecoder.js';
/**
* Render a React element tree from a declaration node and its children
*
* @param {Object} declaration - The component declaration (type + props)
* @param {Array<React.ReactNode>} children - Array of child nodes (text, numbers, React elements)
* @param {Array<Object>} [childDeclarations] - Optional array of child declarations to use for nested rendering
* @returns {React.ReactElement} A React element
*/
const renderDeclarationNode = (declaration, children, childDeclarations) => {
const { type, props = {} } = declaration;
// If props contain a children declaration AND we have childDeclarations to work with,
// we need to recursively render the content with those child declarations
if (props.children && Array.isArray(props.children) && childDeclarations) {
// The children array contains the parsed content from inside this tag
// We need to rebuild the translation string and re-parse it with child declarations
// For now, we'll use the children directly as they're already parsed
// This happens when renderTranslation is called recursively
// Remove children from props since we'll pass them as the third argument
// eslint-disable-next-line no-unused-vars
const { children: _childrenToRemove, ...propsWithoutChildren } = props;
return React.createElement(type, propsWithoutChildren, ...children);
}
// Standard rendering with children from translation
if (children.length === 0) {
return React.createElement(type, props);
}
if (children.length === 1) {
return React.createElement(type, props, children[0]);
}
return React.createElement(type, props, ...children);
};
/**
* Render translation string with declaration tree to create React elements
*
* This function parses an ICU format translation string and reconstructs
* a React element tree using the provided declaration tree. It replaces
* numbered tags (e.g., <0>, <1>) with the corresponding components from
* the declaration array and fills them with the translated text.
*
* @param {string} translation - ICU format string (e.g., "<0>Click here</0>")
* @param {Array<Object>} [declarations=[]] - Array of component declarations matching tag numbers
* @returns {Array<React.ReactNode>} Array of React nodes (elements and text)
*
* @example
* ```jsx
* const result = renderTranslation(
* "<0>bonjour</0> monde",
* [{ type: 'strong', props: { className: 'bold' } }]
* );
* // Returns: [<strong className="bold">bonjour</strong>, " monde"]
* ```
*
* @example
* ```jsx
* // With nested children in declaration
* const result = renderTranslation(
* "<0>Click <1>here</1></0>",
* [
* {
* type: 'div',
* props: {
* children: [{ type: 'span', props: {} }]
* }
* }
* ]
* );
* ```
*/
export const renderTranslation = (translation, declarations = []) => {
if (!translation) {
return [];
}
const tokens = tokenize(translation);
const result = [];
const stack = [];
// Track tag numbers that should be treated as literal text (no declaration found)
const literalTagNumbers = new Set();
// Helper to get the current declarations array based on context
const getCurrentDeclarations = () => {
if (stack.length === 0) {
return declarations;
}
const parentFrame = stack[stack.length - 1];
// If the parent declaration has children declarations, use those
if (
parentFrame.declaration.props?.children &&
Array.isArray(parentFrame.declaration.props.children)
) {
return parentFrame.declaration.props.children;
}
// Otherwise, use the parent's declarations array
return parentFrame.declarations;
};
tokens.forEach((token) => {
// eslint-disable-next-line default-case
switch (token.type) {
case 'Text':
{
const decoded = decodeHtmlEntities(token.value);
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(decoded);
}
break;
case 'TagOpen':
{
const { tagNumber } = token;
const currentDeclarations = getCurrentDeclarations();
const declaration = currentDeclarations[tagNumber];
if (!declaration) {
// No declaration found - treat this tag as literal text
literalTagNumbers.add(tagNumber);
const literalText = `<${tagNumber}>`;
const targetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
targetArray.push(literalText);
break;
}
stack.push({
tagNumber,
children: [],
position: token.position,
declaration,
declarations: currentDeclarations,
});
}
break;
case 'TagClose':
{
const { tagNumber } = token;
// If this tag was treated as literal, output the closing tag as literal text
if (literalTagNumbers.has(tagNumber)) {
const literalText = `</${tagNumber}>`;
const literalTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
literalTargetArray.push(literalText);
literalTagNumbers.delete(tagNumber);
break;
}
if (stack.length === 0) {
throw new TranslationParserError(
`Unexpected closing tag </${tagNumber}> at position ${token.position}`,
token.position,
translation,
);
}
const frame = stack.pop();
if (frame.tagNumber !== tagNumber) {
throw new TranslationParserError(
`Mismatched tags: expected </${frame.tagNumber}> but got </${tagNumber}> at position ${token.position}`,
token.position,
translation,
);
}
// Render the element using the declaration and collected children
const element = renderDeclarationNode(
frame.declaration,
frame.children,
frame.declarations,
);
const elementTargetArray = stack.length > 0 ? stack[stack.length - 1].children : result;
elementTargetArray.push(element);
}
break;
}
});
if (stack.length > 0) {
const unclosed = stack[stack.length - 1];
throw new TranslationParserError(
`Unclosed tag <${unclosed.tagNumber}> at position ${unclosed.position}`,
unclosed.position,
translation,
);
}
return result;
};
+78
View File
@@ -0,0 +1,78 @@
/**
* Tokenize a translation string with numbered tags
* Note: Variables are already interpolated by the i18n system before we receive the string
*
* @param {string} translation - Translation string with numbered tags
* @returns {Array<Token>} Array of tokens
*/
export const tokenize = (translation) => {
const tokens = [];
let position = 0;
let currentText = '';
const flushText = () => {
if (currentText) {
tokens.push({
type: 'Text',
value: currentText,
position: position - currentText.length,
});
currentText = '';
}
};
while (position < translation.length) {
const char = translation[position];
// Check for opening tag: <0>, <1>, etc.
if (char === '<') {
const tagMatch = translation.slice(position).match(/^<(\d+)>/);
if (tagMatch) {
flushText();
tokens.push({
type: 'TagOpen',
value: tagMatch[0],
position,
tagNumber: parseInt(tagMatch[1], 10),
});
position += tagMatch[0].length;
} else {
// Check for closing tag: </0>, </1>, etc.
const closeTagMatch = translation.slice(position).match(/^<\/(\d+)>/);
if (closeTagMatch) {
flushText();
tokens.push({
type: 'TagClose',
value: closeTagMatch[0],
position,
tagNumber: parseInt(closeTagMatch[1], 10),
});
position += closeTagMatch[0].length;
} else {
// Regular text (including any { } characters that aren't our tags)
currentText += char;
position += 1;
}
}
} else {
// Regular text (including any { } characters that aren't our tags)
currentText += char;
position += 1;
}
}
flushText();
return tokens;
};
+146
View File
@@ -0,0 +1,146 @@
import React from 'react';
import { warn, warnOnce, isString } from './utils.js';
import { getI18n } from './i18nInstance.js';
import { renderTranslation } from './IcuTransUtils/index.js';
/**
* IcuTrans component for rendering ICU MessageFormat translations (without React Context)
*
* This is the core implementation without React hooks or context dependencies,
* making it suitable for use in any environment. It uses a declaration tree
* approach where components are defined as type + props blueprints, fetches
* the translated string via i18next, and reconstructs the React element tree
* by replacing numbered tags (<0>, <1>) with actual components.
*
* Key features:
* - No React hooks or context (can be used anywhere)
* - ICU MessageFormat compatible
* - Supports nested component declarations
* - Automatic HTML entity decoding
* - Graceful error handling with fallbacks
* - Merges default interpolation variables
*
* Note: Users should typically use the IcuTrans export which provides automatic
* context support. This component is exposed for advanced use cases where direct
* i18n instance control is needed, or for use outside of React Context.
*
* @param {Object} props - Component props
* @param {string} props.i18nKey - The i18n key to look up the translation
* @param {string} props.defaultTranslation - The default translation in ICU format with numbered tags (e.g., "<0>Click here</0>")
* @param {Array<{type: string|React.ComponentType, props?: Object}>} props.content - Declaration tree describing React components and their props
* @param {string|string[]} [props.ns] - Optional namespace(s) for the translation. Falls back to t.ns, then i18n.options.defaultNS, then 'translation'
* @param {Object} [props.values={}] - Optional values for ICU variable interpolation (merged with i18n.options.interpolation.defaultVariables if present)
* @param {Object} [props.i18n] - i18next instance. If not provided, uses global instance from getI18n()
* @param {Function} [props.t] - Custom translation function. If not provided, uses i18n.t.bind(i18n)
* @returns {React.ReactElement} React fragment containing the rendered translation
*
* @example
* ```jsx
* // Direct usage with i18n instance
* <IcuTransWithoutContext
* i18nKey="welcome.message"
* defaultTranslation="Welcome <0>back</0>!"
* content={[
* { type: 'strong', props: { className: 'highlight' } }
* ]}
* i18n={i18nInstance}
* />
* ```
*
* @example
* ```jsx
* // With nested declarations for list rendering
* <IcuTransWithoutContext
* i18nKey="features.list"
* defaultTranslation="Features: <0><0>Fast</0><1>Reliable</1><2>Secure</2></0>"
* content={[
* {
* type: 'ul',
* props: {
* children: [
* { type: 'li', props: {} },
* { type: 'li', props: {} },
* { type: 'li', props: {} }
* ]
* }
* }
* ]}
* i18n={i18nInstance}
* />
* ```
*
* @example
* ```jsx
* // With values for ICU variable interpolation
* <IcuTransWithoutContext
* i18nKey="greeting"
* defaultTranslation="Hello <0>{name}</0>!"
* content={[{ type: 'strong', props: {} }]}
* values={{ name: 'Alice' }}
* i18n={i18nInstance}
* />
* ```
*/
export function IcuTransWithoutContext({
i18nKey,
defaultTranslation,
content,
ns,
values = {},
i18n: i18nFromProps,
t: tFromProps,
}) {
const i18n = i18nFromProps || getI18n();
if (!i18n) {
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
`IcuTrans: You need to pass in an i18next instance using i18nextReactModule`,
{ i18nKey },
);
return React.createElement(React.Fragment, {}, defaultTranslation);
}
const t = tFromProps || i18n.t?.bind(i18n) || ((k) => k);
// prepare having a namespace
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
// Merge default interpolation variables if they exist
let mergedValues = values;
if (i18n.options?.interpolation?.defaultVariables) {
mergedValues =
values && Object.keys(values).length > 0
? { ...values, ...i18n.options.interpolation.defaultVariables }
: { ...i18n.options.interpolation.defaultVariables };
}
// Get the translation, falling back to defaultTranslation
const translation = t(i18nKey, {
defaultValue: defaultTranslation,
...mergedValues,
ns: namespaces,
});
// Render the translation with the declaration tree
try {
const rendered = renderTranslation(translation, content);
// Return as a React fragment to avoid extra wrapper
return React.createElement(React.Fragment, {}, ...rendered);
} catch (error) {
// If rendering fails, warn and fall back to the translation string
warn(
i18n,
'ICU_TRANS_RENDER_ERROR',
`IcuTrans component error for key "${i18nKey}": ${error.message}`,
{ i18nKey, error },
);
return React.createElement(React.Fragment, {}, translation);
}
}
IcuTransWithoutContext.displayName = 'IcuTransWithoutContext';
+45
View File
@@ -0,0 +1,45 @@
import { useContext } from 'react';
import { nodesToString, Trans as TransWithoutContext } from './TransWithoutContext.js';
import { getI18n, I18nContext } from './context.js';
export { nodesToString };
export function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
const t = tFromProps || i18n?.t.bind(i18n);
return TransWithoutContext({
children,
count,
parent,
i18nKey,
context,
tOptions,
values,
defaults,
components,
// prepare having a namespace
ns: ns || t?.ns || defaultNSFromContext || i18n?.options?.defaultNS,
i18n,
t: tFromProps,
shouldUnescape,
...additionalProps,
});
}
+631
View File
@@ -0,0 +1,631 @@
import { Fragment, isValidElement, cloneElement, createElement, Children } from 'react';
import { keyFromSelector } from 'i18next';
import HTML from 'html-parse-stringify';
import { isObject, isString, warn, warnOnce } from './utils.js';
import { getDefaults } from './defaults.js';
import { getI18n } from './i18nInstance.js';
import { unescape } from './unescape.js';
const hasChildren = (node, checkLength) => {
if (!node) return false;
const base = node.props?.children ?? node.children;
if (checkLength) return base.length > 0;
return !!base;
};
const getChildren = (node) => {
if (!node) return [];
const children = node.props?.children ?? node.children;
return node.props?.i18nIsDynamicList ? getAsArray(children) : children;
};
const hasValidReactChildren = (children) =>
Array.isArray(children) && children.every(isValidElement);
const getAsArray = (data) => (Array.isArray(data) ? data : [data]);
const mergeProps = (source, target) => {
const newTarget = { ...target };
// translation props (source.props) should override component props (target.props)
newTarget.props = { ...target.props, ...source.props };
return newTarget;
};
const getValuesFromChildren = (children) => {
const values = {};
if (!children) return values;
const getData = (childs) => {
const childrenArray = getAsArray(childs);
childrenArray.forEach((child) => {
if (isString(child)) return;
if (hasChildren(child)) getData(getChildren(child));
else if (isObject(child) && !isValidElement(child)) Object.assign(values, child);
});
};
getData(children);
return values;
};
export const nodesToString = (children, i18nOptions, i18n, i18nKey) => {
if (!children) return '';
let stringNode = '';
// do not use `React.Children.toArray`, will fail at object children
const childrenArray = getAsArray(children);
const keepArray = i18nOptions?.transSupportBasicHtmlNodes
? (i18nOptions.transKeepBasicHtmlNodesFor ?? [])
: [];
// e.g. lorem <br/> ipsum {{ messageCount, format }} dolor <strong>bold</strong> amet
childrenArray.forEach((child, childIndex) => {
if (isString(child)) {
// actual e.g. lorem
// expected e.g. lorem
stringNode += `${child}`;
return;
}
if (isValidElement(child)) {
const { props, type } = child;
const childPropsCount = Object.keys(props).length;
const shouldKeepChild = keepArray.indexOf(type) > -1;
const childChildren = props.children;
if (!childChildren && shouldKeepChild && !childPropsCount) {
// actual e.g. lorem <br/> ipsum
// expected e.g. lorem <br/> ipsum
stringNode += `<${type}/>`;
return;
}
if ((!childChildren && (!shouldKeepChild || childPropsCount)) || props.i18nIsDynamicList) {
// actual e.g. lorem <hr className="test" /> ipsum
// expected e.g. lorem <0></0> ipsum
// or
// we got a dynamic list like
// e.g. <ul i18nIsDynamicList>{['a', 'b'].map(item => ( <li key={item}>{item}</li> ))}</ul>
// expected e.g. "<0></0>", not e.g. "<0><0>a</0><1>b</1></0>"
stringNode += `<${childIndex}></${childIndex}>`;
return;
}
if (shouldKeepChild && childPropsCount <= 1) {
// actual e.g. dolor <strong>bold</strong> amet
// expected e.g. dolor <strong>bold</strong> amet
const cnt = isString(childChildren)
? childChildren
: nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${type}>${cnt}</${type}>`;
return;
}
// regular case mapping the inner children
const content = nodesToString(childChildren, i18nOptions, i18n, i18nKey);
stringNode += `<${childIndex}>${content}</${childIndex}>`;
return;
}
if (child === null) {
warn(i18n, 'TRANS_NULL_VALUE', `Passed in a null value as child`, { i18nKey });
return;
}
if (isObject(child)) {
// e.g. lorem {{ value, format }} ipsum
const { format, ...clone } = child;
const keys = Object.keys(clone);
if (keys.length === 1) {
const value = format ? `${keys[0]}, ${format}` : keys[0];
stringNode += `{{${value}}}`;
return;
}
warn(
i18n,
'TRANS_INVALID_OBJ',
`Invalid child - Object should only have keys {{ value, format }} (format is optional).`,
{ i18nKey, child },
);
return;
}
warn(
i18n,
'TRANS_INVALID_VAR',
`Passed in a variable like {number} - pass variables for interpolation as full objects like {{number}}.`,
{ i18nKey, child },
);
});
return stringNode;
};
/**
* Escape literal < characters that are not part of valid tags
* Valid tags are: numbered tags like <0>, </0> or named tags from keepArray/knownComponents
* @param {string} str - The string to escape
* @param {Array<string>} keepArray - Array of HTML tag names to keep
* @param {Object} knownComponentsMap - Map of known component names
* @returns {string} String with literal < characters escaped
*/
const escapeLiteralLessThan = (str, keepArray = [], knownComponentsMap = {}) => {
if (!str) return str;
// Build a list of valid tag names (numbered indices and known component names)
const knownNames = Object.keys(knownComponentsMap);
const allValidNames = [...keepArray, ...knownNames];
// Pattern to match:
// 1. Opening tags: <number> or <name> where name is in allValidNames
// 2. Closing tags: </number> or </name> where name is in allValidNames
// 3. Self-closing tags: <name/> or <name /> where name is in keepArray
// Everything else starting with < should be escaped
let result = '';
let i = 0;
while (i < str.length) {
if (str[i] === '<') {
// Check if this is a valid tag
let isValidTag = false;
// Check for closing tag: </number> or </name>
const closingMatch = str.slice(i).match(/^<\/(\d+|[a-zA-Z][a-zA-Z0-9_-]*)>/);
if (closingMatch) {
const tagName = closingMatch[1];
// Valid if it's a number or in our valid names list
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += closingMatch[0];
i += closingMatch[0].length;
}
}
// Check for opening tag: <number> or <name> or <name/> or <name />
// Also handle tags with attributes: <0 href="..."> or <name class="...">
if (!isValidTag) {
// Match: <tagName [attributes] [/]>
// Attributes pattern: name="value" or name='value' or name (boolean)
const openingMatch = str
.slice(i)
.match(
/^<(\d+|[a-zA-Z][a-zA-Z0-9_-]*)(\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?)*\s*(\/)?>/,
);
if (openingMatch) {
const tagName = openingMatch[1];
// Valid if it's a number or in our valid names list
if (/^\d+$/.test(tagName) || allValidNames.includes(tagName)) {
isValidTag = true;
result += openingMatch[0];
i += openingMatch[0].length;
}
}
}
// If not a valid tag, escape the <
if (!isValidTag) {
result += '&lt;';
i += 1;
}
} else {
result += str[i];
i += 1;
}
}
return result;
};
const renderNodes = (
children,
knownComponentsMap,
targetString,
i18n,
i18nOptions,
combinedTOpts,
shouldUnescape,
) => {
if (targetString === '') return [];
// check if contains tags we need to replace from html string to react nodes
const keepArray = i18nOptions.transKeepBasicHtmlNodesFor || [];
const emptyChildrenButNeedsHandling =
targetString && new RegExp(keepArray.map((keep) => `<${keep}`).join('|')).test(targetString);
// no need to replace tags in the targetstring
if (!children && !knownComponentsMap && !emptyChildrenButNeedsHandling && !shouldUnescape)
return [targetString];
// v2 -> interpolates upfront no need for "some <0>{{var}}</0>"" -> will be just "some {{var}}" in translation file
const data = knownComponentsMap ?? {};
const getData = (childs) => {
const childrenArray = getAsArray(childs);
childrenArray.forEach((child) => {
if (isString(child)) return;
if (hasChildren(child)) getData(getChildren(child));
else if (isObject(child) && !isValidElement(child)) Object.assign(data, child);
});
};
getData(children);
// Escape literal < characters that are not part of valid tags before parsing
const escapedString = escapeLiteralLessThan(targetString, keepArray, data);
// parse ast from string with additional wrapper tag
// -> avoids issues in parser removing prepending text nodes
const ast = HTML.parse(`<0>${escapedString}</0>`);
const opts = { ...data, ...combinedTOpts };
const renderInner = (child, node, rootReactNode) => {
const childs = getChildren(child);
const mappedChildren = mapAST(childs, node.children, rootReactNode);
// `mappedChildren` will always be empty if using the `i18nIsDynamicList` prop,
// but the children might not necessarily be react components
return (hasValidReactChildren(childs) && mappedChildren.length === 0) ||
child.props?.i18nIsDynamicList
? childs
: mappedChildren;
};
const pushTranslatedJSX = (child, inner, mem, i, isVoid) => {
if (child.dummy) {
child.children = inner; // needed on preact!
mem.push(cloneElement(child, { key: i }, isVoid ? undefined : inner));
} else {
mem.push(
...Children.map([child], (c) => {
// Build an override props object while deliberately NOT reading c.ref or c.props.ref
// use a DOM-safe marker name and never forward it to DOM nodes
const INTERNAL_DYNAMIC_MARKER = 'data-i18n-is-dynamic-list';
const override = { key: i, [INTERNAL_DYNAMIC_MARKER]: undefined };
if (c && c.props) {
Object.keys(c.props).forEach((k) => {
// skip special/internal props and the dynamic-list marker so it never reaches DOM
if (
k === 'ref' ||
k === 'children' ||
k === 'i18nIsDynamicList' ||
k === INTERNAL_DYNAMIC_MARKER
)
return;
override[k] = c.props[k];
});
}
// Use cloneElement for all element types so React preserves/forwards refs internally
// and we don't access element.ref nor c.props.ref ourselves.
return cloneElement(c, override, isVoid ? null : inner);
}),
);
}
};
// reactNode (the jsx root element or child)
// astNode (the translation string as html ast)
// rootReactNode (the most outer jsx children array or trans components prop)
const mapAST = (reactNode, astNode, rootReactNode) => {
const reactNodes = getAsArray(reactNode);
const astNodes = getAsArray(astNode);
return astNodes.reduce((mem, node, i) => {
const translationContent =
node.children?.[0]?.content &&
i18n.services.interpolator.interpolate(node.children[0].content, opts, i18n.language);
if (node.type === 'tag') {
// regular array (components or children)
let tmp = reactNodes[parseInt(node.name, 10)];
if (!tmp && knownComponentsMap) tmp = knownComponentsMap[node.name];
// trans components is an object
if (rootReactNode.length === 1 && !tmp) tmp = rootReactNode[0][node.name];
// neither
if (!tmp) tmp = {};
// should fix #1893
const props = { ...node.attrs };
if (shouldUnescape) {
Object.keys(props).forEach((p) => {
const val = props[p];
if (isString(val)) {
props[p] = unescape(val);
}
});
}
const child = Object.keys(props).length !== 0 ? mergeProps({ props }, tmp) : tmp;
const isElement = isValidElement(child);
const isValidTranslationWithChildren =
isElement && hasChildren(node, true) && !node.voidElement;
const isEmptyTransWithHTML =
emptyChildrenButNeedsHandling && isObject(child) && child.dummy && !isElement;
const isKnownComponent =
isObject(knownComponentsMap) && Object.hasOwnProperty.call(knownComponentsMap, node.name);
if (isString(child)) {
const value = i18n.services.interpolator.interpolate(child, opts, i18n.language);
mem.push(value);
} else if (
hasChildren(child) || // the jsx element has children -> loop
isValidTranslationWithChildren // valid jsx element with no children but the translation has -> loop
) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i);
} else if (isEmptyTransWithHTML) {
// we have a empty Trans node (the dummy element) with a targetstring that contains html tags needing
// conversion to react nodes
// so we just need to map the inner stuff
const inner = mapAST(
reactNodes /* wrong but we need something */,
node.children,
rootReactNode,
);
pushTranslatedJSX(child, inner, mem, i);
} else if (Number.isNaN(parseFloat(node.name))) {
if (isKnownComponent) {
const inner = renderInner(child, node, rootReactNode);
pushTranslatedJSX(child, inner, mem, i, node.voidElement);
} else if (i18nOptions.transSupportBasicHtmlNodes && keepArray.indexOf(node.name) > -1) {
if (node.voidElement) {
mem.push(createElement(node.name, { key: `${node.name}-${i}` }));
} else {
const inner = mapAST(
reactNodes /* wrong but we need something */,
node.children,
rootReactNode,
);
mem.push(createElement(node.name, { key: `${node.name}-${i}` }, inner));
}
} else if (node.voidElement) {
mem.push(`<${node.name} />`);
} else {
const inner = mapAST(
reactNodes /* wrong but we need something */,
node.children,
rootReactNode,
);
mem.push(`<${node.name}>${inner}</${node.name}>`);
}
} else if (isObject(child) && !isElement) {
const content = node.children[0] ? translationContent : null;
// v1
// as interpolation was done already we just have a regular content node
// in the translation AST while having an object in reactNodes
// -> push the content no need to interpolate again
if (content) mem.push(content);
} else {
// If component does not have children, but translation - has
// with this in component could be components={[<span class='make-beautiful'/>]} and in translation - 'some text <0>some highlighted message</0>'
pushTranslatedJSX(
child,
translationContent,
mem,
i,
node.children.length !== 1 || !translationContent,
);
}
} else if (node.type === 'text') {
const wrapTextNodes = i18nOptions.transWrapTextNodes;
const unescapeFn =
typeof i18nOptions.unescape === 'function'
? i18nOptions.unescape
: getDefaults().unescape;
const content = shouldUnescape
? unescapeFn(i18n.services.interpolator.interpolate(node.content, opts, i18n.language))
: i18n.services.interpolator.interpolate(node.content, opts, i18n.language);
if (wrapTextNodes) {
mem.push(createElement(wrapTextNodes, { key: `${node.name}-${i}` }, content));
} else {
mem.push(content);
}
}
return mem;
}, []);
};
// call mapAST with having react nodes nested into additional node like
// we did for the string ast from translation
// return the children of that extra node to get expected result
const result = mapAST(
[{ dummy: true, children: children || [] }],
ast,
getAsArray(children || []),
);
return getChildren(result[0]);
};
const fixComponentProps = (component, index, translation) => {
const componentKey = component.key || index;
const comp = cloneElement(component, { key: componentKey });
if (
!comp.props ||
!comp.props.children ||
(translation.indexOf(`${index}/>`) < 0 && translation.indexOf(`${index} />`) < 0)
) {
return comp;
}
function Componentized() {
// <>{comp}</>
return createElement(Fragment, null, comp);
}
// <Componentized />
return createElement(Componentized, { key: componentKey });
};
const generateArrayComponents = (components, translation) =>
components.map((c, index) => fixComponentProps(c, index, translation));
const generateObjectComponents = (components, translation) => {
const componentMap = {};
Object.keys(components).forEach((c) => {
Object.assign(componentMap, {
[c]: fixComponentProps(components[c], c, translation),
});
});
return componentMap;
};
const generateComponents = (components, translation, i18n, i18nKey) => {
if (!components) return null;
// components could be either an array or an object
if (Array.isArray(components)) {
return generateArrayComponents(components, translation);
}
if (isObject(components)) {
return generateObjectComponents(components, translation);
}
// if components is not an array or an object, warn the user
// and return null
warnOnce(
i18n,
'TRANS_INVALID_COMPONENTS',
`<Trans /> "components" prop expects an object or array`,
{ i18nKey },
);
return null;
};
// A component map is an object like: { Button: <button> }, but not an object like { 1: <button> }
const isComponentsMap = (object) => {
if (!isObject(object)) return false;
if (Array.isArray(object)) return false;
return Object.keys(object).reduce(
(acc, key) => acc && Number.isNaN(Number.parseFloat(key)),
true,
);
};
export function Trans({
children,
count,
parent,
i18nKey,
context,
tOptions = {},
values,
defaults,
components,
ns,
i18n: i18nFromProps,
t: tFromProps,
shouldUnescape,
...additionalProps
}) {
const i18n = i18nFromProps || getI18n();
if (!i18n) {
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
`Trans: You need to pass in an i18next instance using i18nextReactModule`,
{ i18nKey },
);
return children;
}
const t = tFromProps || i18n.t.bind(i18n) || ((k) => k);
const reactI18nextOptions = { ...getDefaults(), ...i18n.options?.react };
// prepare having a namespace
let namespaces = ns || t.ns || i18n.options?.defaultNS;
namespaces = isString(namespaces) ? [namespaces] : namespaces || ['translation'];
const { transDefaultProps } = reactI18nextOptions;
const mergedTOptions = transDefaultProps?.tOptions
? { ...transDefaultProps.tOptions, ...tOptions }
: tOptions;
const mergedShouldUnescape = shouldUnescape ?? transDefaultProps?.shouldUnescape;
const mergedValues = transDefaultProps?.values
? { ...transDefaultProps.values, ...values }
: values;
const mergedComponents = transDefaultProps?.components
? { ...transDefaultProps.components, ...components }
: components;
const nodeAsString = nodesToString(children, reactI18nextOptions, i18n, i18nKey);
const defaultValue =
defaults ||
mergedTOptions?.defaultValue ||
nodeAsString ||
reactI18nextOptions.transEmptyNodeValue ||
(typeof i18nKey === 'function' ? keyFromSelector(i18nKey) : i18nKey);
const { hashTransKey } = reactI18nextOptions;
const key =
i18nKey ||
(hashTransKey ? hashTransKey(nodeAsString || defaultValue) : nodeAsString || defaultValue);
if (i18n.options?.interpolation?.defaultVariables) {
// eslint-disable-next-line no-param-reassign
values =
mergedValues && Object.keys(mergedValues).length > 0
? { ...mergedValues, ...i18n.options.interpolation.defaultVariables }
: { ...i18n.options.interpolation.defaultVariables };
} else {
// eslint-disable-next-line no-param-reassign
values = mergedValues;
}
const valuesFromChildren = getValuesFromChildren(children);
if (valuesFromChildren && typeof valuesFromChildren.count === 'number' && count === undefined) {
// eslint-disable-next-line no-param-reassign
count = valuesFromChildren.count;
}
const interpolationOverride =
values ||
(count !== undefined && !i18n.options?.interpolation?.alwaysFormat) || // https://github.com/i18next/react-i18next/issues/1719 + https://github.com/i18next/react-i18next/issues/1801
!children // if !children gets problems in future, undo that fix: https://github.com/i18next/react-i18next/issues/1729 by removing !children from this condition
? mergedTOptions.interpolation
: { interpolation: { ...mergedTOptions.interpolation, prefix: '#$?', suffix: '?$#' } };
const combinedTOpts = {
...mergedTOptions,
context: context || mergedTOptions.context, // Add `context` from the props or fallback to the value from `tOptions`
count,
...values,
...interpolationOverride,
defaultValue,
ns: namespaces,
};
let translation = key ? t(key, combinedTOpts) : defaultValue;
if (translation === key && defaultValue) translation = defaultValue;
const generatedComponents = generateComponents(mergedComponents, translation, i18n, i18nKey);
let indexedChildren = generatedComponents || children;
let componentsMap = null;
if (isComponentsMap(generatedComponents)) {
componentsMap = generatedComponents;
indexedChildren = children;
}
const content = renderNodes(
indexedChildren,
componentsMap,
translation,
i18n,
reactI18nextOptions,
combinedTOpts,
mergedShouldUnescape,
);
// allows user to pass `null` to `parent`
// and override `defaultTransParent` if is present
const useAsParent = parent ?? reactI18nextOptions.defaultTransParent;
return useAsParent ? createElement(useAsParent, additionalProps, content) : content;
}
+14
View File
@@ -0,0 +1,14 @@
import { useTranslation } from './useTranslation.js';
export const Translation = ({ ns, children, ...options }) => {
const [t, i18n, ready] = useTranslation(ns, options);
return children(
t,
{
i18n,
lng: i18n?.language,
},
ready,
);
};
+60
View File
@@ -0,0 +1,60 @@
import { createContext } from 'react';
import { getDefaults, setDefaults } from './defaults.js';
import { getI18n, setI18n } from './i18nInstance.js';
import { initReactI18next } from './initReactI18next.js';
export { getDefaults, setDefaults, getI18n, setI18n, initReactI18next };
export const I18nContext = createContext();
export class ReportNamespaces {
constructor() {
this.usedNamespaces = {};
}
addUsedNamespaces(namespaces) {
namespaces.forEach((ns) => {
if (!this.usedNamespaces[ns]) this.usedNamespaces[ns] = true;
});
}
getUsedNamespaces() {
return Object.keys(this.usedNamespaces);
}
}
export const composeInitialProps = (ForComponent) => async (ctx) => {
const componentsInitialProps = (await ForComponent.getInitialProps?.(ctx)) ?? {};
const i18nInitialProps = getInitialProps();
return {
...componentsInitialProps,
...i18nInitialProps,
};
};
export const getInitialProps = () => {
const i18n = getI18n();
if (!i18n) {
console.warn(
'react-i18next:: getInitialProps: You will need to pass in an i18next instance by using initReactI18next',
);
return {};
}
const namespaces = i18n.reportNamespaces?.getUsedNamespaces() ?? [];
const ret = {};
const initialI18nStore = {};
i18n.languages.forEach((l) => {
initialI18nStore[l] = {};
namespaces.forEach((ns) => {
initialI18nStore[l][ns] = i18n.getResourceBundle(l, ns) || {};
});
});
ret.initialI18nStore = initialI18nStore;
ret.initialLanguage = i18n.language;
return ret;
};
+21
View File
@@ -0,0 +1,21 @@
import { unescape } from './unescape.js';
let defaultOptions = {
bindI18n: 'languageChanged',
bindI18nStore: '',
// nsMode: 'fallback' // loop through all namespaces given to hook, HOC, render prop for key lookup
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transWrapTextNodes: '',
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'p'],
// hashTransKey: key => key // calculate a key for Trans component based on defaultValue
useSuspense: true,
unescape,
transDefaultProps: undefined, // { tOptions: {}, shouldUnescape: false, values: {}, components: [] }
};
export const setDefaults = (options = {}) => {
defaultOptions = { ...defaultOptions, ...options };
};
export const getDefaults = () => defaultOptions;
+7
View File
@@ -0,0 +1,7 @@
let i18nInstance;
export const setI18n = (instance) => {
i18nInstance = instance;
};
export const getI18n = () => i18nInstance;
+26
View File
@@ -0,0 +1,26 @@
export { Trans } from './Trans.js';
export { Trans as TransWithoutContext } from './TransWithoutContext.js';
export { IcuTrans } from './IcuTrans.js';
export { IcuTransWithoutContext } from './IcuTransWithoutContext.js';
export { useTranslation } from './useTranslation.js';
export { withTranslation } from './withTranslation.js';
export { Translation } from './Translation.js';
export { I18nextProvider } from './I18nextProvider.js';
export { withSSR } from './withSSR.js';
export { useSSR } from './useSSR.js';
export { initReactI18next } from './initReactI18next.js';
export { setDefaults, getDefaults } from './defaults.js';
export { setI18n, getI18n } from './i18nInstance.js';
export { nodesToString } from './Trans.js';
export { I18nContext, composeInitialProps, getInitialProps } from './context.js';
// dummy functions for icu.macro support
export const date = () => '';
export const time = () => '';
export const number = () => '';
export const select = () => '';
export const plural = () => '';
export const selectOrdinal = () => '';
+11
View File
@@ -0,0 +1,11 @@
import { setDefaults } from './defaults.js';
import { setI18n } from './i18nInstance.js';
export const initReactI18next = {
type: '3rdParty',
init(instance) {
setDefaults(instance.options.react);
setI18n(instance);
},
};
+31
View File
@@ -0,0 +1,31 @@
// unescape common html entities
const matchHtmlEntity =
/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
const htmlEntities = {
'&amp;': '&',
'&#38;': '&',
'&lt;': '<',
'&#60;': '<',
'&gt;': '>',
'&#62;': '>',
'&apos;': "'",
'&#39;': "'",
'&quot;': '"',
'&#34;': '"',
'&nbsp;': ' ',
'&#160;': ' ',
'&copy;': '©',
'&#169;': '©',
'&reg;': '®',
'&#174;': '®',
'&hellip;': '…',
'&#8230;': '…',
'&#x2F;': '/',
'&#47;': '/',
};
const unescapeHtmlEntity = (m) => htmlEntities[m];
export const unescape = (text) => text.replace(matchHtmlEntity, unescapeHtmlEntity);
+51
View File
@@ -0,0 +1,51 @@
import { useContext } from 'react';
import { getI18n, I18nContext } from './context.js';
import { warnOnce } from './utils.js';
export const useSSR = (initialI18nStore, initialLanguage, props = {}) => {
const { i18n: i18nFromProps } = props;
const { i18n: i18nFromContext } = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (!i18n) {
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
'useSSR: You will need to pass in an i18next instance by using initReactI18next or by passing it via props or context. In monorepo setups, make sure there is only one instance of react-i18next.',
);
return;
}
// opt out if is a cloned instance, eg. created by i18next-http-middleware on request
// -> do not set initial stuff on server side
if (i18n.options?.isClone) return;
// nextjs / SSR: getting data from next.js or other ssr stack
if (initialI18nStore && !i18n.initializedStoreOnce) {
if (!i18n.services?.resourceStore) {
warnOnce(
i18n,
'I18N_NOT_INITIALIZED',
'useSSR: i18n instance was found but not initialized (services.resourceStore is missing). Make sure you call i18next.init() before using useSSR — e.g. at module level, not only in getStaticProps/getServerSideProps.',
);
return;
}
i18n.services.resourceStore.data = initialI18nStore;
// add namespaces to the config - so a languageChange call loads all namespaces needed
i18n.options.ns = Object.values(initialI18nStore).reduce((mem, lngResources) => {
Object.keys(lngResources).forEach((ns) => {
if (mem.indexOf(ns) < 0) mem.push(ns);
});
return mem;
}, i18n.options.ns);
i18n.initializedStoreOnce = true;
i18n.isInitialized = true;
}
if (initialLanguage && !i18n.initializedLanguageOnce) {
i18n.changeLanguage(initialLanguage);
i18n.initializedLanguageOnce = true;
}
};
+222
View File
@@ -0,0 +1,222 @@
import { useContext, useCallback, useMemo, useEffect, useRef, useState } from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { getI18n, getDefaults, ReportNamespaces, I18nContext } from './context.js';
import {
warnOnce,
loadNamespaces,
loadLanguages,
hasLoadedNamespace,
isString,
isObject,
} from './utils.js';
const notReadyT = (k, optsOrDefaultValue) => {
if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue))
return optsOrDefaultValue.defaultValue;
// Selector functions and arrays of selector functions cannot be meaningfully resolved
// before i18n is ready — return empty string rather than leaking a function reference.
if (typeof k === 'function') return '';
if (Array.isArray(k)) {
const last = k[k.length - 1];
return typeof last === 'function' ? '' : last;
}
return k;
};
const notReadySnapshot = { t: notReadyT, ready: false };
const dummySubscribe = () => () => {};
export const useTranslation = (ns, props = {}) => {
const { i18n: i18nFromProps } = props;
const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
const i18n = i18nFromProps || i18nFromContext || getI18n();
if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
if (!i18n) {
warnOnce(
i18n,
'NO_I18NEXT_INSTANCE',
'useTranslation: You will need to pass in an i18next instance by using initReactI18next',
);
}
const i18nOptions = useMemo(
() => ({ ...getDefaults(), ...i18n?.options?.react, ...props }),
[i18n, props],
);
const { useSuspense, keyPrefix } = i18nOptions;
const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS;
const unstableNamespaces = isString(nsOrContext) ? [nsOrContext] : nsOrContext || ['translation'];
const namespaces = useMemo(() => unstableNamespaces, unstableNamespaces);
i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
const revisionRef = useRef(0);
const subscribe = useCallback(
(callback) => {
if (!i18n) return dummySubscribe;
const { bindI18n, bindI18nStore } = i18nOptions;
const wrappedCallback = () => {
revisionRef.current += 1;
callback();
};
if (bindI18n) i18n.on(bindI18n, wrappedCallback);
if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
return () => {
if (bindI18n) bindI18n.split(' ').forEach((e) => i18n.off(e, wrappedCallback));
if (bindI18nStore)
bindI18nStore.split(' ').forEach((e) => i18n.store.off(e, wrappedCallback));
};
},
[i18n, i18nOptions],
);
const snapshotRef = useRef();
const getSnapshot = useCallback(() => {
if (!i18n) {
return notReadySnapshot;
}
const calculatedReady =
!!(i18n.isInitialized || i18n.initializedStoreOnce) &&
namespaces.every((n) => hasLoadedNamespace(n, i18n, i18nOptions));
const currentLng = props.lng || i18n.language;
const currentRevision = revisionRef.current;
const lastSnapshot = snapshotRef.current;
if (
lastSnapshot &&
lastSnapshot.ready === calculatedReady &&
lastSnapshot.lng === currentLng &&
lastSnapshot.keyPrefix === keyPrefix &&
lastSnapshot.revision === currentRevision // Check revision
) {
return lastSnapshot;
}
const calculatedT = i18n.getFixedT(
currentLng,
i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
keyPrefix,
);
const newSnapshot = {
t: calculatedT,
ready: calculatedReady,
lng: currentLng,
keyPrefix,
revision: currentRevision, // Store revision
};
snapshotRef.current = newSnapshot;
return newSnapshot;
}, [i18n, namespaces, keyPrefix, i18nOptions, props.lng]);
// We still need a state to manually trigger a re-render on load when the store doesn't emit an event.
const [loadCount, setLoadCount] = useState(0);
const { t, ready } = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
useEffect(() => {
if (i18n && !ready && !useSuspense) {
const onLoaded = () => setLoadCount((c) => c + 1);
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, onLoaded);
} else {
loadNamespaces(i18n, namespaces, onLoaded);
}
}
}, [i18n, props.lng, namespaces, ready, useSuspense, loadCount]);
const finalI18n = i18n || {};
// cache one wrapper per hook caller and only recreate it when language changes
const wrapperRef = useRef(null);
const wrapperLangRef = useRef();
// helper to create a wrapper instance (avoid duplicating descriptor logic)
const createI18nWrapper = (original) => {
const descriptors = Object.getOwnPropertyDescriptors(original);
if (descriptors.__original) delete descriptors.__original;
const wrapper = Object.create(Object.getPrototypeOf(original), descriptors);
if (!Object.prototype.hasOwnProperty.call(wrapper, '__original')) {
try {
Object.defineProperty(wrapper, '__original', {
value: original,
writable: false,
enumerable: false,
configurable: false,
});
} catch (_) {
/* ignore */
}
}
return wrapper;
};
const ret = useMemo(() => {
const original = finalI18n;
const lang = original?.language;
let i18nWrapper = original;
if (original) {
// if we already created a wrapper for this original instance
if (wrapperRef.current && wrapperRef.current.__original === original) {
// language changed -> create fresh wrapper so identity changes
if (wrapperLangRef.current !== lang) {
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
} else {
// reuse existing wrapper when language didn't change
i18nWrapper = wrapperRef.current;
}
} else {
// first time for this original instance -> create wrapper
i18nWrapper = createI18nWrapper(original);
wrapperRef.current = i18nWrapper;
wrapperLangRef.current = lang;
}
}
const effectiveT =
!ready && !useSuspense
? (...args) => {
warnOnce(
i18n,
'USE_T_BEFORE_READY',
'useTranslation: t was called before ready. When using useSuspense: false, make sure to check the ready flag before using t.',
);
return t(...args);
}
: t;
const arr = [effectiveT, i18nWrapper, ready];
arr.t = effectiveT;
arr.i18n = i18nWrapper;
arr.ready = ready;
return arr;
}, [t, finalI18n, ready, finalI18n.resolvedLanguage, finalI18n.language, finalI18n.languages]);
if (i18n && useSuspense && !ready) {
throw new Promise((resolve) => {
const onLoaded = () => resolve();
if (props.lng) {
loadLanguages(i18n, props.lng, namespaces, onLoaded);
} else {
loadNamespaces(i18n, namespaces, onLoaded);
}
});
}
return ret;
};
+93
View File
@@ -0,0 +1,93 @@
/** @type {(i18n:any,code:import('../TransWithoutContext').ErrorCode,msg?:string, rest?:{[key:string]: any})=>void} */
export const warn = (i18n, code, msg, rest) => {
const args = [msg, { code, ...(rest || {}) }];
if (i18n?.services?.logger?.forward) {
return i18n.services.logger.forward(args, 'warn', 'react-i18next::', true);
}
if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
if (i18n?.services?.logger?.warn) {
i18n.services.logger.warn(...args);
} else if (console?.warn) {
console.warn(...args);
}
};
const alreadyWarned = {};
/** @type {typeof warn} */
export const warnOnce = (i18n, code, msg, rest) => {
if (isString(msg) && alreadyWarned[msg]) return;
if (isString(msg)) alreadyWarned[msg] = new Date();
warn(i18n, code, msg, rest);
};
// not needed right now
//
// export const deprecated = (i18n, ...args) => {
// if (process && process.env && (!process.env.NODE_ENV || process.env.NODE_ENV === 'development')) {
// if (isString(args[0])) args[0] = `deprecation warning -> ${args[0]}`;
// warnOnce(i18n, ...args);
// }
// }
const loadedClb = (i18n, cb) => () => {
// delay ready if not yet initialized i18n instance
if (i18n.isInitialized) {
cb();
} else {
const initialized = () => {
// due to emitter removing issue in i18next we need to delay remove
setTimeout(() => {
i18n.off('initialized', initialized);
}, 0);
cb();
};
i18n.on('initialized', initialized);
}
};
export const loadNamespaces = (i18n, ns, cb) => {
i18n.loadNamespaces(ns, loadedClb(i18n, cb));
};
// should work with I18NEXT >= v22.5.0
export const loadLanguages = (i18n, lng, ns, cb) => {
// eslint-disable-next-line no-param-reassign
if (isString(ns)) ns = [ns];
if (i18n.options.preload && i18n.options.preload.indexOf(lng) > -1)
return loadNamespaces(i18n, ns, cb);
ns.forEach((n) => {
if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
});
i18n.loadLanguages(lng, loadedClb(i18n, cb));
};
export const hasLoadedNamespace = (ns, i18n, options = {}) => {
if (!i18n.languages || !i18n.languages.length) {
warnOnce(i18n, 'NO_LANGUAGES', 'i18n.languages were undefined or empty', {
languages: i18n.languages,
});
return true;
}
return i18n.hasLoadedNamespace(ns, {
lng: options.lng,
precheck: (i18nInstance, loadNotPending) => {
if (
options.bindI18n &&
options.bindI18n.indexOf('languageChanging') > -1 &&
i18nInstance.services.backendConnector.backend &&
i18nInstance.isLanguageChangingTo &&
!loadNotPending(i18nInstance.isLanguageChangingTo, ns)
)
return false;
},
});
};
export const getDisplayName = (Component) =>
Component.displayName ||
Component.name ||
(isString(Component) && Component.length > 0 ? Component : 'Unknown');
export const isString = (obj) => typeof obj === 'string';
export const isObject = (obj) => typeof obj === 'object' && obj !== null;
+21
View File
@@ -0,0 +1,21 @@
import { createElement } from 'react';
import { useSSR } from './useSSR.js';
import { composeInitialProps } from './context.js';
import { getDisplayName } from './utils.js';
export const withSSR = () =>
function Extend(WrappedComponent) {
function I18nextWithSSR({ initialI18nStore, initialLanguage, ...rest }) {
useSSR(initialI18nStore, initialLanguage);
return createElement(WrappedComponent, {
...rest,
});
}
I18nextWithSSR.getInitialProps = composeInitialProps(WrappedComponent);
I18nextWithSSR.displayName = `withI18nextSSR(${getDisplayName(WrappedComponent)})`;
I18nextWithSSR.WrappedComponent = WrappedComponent;
return I18nextWithSSR;
};
+35
View File
@@ -0,0 +1,35 @@
import { createElement, forwardRef as forwardRefReact } from 'react';
import { useTranslation } from './useTranslation.js';
import { getDisplayName } from './utils.js';
export const withTranslation = (ns, options = {}) =>
function Extend(WrappedComponent) {
function I18nextWithTranslation({ forwardedRef, ...rest }) {
const [t, i18n, ready] = useTranslation(ns, { ...rest, keyPrefix: options.keyPrefix });
const passDownProps = {
...rest,
t,
i18n,
tReady: ready,
};
if (options.withRef && forwardedRef) {
passDownProps.ref = forwardedRef;
} else if (!options.withRef && forwardedRef) {
passDownProps.forwardedRef = forwardedRef;
}
return createElement(WrappedComponent, passDownProps);
}
I18nextWithTranslation.displayName = `withI18nextTranslation(${getDisplayName(
WrappedComponent,
)})`;
I18nextWithTranslation.WrappedComponent = WrappedComponent;
const forwardRef = (props, ref) =>
// eslint-disable-next-line prefer-object-spread
createElement(I18nextWithTranslation, Object.assign({}, props, { forwardedRef: ref }));
return options.withRef ? forwardRefReact(forwardRef) : I18nextWithTranslation;
};
+54
View File
@@ -0,0 +1,54 @@
import { readdirSync } from 'node:fs';
import { defineConfig, defineProject } from 'vitest/config';
export default defineConfig({
test: {
projects: readdirSync('./test/typescript', { withFileTypes: true })
.filter((dir) => dir.isDirectory())
// Exclude issue-1899 - it runs separately via test:typescript:issue-1899
// because it requires skipLibCheck: false which exposes @types/node errors
.filter((dir) => dir.name !== 'issue-1899')
.reduce<ReturnType<typeof defineProject>[]>((workspaces, dir) => {
const dirPath = `test/typescript/${dir.name}` as const;
const tsConfigFiles = readdirSync(dirPath).filter(
// Do not include temporary vitest tsconfig files
(it) => it.startsWith('tsconfig.') && it.endsWith('.json') && !it.includes('vitest-temp'),
);
tsConfigFiles.forEach((tsConfigFileName) => {
const workspaceName =
tsConfigFileName === 'tsconfig.json'
? `typescript-${dir.name}`
: `${dir.name}-${tsConfigFileName.split('.')[1]}`;
workspaces.push(
defineProject({
test: {
name: workspaceName,
alias: {
/**
* Imports are resolved even if we are running only typecheck tests.
* This will result in:
* ```text
* Error: Failed to resolve entry for package "react-i18next". The package may have incorrect main/module/exports specified in its package.json.
* ```
* To avoid a useless build process before running these tests a stub alias to `react-i18next` is added.
*/
'react-i18next': './test/typescript/_stub.js',
},
typecheck: {
enabled: true,
only: true,
include: [`./${dirPath}/*.test.{ts,tsx}`],
tsconfig: `./${dirPath}/${tsConfigFileName}`,
},
},
}),
);
});
return workspaces;
}, []),
},
});