@mfeflow/i18n 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -2
- package/dist/index.d.ts +51 -0
- package/dist/index.js +86 -0
- package/dist/react.d.ts +20 -0
- package/dist/react.js +64 -0
- package/package.json +76 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanjeev Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mfeflow/i18n
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Internationalisation for micro-frontends. The shell owns one i18next instance; each remote lazy-loads its own namespace from its own deploy. It sets `lang` and `dir` (RTL) on `<html>` and persists the chosen language.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @mfeflow/i18n i18next react-i18next react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`i18next` (26), `react-i18next` (17) and `react` (19) are peer dependencies. All three are required, even if you only use the root entry: it registers `react-i18next` with the i18next instance.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
In the shell, once, before rendering:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { initI18n } from '@mfeflow/i18n';
|
|
19
|
+
|
|
20
|
+
await initI18n({
|
|
21
|
+
languages: [
|
|
22
|
+
{ code: 'en', label: 'English' },
|
|
23
|
+
{ code: 'ar', label: 'العربية' },
|
|
24
|
+
],
|
|
25
|
+
defaultLanguage: 'en',
|
|
26
|
+
resources: { en: { shell: { title: 'Store' } } },
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In a remote:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { useMfeTranslation } from '@mfeflow/i18n/react';
|
|
34
|
+
|
|
35
|
+
function Cart() {
|
|
36
|
+
const { t, ready } = useMfeTranslation('cart', (lng) => import(`./i18n/${lng}.json`));
|
|
37
|
+
return ready ? <h1>{t('title')}</h1> : null;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`useLanguage()` returns the current language, the configured list and `setLanguage(code)` for a picker.
|
|
42
|
+
|
|
43
|
+
## Module Federation sharing
|
|
44
|
+
|
|
45
|
+
Bucket: **singleton**. This package keeps page-wide state, so the shell and every remote must use one copy. `@mfeflow/rsbuild` shares it as a Module Federation singleton by default (`createShellConfig` and `createRemoteConfig`); if you configure sharing by hand, mark it `singleton: true`.
|
|
46
|
+
|
|
47
|
+
## Links
|
|
48
|
+
|
|
49
|
+
- [mfeflow repository](https://github.com/mfeflow/mfeflow) and its README
|
|
50
|
+
- [Documentation](https://mfeflow.github.io/mfeflow/)
|
|
51
|
+
|
|
52
|
+
MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type i18n as I18n, type Resource } from 'i18next';
|
|
2
|
+
import { type MfeStorage } from '@mfeflow/storage';
|
|
3
|
+
export type { I18n };
|
|
4
|
+
export interface LanguageOption {
|
|
5
|
+
/** BCP 47 code, e.g. `en`, `hi`, `ar`, `pt-BR`. */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Shown in language pickers, in that language: `English`, `हिन्दी`, `العربية`. */
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
export interface InitI18nOptions {
|
|
11
|
+
languages: readonly LanguageOption[];
|
|
12
|
+
defaultLanguage: string;
|
|
13
|
+
/** Messages the shell ships up front. Remotes load their own with `loadNamespace`. */
|
|
14
|
+
resources?: Resource;
|
|
15
|
+
/** Use the browser's languages when nothing is stored. Default true. */
|
|
16
|
+
detect?: boolean;
|
|
17
|
+
/** Defaults to the shell's storage. */
|
|
18
|
+
storage?: MfeStorage;
|
|
19
|
+
/** Gets `lang` and `dir`. Defaults to `<html>`. */
|
|
20
|
+
root?: HTMLElement;
|
|
21
|
+
}
|
|
22
|
+
export declare const LANGUAGE_STORAGE_KEY = "language";
|
|
23
|
+
interface I18nState {
|
|
24
|
+
instance: I18n;
|
|
25
|
+
languages: readonly LanguageOption[];
|
|
26
|
+
}
|
|
27
|
+
declare global {
|
|
28
|
+
var mfeflowI18n: I18nState | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Stored choice, then the first browser language we support (exact or base), then the default. */
|
|
31
|
+
export declare function detectLanguage(supported: readonly string[], stored: string | undefined, preferred: readonly string[], fallback: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Creates the page's single i18next instance. Call once, in the shell, before
|
|
34
|
+
* rendering. Calling it again returns the existing instance.
|
|
35
|
+
*/
|
|
36
|
+
export declare function initI18n(options: InitI18nOptions): Promise<I18n>;
|
|
37
|
+
/** The page's i18next instance. Throws if the shell has not called `initI18n`. */
|
|
38
|
+
export declare function getI18n(): I18n;
|
|
39
|
+
/** Languages the shell configured. */
|
|
40
|
+
export declare function getLanguages(): readonly LanguageOption[];
|
|
41
|
+
export type Messages = Readonly<Record<string, unknown>>;
|
|
42
|
+
/** Usually `(lng) => import(`./i18n/${lng}.json`)`, so messages ship with the remote's own deploy. */
|
|
43
|
+
export type NamespaceLoader = (language: string) => Promise<unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Adds a remote's namespace for `language` if it is not loaded yet. If that
|
|
46
|
+
* language fails to load, the fallback language is loaded instead so the
|
|
47
|
+
* remote still renders text.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadNamespace(instance: I18n, namespace: string, loader: NamespaceLoader, language?: string): Promise<void>;
|
|
50
|
+
/** Only for tests: forget the page's instance. */
|
|
51
|
+
export declare function resetI18nForTests(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createInstance } from "i18next";
|
|
2
|
+
import { initReactI18next } from "react-i18next";
|
|
3
|
+
import { createStorage } from "@mfeflow/storage";
|
|
4
|
+
const LANGUAGE_STORAGE_KEY = 'language';
|
|
5
|
+
function detectLanguage(supported, stored, preferred, fallback) {
|
|
6
|
+
if (void 0 !== stored && supported.includes(stored)) return stored;
|
|
7
|
+
for (const lang of preferred){
|
|
8
|
+
if (supported.includes(lang)) return lang;
|
|
9
|
+
const base = lang.split('-')[0];
|
|
10
|
+
if (void 0 !== base && supported.includes(base)) return base;
|
|
11
|
+
}
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
function browserLanguages() {
|
|
15
|
+
return "u" < typeof navigator ? [] : navigator.languages;
|
|
16
|
+
}
|
|
17
|
+
function applyDocumentLanguage(instance, root, lng) {
|
|
18
|
+
const el = root ?? ("u" < typeof document ? void 0 : document.documentElement);
|
|
19
|
+
if (!el) return;
|
|
20
|
+
el.lang = lng;
|
|
21
|
+
el.dir = instance.dir(lng);
|
|
22
|
+
}
|
|
23
|
+
async function initI18n(options) {
|
|
24
|
+
if (globalThis.mfeflowI18n) return globalThis.mfeflowI18n.instance;
|
|
25
|
+
const codes = options.languages.map((l)=>l.code);
|
|
26
|
+
if (!codes.includes(options.defaultLanguage)) throw new Error(`Default language "${options.defaultLanguage}" is not in the language list`);
|
|
27
|
+
const storage = options.storage ?? createStorage('shell');
|
|
28
|
+
const stored = storage.get(LANGUAGE_STORAGE_KEY, void 0, (v)=>'string' == typeof v);
|
|
29
|
+
const lng = detectLanguage(codes, stored, false === options.detect ? [] : browserLanguages(), options.defaultLanguage);
|
|
30
|
+
const instance = createInstance();
|
|
31
|
+
await instance.use(initReactI18next).init({
|
|
32
|
+
lng,
|
|
33
|
+
fallbackLng: options.defaultLanguage,
|
|
34
|
+
supportedLngs: codes,
|
|
35
|
+
resources: options.resources ?? {},
|
|
36
|
+
partialBundledLanguages: true,
|
|
37
|
+
interpolation: {
|
|
38
|
+
escapeValue: false
|
|
39
|
+
},
|
|
40
|
+
react: {
|
|
41
|
+
useSuspense: false
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
applyDocumentLanguage(instance, options.root, lng);
|
|
45
|
+
instance.on('languageChanged', (next)=>{
|
|
46
|
+
applyDocumentLanguage(instance, options.root, next);
|
|
47
|
+
storage.set(LANGUAGE_STORAGE_KEY, next);
|
|
48
|
+
});
|
|
49
|
+
globalThis.mfeflowI18n = {
|
|
50
|
+
instance,
|
|
51
|
+
languages: options.languages
|
|
52
|
+
};
|
|
53
|
+
return instance;
|
|
54
|
+
}
|
|
55
|
+
function getI18n() {
|
|
56
|
+
const state = globalThis.mfeflowI18n;
|
|
57
|
+
if (!state) throw new Error('getI18n: the shell must call initI18n() before remotes use translations');
|
|
58
|
+
return state.instance;
|
|
59
|
+
}
|
|
60
|
+
function getLanguages() {
|
|
61
|
+
return globalThis.mfeflowI18n?.languages ?? [];
|
|
62
|
+
}
|
|
63
|
+
function isMessages(value) {
|
|
64
|
+
return 'object' == typeof value && null !== value && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
function unwrap(mod) {
|
|
67
|
+
if (isMessages(mod) && 'default' in mod && isMessages(mod.default)) return mod.default;
|
|
68
|
+
if (isMessages(mod)) return mod;
|
|
69
|
+
throw new Error('loadNamespace: the loader must resolve to an object of messages');
|
|
70
|
+
}
|
|
71
|
+
async function loadNamespace(instance, namespace, loader, language = instance.language) {
|
|
72
|
+
if (instance.hasResourceBundle(language, namespace)) return;
|
|
73
|
+
try {
|
|
74
|
+
instance.addResourceBundle(language, namespace, unwrap(await loader(language)), true, false);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const configured = instance.options.fallbackLng;
|
|
77
|
+
const first = Array.isArray(configured) ? configured[0] : configured;
|
|
78
|
+
const fallback = 'string' == typeof first ? first : void 0;
|
|
79
|
+
if (void 0 === fallback || fallback === language || instance.hasResourceBundle(fallback, namespace)) throw error;
|
|
80
|
+
instance.addResourceBundle(fallback, namespace, unwrap(await loader(fallback)), true, false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resetI18nForTests() {
|
|
84
|
+
globalThis.mfeflowI18n = void 0;
|
|
85
|
+
}
|
|
86
|
+
export { LANGUAGE_STORAGE_KEY, detectLanguage, getI18n, getLanguages, initI18n, loadNamespace, resetI18nForTests };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next';
|
|
2
|
+
import { type LanguageOption, type NamespaceLoader } from './index.js';
|
|
3
|
+
export interface MfeTranslation {
|
|
4
|
+
t: TFunction;
|
|
5
|
+
/** False until the namespace for the current language has loaded. */
|
|
6
|
+
ready: boolean;
|
|
7
|
+
language: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Translations for one remote. Loads `namespace` with `loader` for the current
|
|
11
|
+
* language when the component mounts, and again whenever the language changes.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useMfeTranslation(namespace: string, loader: NamespaceLoader): MfeTranslation;
|
|
14
|
+
export interface LanguageState {
|
|
15
|
+
language: string;
|
|
16
|
+
languages: readonly LanguageOption[];
|
|
17
|
+
setLanguage: (code: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** The current language, the configured list, and a setter, for language pickers. */
|
|
20
|
+
export declare function useLanguage(): LanguageState;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useEffectEvent, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { getI18n, getLanguages, loadNamespace } from "./index.js";
|
|
4
|
+
function useMfeTranslation(namespace, loader) {
|
|
5
|
+
const i18n = getI18n();
|
|
6
|
+
const { t } = useTranslation(namespace, {
|
|
7
|
+
i18n,
|
|
8
|
+
useSuspense: false
|
|
9
|
+
});
|
|
10
|
+
const loadWith = useEffectEvent((lng)=>loadNamespace(i18n, namespace, loader, lng));
|
|
11
|
+
const [ready, setReady] = useState(()=>i18n.hasResourceBundle(i18n.language, namespace));
|
|
12
|
+
const [language, setLanguage] = useState(i18n.language);
|
|
13
|
+
useEffect(()=>{
|
|
14
|
+
let alive = true;
|
|
15
|
+
const load = (lng)=>{
|
|
16
|
+
setLanguage(lng);
|
|
17
|
+
setReady(i18n.hasResourceBundle(lng, namespace));
|
|
18
|
+
loadWith(lng).then(()=>{
|
|
19
|
+
if (alive) setReady(true);
|
|
20
|
+
}, (error)=>{
|
|
21
|
+
console.error(`[mfeflow/i18n] could not load namespace "${namespace}"`, error);
|
|
22
|
+
if (alive) setReady(true);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
load(i18n.language);
|
|
26
|
+
i18n.on('languageChanged', load);
|
|
27
|
+
return ()=>{
|
|
28
|
+
alive = false;
|
|
29
|
+
i18n.off('languageChanged', load);
|
|
30
|
+
};
|
|
31
|
+
}, [
|
|
32
|
+
i18n,
|
|
33
|
+
namespace
|
|
34
|
+
]);
|
|
35
|
+
return {
|
|
36
|
+
t,
|
|
37
|
+
ready,
|
|
38
|
+
language
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function useLanguage() {
|
|
42
|
+
const i18n = getI18n();
|
|
43
|
+
const subscribe = useCallback((onChange)=>{
|
|
44
|
+
i18n.on('languageChanged', onChange);
|
|
45
|
+
return ()=>{
|
|
46
|
+
i18n.off('languageChanged', onChange);
|
|
47
|
+
};
|
|
48
|
+
}, [
|
|
49
|
+
i18n
|
|
50
|
+
]);
|
|
51
|
+
const language = useSyncExternalStore(subscribe, ()=>i18n.language, ()=>i18n.language);
|
|
52
|
+
const setLanguage = useCallback(async (code)=>{
|
|
53
|
+
if (!getLanguages().some((l)=>l.code === code)) throw new Error(`Unknown language "${code}"`);
|
|
54
|
+
await i18n.changeLanguage(code);
|
|
55
|
+
}, [
|
|
56
|
+
i18n
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
language,
|
|
60
|
+
languages: getLanguages(),
|
|
61
|
+
setLanguage
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export { useLanguage, useMfeTranslation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mfeflow/i18n",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "i18n for micro-frontends: one i18next instance owned by the shell, each remote lazy-loads its own namespace from its own deploy, RTL and persisted language.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"micro-frontend",
|
|
7
|
+
"mfe",
|
|
8
|
+
"module-federation",
|
|
9
|
+
"i18n",
|
|
10
|
+
"i18next",
|
|
11
|
+
"react-i18next",
|
|
12
|
+
"rtl",
|
|
13
|
+
"react",
|
|
14
|
+
"rsbuild"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://mfeflow.github.io/mfeflow/",
|
|
17
|
+
"bugs": "https://github.com/mfeflow/mfeflow/issues",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mfeflow/mfeflow.git",
|
|
21
|
+
"directory": "packages/i18n"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Sanjeev Sharma <speedsharma.ai@gmail.com>",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./react": {
|
|
34
|
+
"types": "./dist/react.d.ts",
|
|
35
|
+
"import": "./dist/react.js",
|
|
36
|
+
"default": "./dist/react.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.12"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@mfeflow/storage": "0.1.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"i18next": "^26.0.0",
|
|
54
|
+
"react-i18next": "^17.0.0",
|
|
55
|
+
"react": "^19.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@rsbuild/plugin-react": "2.1.1",
|
|
59
|
+
"@rslib/core": "1.0.5",
|
|
60
|
+
"@testing-library/dom": "10.4.2",
|
|
61
|
+
"@testing-library/react": "16.3.3",
|
|
62
|
+
"@types/node": "26.6.6",
|
|
63
|
+
"@types/react": "19.3.0",
|
|
64
|
+
"@types/react-dom": "19.3.0",
|
|
65
|
+
"i18next": "26.5.12",
|
|
66
|
+
"jsdom": "30.1.3",
|
|
67
|
+
"react": "19.3.1",
|
|
68
|
+
"react-dom": "19.3.1",
|
|
69
|
+
"react-i18next": "17.1.9",
|
|
70
|
+
"typescript": "7.0.2"
|
|
71
|
+
},
|
|
72
|
+
"mfeflow": {
|
|
73
|
+
"bucket": "singleton"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "rslib build",
|
|
77
|
+
"typecheck": "tsc --noEmit"
|
|
78
|
+
}
|
|
6
79
|
}
|