react-scoped-i18n 0.0.1
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 +148 -0
- package/dist/cjs/createI18nContext/api/createCommons.d.ts +6 -0
- package/dist/cjs/createI18nContext/api/createCommons.js +30 -0
- package/dist/cjs/createI18nContext/api/createCommons.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createCommons.test.js +26 -0
- package/dist/cjs/createI18nContext/api/createFormat.d.ts +9 -0
- package/dist/cjs/createI18nContext/api/createFormat.js +54 -0
- package/dist/cjs/createI18nContext/api/createFormat.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createFormat.test.js +71 -0
- package/dist/cjs/createI18nContext/api/createT.d.ts +7 -0
- package/dist/cjs/createI18nContext/api/createT.js +15 -0
- package/dist/cjs/createI18nContext/api/createT.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createT.test.js +23 -0
- package/dist/cjs/createI18nContext/api/createTGlobal.d.ts +8 -0
- package/dist/cjs/createI18nContext/api/createTGlobal.js +31 -0
- package/dist/cjs/createI18nContext/api/createTGlobal.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createTGlobal.test.js +20 -0
- package/dist/cjs/createI18nContext/api/createTPlural.d.ts +5 -0
- package/dist/cjs/createI18nContext/api/createTPlural.js +19 -0
- package/dist/cjs/createI18nContext/api/createTPlural.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createTPlural.test.js +31 -0
- package/dist/cjs/createI18nContext/api/createUseI18n.d.ts +21 -0
- package/dist/cjs/createI18nContext/api/createUseI18n.js +52 -0
- package/dist/cjs/createI18nContext/api/createUseI18n.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/api/createUseI18n.test.js +46 -0
- package/dist/cjs/createI18nContext/const/index.d.ts +1 -0
- package/dist/cjs/createI18nContext/const/index.js +4 -0
- package/dist/cjs/createI18nContext/index.d.ts +29 -0
- package/dist/cjs/createI18nContext/index.integration.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/index.integration.test.js +41 -0
- package/dist/cjs/createI18nContext/index.js +45 -0
- package/dist/cjs/createI18nContext/provider/createI18nProvider.d.ts +15 -0
- package/dist/cjs/createI18nContext/provider/createI18nProvider.js +23 -0
- package/dist/cjs/createI18nContext/type/index.d.ts +19 -0
- package/dist/cjs/createI18nContext/type/index.js +7 -0
- package/dist/cjs/createI18nContext/type/isLanguage.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/type/isLanguage.test.js +16 -0
- package/dist/cjs/createI18nContext/util/getPluralTranslation.d.ts +7 -0
- package/dist/cjs/createI18nContext/util/getPluralTranslation.js +48 -0
- package/dist/cjs/createI18nContext/util/getPluralTranslation.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/util/getPluralTranslation.test.js +126 -0
- package/dist/cjs/createI18nContext/util/getTranslation.d.ts +7 -0
- package/dist/cjs/createI18nContext/util/getTranslation.js +32 -0
- package/dist/cjs/createI18nContext/util/getTranslation.test.d.ts +1 -0
- package/dist/cjs/createI18nContext/util/getTranslation.test.js +96 -0
- package/dist/cjs/env.d.ts +1 -0
- package/dist/cjs/env.js +9 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.test.d.ts +0 -0
- package/dist/cjs/index.test.js +40 -0
- package/dist/esm/createI18nContext/api/createCommons.d.ts +7 -0
- package/dist/esm/createI18nContext/api/createCommons.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createCommons.js +26 -0
- package/dist/esm/createI18nContext/api/createCommons.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createCommons.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createCommons.test.js +24 -0
- package/dist/esm/createI18nContext/api/createFormat.d.ts +10 -0
- package/dist/esm/createI18nContext/api/createFormat.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createFormat.js +50 -0
- package/dist/esm/createI18nContext/api/createFormat.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createFormat.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createFormat.test.js +69 -0
- package/dist/esm/createI18nContext/api/createT.d.ts +8 -0
- package/dist/esm/createI18nContext/api/createT.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createT.js +11 -0
- package/dist/esm/createI18nContext/api/createT.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createT.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createT.test.js +21 -0
- package/dist/esm/createI18nContext/api/createTGlobal.d.ts +9 -0
- package/dist/esm/createI18nContext/api/createTGlobal.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createTGlobal.js +27 -0
- package/dist/esm/createI18nContext/api/createTGlobal.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createTGlobal.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createTGlobal.test.js +18 -0
- package/dist/esm/createI18nContext/api/createTPlural.d.ts +6 -0
- package/dist/esm/createI18nContext/api/createTPlural.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createTPlural.js +15 -0
- package/dist/esm/createI18nContext/api/createTPlural.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createTPlural.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createTPlural.test.js +29 -0
- package/dist/esm/createI18nContext/api/createUseI18n.d.ts +22 -0
- package/dist/esm/createI18nContext/api/createUseI18n.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createUseI18n.js +48 -0
- package/dist/esm/createI18nContext/api/createUseI18n.test.d.ts +2 -0
- package/dist/esm/createI18nContext/api/createUseI18n.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/api/createUseI18n.test.js +44 -0
- package/dist/esm/createI18nContext/const/index.d.ts +2 -0
- package/dist/esm/createI18nContext/const/index.d.ts.map +1 -0
- package/dist/esm/createI18nContext/const/index.js +1 -0
- package/dist/esm/createI18nContext/index.d.ts +30 -0
- package/dist/esm/createI18nContext/index.d.ts.map +1 -0
- package/dist/esm/createI18nContext/index.integration.test.d.ts +2 -0
- package/dist/esm/createI18nContext/index.integration.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/index.integration.test.js +39 -0
- package/dist/esm/createI18nContext/index.js +42 -0
- package/dist/esm/createI18nContext/provider/createI18nProvider.d.ts +16 -0
- package/dist/esm/createI18nContext/provider/createI18nProvider.d.ts.map +1 -0
- package/dist/esm/createI18nContext/provider/createI18nProvider.js +19 -0
- package/dist/esm/createI18nContext/type/index.d.ts +20 -0
- package/dist/esm/createI18nContext/type/index.d.ts.map +1 -0
- package/dist/esm/createI18nContext/type/index.js +3 -0
- package/dist/esm/createI18nContext/type/isLanguage.test.d.ts +2 -0
- package/dist/esm/createI18nContext/type/isLanguage.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/type/isLanguage.test.js +14 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.d.ts +8 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.d.ts.map +1 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.js +44 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.test.d.ts +2 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/util/getPluralTranslation.test.js +124 -0
- package/dist/esm/createI18nContext/util/getTranslation.d.ts +8 -0
- package/dist/esm/createI18nContext/util/getTranslation.d.ts.map +1 -0
- package/dist/esm/createI18nContext/util/getTranslation.js +28 -0
- package/dist/esm/createI18nContext/util/getTranslation.test.d.ts +2 -0
- package/dist/esm/createI18nContext/util/getTranslation.test.d.ts.map +1 -0
- package/dist/esm/createI18nContext/util/getTranslation.test.js +94 -0
- package/dist/esm/env.d.ts +2 -0
- package/dist/esm/env.d.ts.map +1 -0
- package/dist/esm/env.js +6 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.test.d.ts +1 -0
- package/dist/esm/index.test.d.ts.map +1 -0
- package/dist/esm/index.test.js +7 -0
- package/package.json +55 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @jest-environment jsdom
|
|
4
|
+
*/
|
|
5
|
+
import { act } from "react";
|
|
6
|
+
import { render, screen } from "@testing-library/react";
|
|
7
|
+
import { createI18nContext } from "./index";
|
|
8
|
+
test(`very basic integration test`, () => {
|
|
9
|
+
const { I18nProvider, useI18n } = createI18nContext({
|
|
10
|
+
languages: [`en`, `es`],
|
|
11
|
+
fallbackLanguage: `en`,
|
|
12
|
+
commons: {
|
|
13
|
+
continue: {
|
|
14
|
+
en: `Continue`,
|
|
15
|
+
es: `Continuar`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const TestComponent = () => {
|
|
20
|
+
const { t, currentLanguage, setCurrentLanguage, commons, format } = useI18n();
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("h1", { "data-testid": `current-language`, children: currentLanguage }), _jsx("div", { "data-testid": `test-component`, children: t({ en: `Hello`, es: `Hola` }) }), _jsx("button", { "data-testid": `switch-to-english-button`, onClick: () => {
|
|
22
|
+
setCurrentLanguage(`en`);
|
|
23
|
+
} }), _jsx("div", { "data-testid": `formatted-number`, children: format.number(10000.555, {
|
|
24
|
+
maximumFractionDigits: 2,
|
|
25
|
+
}) }), _jsx("button", { "data-testid": `continue-button`, children: commons.continue })] }));
|
|
26
|
+
};
|
|
27
|
+
render(_jsx(I18nProvider, { initialLanguage: `es`, children: _jsx(TestComponent, {}) }));
|
|
28
|
+
expect(screen.getByTestId(`current-language`).textContent).toBe(`es`);
|
|
29
|
+
expect(screen.getByTestId(`test-component`).textContent).toBe(`Hola`);
|
|
30
|
+
expect(screen.getByTestId(`continue-button`).textContent).toBe(`Continuar`);
|
|
31
|
+
expect(screen.getByTestId(`formatted-number`).textContent).toBe(`10.000,56`);
|
|
32
|
+
act(() => {
|
|
33
|
+
screen.getByTestId(`switch-to-english-button`).click();
|
|
34
|
+
});
|
|
35
|
+
expect(screen.getByTestId(`current-language`).textContent).toBe(`en`);
|
|
36
|
+
expect(screen.getByTestId(`test-component`).textContent).toBe(`Hello`);
|
|
37
|
+
expect(screen.getByTestId(`continue-button`).textContent).toBe(`Continue`);
|
|
38
|
+
expect(screen.getByTestId(`formatted-number`).textContent).toBe(`10,000.56`);
|
|
39
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { createI18nProvider } from "./provider/createI18nProvider";
|
|
3
|
+
import { createUseI18n } from "./api/createUseI18n";
|
|
4
|
+
import { createTGlobal } from "./api/createTGlobal";
|
|
5
|
+
import { IS_DEV } from "../env";
|
|
6
|
+
export function createI18nContext({ languages, fallbackLanguage, commons, }) {
|
|
7
|
+
if (IS_DEV) {
|
|
8
|
+
if (commons) {
|
|
9
|
+
Object.freeze(commons);
|
|
10
|
+
}
|
|
11
|
+
Object.freeze(languages);
|
|
12
|
+
}
|
|
13
|
+
const moduleScopedCurrentLanguageRef =
|
|
14
|
+
/**
|
|
15
|
+
* Module-scoped variable to hold the current language for tGlobal function.
|
|
16
|
+
* This is updated by the I18nProvider component.
|
|
17
|
+
* Don't rely on this
|
|
18
|
+
*/
|
|
19
|
+
{ current: fallbackLanguage };
|
|
20
|
+
if (!fallbackLanguage || !languages.includes(fallbackLanguage)) {
|
|
21
|
+
throw new Error(`Default language "${fallbackLanguage}" must be one of the supported languages: [${languages.join(`, `)}]`);
|
|
22
|
+
}
|
|
23
|
+
const I18nContext = createContext(null);
|
|
24
|
+
I18nContext.displayName = `I18nContext`;
|
|
25
|
+
const I18nProvider = createI18nProvider({
|
|
26
|
+
I18nContext,
|
|
27
|
+
moduleScopedCurrentLanguageRef,
|
|
28
|
+
fallbackLanguage,
|
|
29
|
+
});
|
|
30
|
+
const useI18n = createUseI18n({
|
|
31
|
+
I18nContext,
|
|
32
|
+
languages,
|
|
33
|
+
fallbackLanguage,
|
|
34
|
+
commons,
|
|
35
|
+
});
|
|
36
|
+
const tGlobal = createTGlobal({
|
|
37
|
+
languages,
|
|
38
|
+
currentLanguage: moduleScopedCurrentLanguageRef,
|
|
39
|
+
fallbackLanguage,
|
|
40
|
+
});
|
|
41
|
+
return { I18nProvider, useI18n, tGlobal };
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context, ReactNode } from "react";
|
|
2
|
+
import { I18nContextType } from "../type";
|
|
3
|
+
export declare const createI18nProvider: <const Language extends string>({ I18nContext, moduleScopedCurrentLanguageRef, fallbackLanguage, }: {
|
|
4
|
+
I18nContext: Context<I18nContextType<Language> | null>;
|
|
5
|
+
moduleScopedCurrentLanguageRef: {
|
|
6
|
+
current: Language;
|
|
7
|
+
};
|
|
8
|
+
fallbackLanguage: Language;
|
|
9
|
+
}) => {
|
|
10
|
+
({ children, initialLanguage, }: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
initialLanguage: Language;
|
|
13
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
displayName: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=createI18nProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createI18nProvider.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/provider/createI18nProvider.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,OAAO,EAAE,SAAS,EAAuB,MAAM,OAAO,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,eAAO,MAAM,kBAAkB,GAAI,KAAK,CAAC,QAAQ,SAAS,MAAM,EAAE,oEAI/D;IACD,WAAW,EAAE,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;IACvD,8BAA8B,EAAE;QAAE,OAAO,EAAE,QAAQ,CAAA;KAAE,CAAC;IACtD,gBAAgB,EAAE,QAAQ,CAAC;CAC5B;qCAII;QACD,QAAQ,EAAE,SAAS,CAAC;QACpB,eAAe,EAAE,QAAQ,CAAC;KAC3B;;CA2BF,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
export const createI18nProvider = ({ I18nContext, moduleScopedCurrentLanguageRef, fallbackLanguage, }) => {
|
|
4
|
+
const I18nProvider = ({ children, initialLanguage, }) => {
|
|
5
|
+
const [currentLanguage, setCurrentLanguage] = useState(initialLanguage || fallbackLanguage);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
// Used only for tGlobal function outside React components.
|
|
8
|
+
// Do not rely on this!
|
|
9
|
+
moduleScopedCurrentLanguageRef.current = currentLanguage;
|
|
10
|
+
}, [currentLanguage]);
|
|
11
|
+
return (_jsx(I18nContext.Provider, { value: {
|
|
12
|
+
currentLanguage,
|
|
13
|
+
fallbackLanguage,
|
|
14
|
+
setCurrentLanguage,
|
|
15
|
+
}, children: children }));
|
|
16
|
+
};
|
|
17
|
+
I18nProvider.displayName = `I18nProvider`;
|
|
18
|
+
return I18nProvider;
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Dispatch, ReactNode, SetStateAction } from "react";
|
|
2
|
+
export type TranslationValue = ReactNode | string | number;
|
|
3
|
+
export type TranslationMap<Language extends string, Value = TranslationValue> = Record<Language, Value>;
|
|
4
|
+
export type Languages = readonly string[];
|
|
5
|
+
export declare const isLanguage: (language: string, languages: Languages) => language is Languages[number];
|
|
6
|
+
export type I18nContextType<Language extends string> = {
|
|
7
|
+
currentLanguage: Language;
|
|
8
|
+
fallbackLanguage: Language;
|
|
9
|
+
setCurrentLanguage: Dispatch<SetStateAction<Language>>;
|
|
10
|
+
};
|
|
11
|
+
export type PluralTranslation = {
|
|
12
|
+
negative?: ReactNode;
|
|
13
|
+
zero?: ReactNode;
|
|
14
|
+
one?: ReactNode;
|
|
15
|
+
two?: ReactNode;
|
|
16
|
+
many: ReactNode;
|
|
17
|
+
[key: number]: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
export type PluralTranslationMap<Language extends string> = Record<Language, PluralTranslation>;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/type/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3D,MAAM,MAAM,cAAc,CACxB,QAAQ,SAAS,MAAM,EACvB,KAAK,GAAG,gBAAgB,IACtB,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AAE5B,MAAM,MAAM,SAAS,GAAG,SAAS,MAAM,EAAE,CAAC;AAE1C,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,WAAW,SAAS,KACnB,QAAQ,IAAI,SAAS,CAAC,MAAM,CAE9B,CAAC;AAEF,MAAM,MAAM,eAAe,CAAC,QAAQ,SAAS,MAAM,IAAI;IACrD,eAAe,EAAE,QAAQ,CAAC;IAC1B,gBAAgB,EAAE,QAAQ,CAAC;IAC3B,kBAAkB,EAAE,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,QAAQ,SAAS,MAAM,IAAI,MAAM,CAChE,QAAQ,EACR,iBAAiB,CAClB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isLanguage.test.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/type/isLanguage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isLanguage } from "./index";
|
|
2
|
+
describe(`isLanguage`, () => {
|
|
3
|
+
const languages = [`en`, `es`, `sl`];
|
|
4
|
+
it(`returns true for supported languages`, () => {
|
|
5
|
+
expect(isLanguage(`en`, languages)).toBe(true);
|
|
6
|
+
expect(isLanguage(`es`, languages)).toBe(true);
|
|
7
|
+
expect(isLanguage(`sl`, languages)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it(`returns false for unsupported languages`, () => {
|
|
10
|
+
expect(isLanguage(`fr`, languages)).toBe(false);
|
|
11
|
+
expect(isLanguage(`de`, languages)).toBe(false);
|
|
12
|
+
expect(isLanguage(`it`, languages)).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PluralTranslationMap } from "../type";
|
|
2
|
+
export declare const getPluralTranslation: <const Language extends string>({ currentLanguage, fallbackLanguage, count, translations, }: {
|
|
3
|
+
currentLanguage: Language;
|
|
4
|
+
fallbackLanguage: Language;
|
|
5
|
+
count: number;
|
|
6
|
+
translations: PluralTranslationMap<Language[][number]>;
|
|
7
|
+
}) => string | number | bigint | true | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode> | null | undefined>;
|
|
8
|
+
//# sourceMappingURL=getPluralTranslation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getPluralTranslation.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/util/getPluralTranslation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAI/C,eAAO,MAAM,oBAAoB,GAAI,KAAK,CAAC,QAAQ,SAAS,MAAM,EAAE,6DAKjE;IACD,eAAe,EAAE,QAAQ,CAAC;IAC1B,gBAAgB,EAAE,QAAQ,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,oBAAoB,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;CACxD,yYA8DA,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { MISSING_TRANSLATION } from "../const";
|
|
2
|
+
import { IS_DEV } from "../../env";
|
|
3
|
+
export const getPluralTranslation = ({ currentLanguage, fallbackLanguage, count, translations, }) => {
|
|
4
|
+
if (!translations) {
|
|
5
|
+
throw new Error(`[i18n] Translations object is undefined or null`);
|
|
6
|
+
}
|
|
7
|
+
const targetTranslation = translations[currentLanguage];
|
|
8
|
+
const defaultTranslation = translations[fallbackLanguage];
|
|
9
|
+
if (targetTranslation == undefined && defaultTranslation === undefined) {
|
|
10
|
+
if (IS_DEV) {
|
|
11
|
+
console.warn(`[i18n] Missing plural translation for language "${currentLanguage}" and for default language "${fallbackLanguage}"`);
|
|
12
|
+
}
|
|
13
|
+
return MISSING_TRANSLATION;
|
|
14
|
+
}
|
|
15
|
+
if (targetTranslation === undefined) {
|
|
16
|
+
if (IS_DEV) {
|
|
17
|
+
console.warn(`[i18n] Missing plural translation for language "${currentLanguage}". Falling back to default language "${fallbackLanguage}"`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const translation = targetTranslation || defaultTranslation;
|
|
21
|
+
const exactMatch = translation[count];
|
|
22
|
+
if (exactMatch) {
|
|
23
|
+
return exactMatch;
|
|
24
|
+
}
|
|
25
|
+
if (count < 0 && translation.negative) {
|
|
26
|
+
return translation.negative;
|
|
27
|
+
}
|
|
28
|
+
if (count === 0 && translation.zero) {
|
|
29
|
+
return translation.zero;
|
|
30
|
+
}
|
|
31
|
+
if (count === 1 && translation.one) {
|
|
32
|
+
return translation.one;
|
|
33
|
+
}
|
|
34
|
+
if (count === 2 && translation.two) {
|
|
35
|
+
return translation.two;
|
|
36
|
+
}
|
|
37
|
+
if (translation.many) {
|
|
38
|
+
return translation.many;
|
|
39
|
+
}
|
|
40
|
+
if (IS_DEV) {
|
|
41
|
+
console.warn(`[i18n] No pluralization match found for count "${count}". Please provide appropriate pluralization translations.`);
|
|
42
|
+
}
|
|
43
|
+
return MISSING_TRANSLATION;
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getPluralTranslation.test.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/util/getPluralTranslation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getPluralTranslation } from "./getPluralTranslation";
|
|
2
|
+
import { MISSING_TRANSLATION } from "../const";
|
|
3
|
+
describe(`getPluralTranslation`, () => {
|
|
4
|
+
const fallbackLanguage = `en`;
|
|
5
|
+
const translations = {
|
|
6
|
+
en: {
|
|
7
|
+
negative: `You have a negative count.`,
|
|
8
|
+
zero: `You have no items.`,
|
|
9
|
+
one: `You have one item.`,
|
|
10
|
+
two: `You have two items.`,
|
|
11
|
+
many: `You have many items.`,
|
|
12
|
+
42: `You have the perfect count of items!`,
|
|
13
|
+
},
|
|
14
|
+
es: {
|
|
15
|
+
negative: `Tienes un conteo negativo.`,
|
|
16
|
+
zero: `No tienes artículos.`,
|
|
17
|
+
one: `Tienes un artículo.`,
|
|
18
|
+
two: `Tienes dos artículos.`,
|
|
19
|
+
many: `Tienes muchos artículos.`,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
it(`Error: Translations object is undefined or null`, () => {
|
|
23
|
+
expect(() => getPluralTranslation({
|
|
24
|
+
currentLanguage: `es`,
|
|
25
|
+
fallbackLanguage: fallbackLanguage,
|
|
26
|
+
count: -5,
|
|
27
|
+
// @ts-expect-error testing undefined translations
|
|
28
|
+
translations: undefined,
|
|
29
|
+
})).toThrow(`[i18n] Translations object is undefined or null`);
|
|
30
|
+
});
|
|
31
|
+
it(`returns the correct translation for negative count`, () => {
|
|
32
|
+
expect(getPluralTranslation({
|
|
33
|
+
currentLanguage: `es`,
|
|
34
|
+
fallbackLanguage: fallbackLanguage,
|
|
35
|
+
count: -5,
|
|
36
|
+
translations,
|
|
37
|
+
})).toBe(`Tienes un conteo negativo.`);
|
|
38
|
+
});
|
|
39
|
+
it(`returns the correct translation for zero count`, () => {
|
|
40
|
+
expect(getPluralTranslation({
|
|
41
|
+
currentLanguage: `en`,
|
|
42
|
+
fallbackLanguage: fallbackLanguage,
|
|
43
|
+
count: 0,
|
|
44
|
+
translations,
|
|
45
|
+
})).toBe(`You have no items.`);
|
|
46
|
+
});
|
|
47
|
+
it(`returns the correct translation for one count`, () => {
|
|
48
|
+
expect(getPluralTranslation({
|
|
49
|
+
currentLanguage: `es`,
|
|
50
|
+
fallbackLanguage: fallbackLanguage,
|
|
51
|
+
count: 1,
|
|
52
|
+
translations,
|
|
53
|
+
})).toBe(`Tienes un artículo.`);
|
|
54
|
+
});
|
|
55
|
+
it(`returns the correct translation for two count`, () => {
|
|
56
|
+
expect(getPluralTranslation({
|
|
57
|
+
currentLanguage: `en`,
|
|
58
|
+
fallbackLanguage: fallbackLanguage,
|
|
59
|
+
count: 2,
|
|
60
|
+
translations,
|
|
61
|
+
})).toBe(`You have two items.`);
|
|
62
|
+
});
|
|
63
|
+
it(`returns the correct translation for many count`, () => {
|
|
64
|
+
expect(getPluralTranslation({
|
|
65
|
+
currentLanguage: `es`,
|
|
66
|
+
fallbackLanguage: fallbackLanguage,
|
|
67
|
+
count: 10,
|
|
68
|
+
translations,
|
|
69
|
+
})).toBe(`Tienes muchos artículos.`);
|
|
70
|
+
});
|
|
71
|
+
it(`returns the correct translation for exact match count`, () => {
|
|
72
|
+
expect(getPluralTranslation({
|
|
73
|
+
currentLanguage: `en`,
|
|
74
|
+
fallbackLanguage: fallbackLanguage,
|
|
75
|
+
count: 42,
|
|
76
|
+
translations,
|
|
77
|
+
})).toBe(`You have the perfect count of items!`);
|
|
78
|
+
});
|
|
79
|
+
it(`falls back to default language if translation is missing for current language`, () => {
|
|
80
|
+
const incompleteTranslations = {
|
|
81
|
+
en: {
|
|
82
|
+
one: `You have one item.`,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
expect(getPluralTranslation({
|
|
86
|
+
currentLanguage: `es`,
|
|
87
|
+
fallbackLanguage: fallbackLanguage,
|
|
88
|
+
count: 1,
|
|
89
|
+
// @ts-expect-error incomplete translation for testing
|
|
90
|
+
translations: incompleteTranslations,
|
|
91
|
+
})).toBe(`You have one item.`);
|
|
92
|
+
});
|
|
93
|
+
it(`returns ${MISSING_TRANSLATION} if translation is missing for both current and default languages`, () => {
|
|
94
|
+
const incompleteTranslations = {
|
|
95
|
+
de: {
|
|
96
|
+
one: `Tienes un artículo.`,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
expect(getPluralTranslation({
|
|
100
|
+
currentLanguage: `en`,
|
|
101
|
+
fallbackLanguage: fallbackLanguage,
|
|
102
|
+
count: 1,
|
|
103
|
+
// @ts-expect-error incomplete translation for testing
|
|
104
|
+
translations: incompleteTranslations,
|
|
105
|
+
})).toBe(MISSING_TRANSLATION);
|
|
106
|
+
});
|
|
107
|
+
it(`Warn: No pluralization match found for count`, () => {
|
|
108
|
+
const consoleWarnSpy = jest.spyOn(console, `warn`).mockImplementation();
|
|
109
|
+
const incompleteTranslations = {
|
|
110
|
+
en: {
|
|
111
|
+
one: `You have one item.`,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
getPluralTranslation({
|
|
115
|
+
currentLanguage: `en`,
|
|
116
|
+
fallbackLanguage: fallbackLanguage,
|
|
117
|
+
count: 5,
|
|
118
|
+
// @ts-expect-error incomplete translation for testing
|
|
119
|
+
translations: incompleteTranslations,
|
|
120
|
+
});
|
|
121
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(`[i18n] No pluralization match found for count "5". Please provide appropriate pluralization translations.`);
|
|
122
|
+
consoleWarnSpy.mockRestore();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TranslationMap, TranslationValue } from "../type";
|
|
2
|
+
export declare const getTranslation: <const Language extends string, Value = TranslationValue>({ translation, language, languages, fallbackLanguage, }: {
|
|
3
|
+
translation: TranslationMap<Language, Value>;
|
|
4
|
+
language: Language;
|
|
5
|
+
languages: readonly Language[];
|
|
6
|
+
fallbackLanguage: Language;
|
|
7
|
+
}) => Value;
|
|
8
|
+
//# sourceMappingURL=getTranslation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getTranslation.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/util/getTranslation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAI3D,eAAO,MAAM,cAAc,GACzB,KAAK,CAAC,QAAQ,SAAS,MAAM,EAC7B,KAAK,GAAG,gBAAgB,EACxB,yDAKC;IACD,WAAW,EAAE,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7C,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IAC/B,gBAAgB,EAAE,QAAQ,CAAC;CAC5B,KAAG,KAwCH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MISSING_TRANSLATION } from "../const";
|
|
2
|
+
import { IS_DEV } from "../../env";
|
|
3
|
+
export const getTranslation = ({ translation, language, languages, fallbackLanguage, }) => {
|
|
4
|
+
if (!translation) {
|
|
5
|
+
throw new Error(`[i18n] Translation object is undefined or null`);
|
|
6
|
+
}
|
|
7
|
+
if (IS_DEV) {
|
|
8
|
+
const missing = languages.filter((l) => !(l in translation));
|
|
9
|
+
if (missing.length > 0) {
|
|
10
|
+
console.warn(`[i18n] Missing languages: ${missing.join(`, `)} in ${Object.keys(translation)}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const targetTranslation = translation[language];
|
|
14
|
+
if (targetTranslation !== undefined) {
|
|
15
|
+
return targetTranslation;
|
|
16
|
+
}
|
|
17
|
+
const fallbackTranslation = translation[fallbackLanguage];
|
|
18
|
+
if (fallbackTranslation !== undefined) {
|
|
19
|
+
if (IS_DEV) {
|
|
20
|
+
console.warn(`[i18n] Missing translation for language "${language}". Falling back to default language "${fallbackLanguage}"`);
|
|
21
|
+
}
|
|
22
|
+
return fallbackTranslation;
|
|
23
|
+
}
|
|
24
|
+
if (IS_DEV) {
|
|
25
|
+
console.warn(`[i18n] Missing translation for language "${language}" and for default language "${fallbackLanguage}"`);
|
|
26
|
+
}
|
|
27
|
+
return MISSING_TRANSLATION;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getTranslation.test.d.ts","sourceRoot":"","sources":["../../../../src/createI18nContext/util/getTranslation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getTranslation } from "./getTranslation";
|
|
2
|
+
import { MISSING_TRANSLATION } from "../const";
|
|
3
|
+
describe(`getTranslation`, () => {
|
|
4
|
+
const languages = [`en`, `es`, `sl`];
|
|
5
|
+
const translation = {
|
|
6
|
+
en: `Hello!`,
|
|
7
|
+
es: `¡Hola!`,
|
|
8
|
+
sl: `Živjo!`,
|
|
9
|
+
};
|
|
10
|
+
const fallbackLanguage = `es`;
|
|
11
|
+
it(`returns the correct translation for the specified language`, () => {
|
|
12
|
+
expect(getTranslation({
|
|
13
|
+
translation,
|
|
14
|
+
language: `sl`,
|
|
15
|
+
languages,
|
|
16
|
+
fallbackLanguage: fallbackLanguage,
|
|
17
|
+
})).toBe(`Živjo!`);
|
|
18
|
+
});
|
|
19
|
+
it(`falls back to the default language if the specified language translation is missing`, () => {
|
|
20
|
+
expect(
|
|
21
|
+
// @ts-expect-error testing missing language
|
|
22
|
+
getTranslation({
|
|
23
|
+
translation,
|
|
24
|
+
languages,
|
|
25
|
+
fallbackLanguage: fallbackLanguage,
|
|
26
|
+
})).toBe(`¡Hola!`);
|
|
27
|
+
});
|
|
28
|
+
it(`falls back to the default language if the specified language is not supported`, () => {
|
|
29
|
+
expect(getTranslation({
|
|
30
|
+
// @ts-expect-error unsupported language
|
|
31
|
+
translation,
|
|
32
|
+
language: `fr`,
|
|
33
|
+
languages,
|
|
34
|
+
fallbackLanguage: fallbackLanguage,
|
|
35
|
+
})).toBe(`¡Hola!`);
|
|
36
|
+
});
|
|
37
|
+
it(`returns ${MISSING_TRANSLATION} if the translation is missing for both the specified and default languages`, () => {
|
|
38
|
+
const incompleteTranslation = {
|
|
39
|
+
en: `Hello!`,
|
|
40
|
+
};
|
|
41
|
+
expect(getTranslation({
|
|
42
|
+
// @ts-expect-error incomplete translation
|
|
43
|
+
translation: incompleteTranslation,
|
|
44
|
+
languages,
|
|
45
|
+
fallbackLanguage: fallbackLanguage,
|
|
46
|
+
language: `de`,
|
|
47
|
+
})).toBe(MISSING_TRANSLATION);
|
|
48
|
+
});
|
|
49
|
+
it(`Error: Translation object is undefined or null`, () => {
|
|
50
|
+
expect(() => {
|
|
51
|
+
getTranslation({
|
|
52
|
+
// @ts-expect-error testing undefined translation
|
|
53
|
+
translation: undefined,
|
|
54
|
+
languages,
|
|
55
|
+
fallbackLanguage: fallbackLanguage,
|
|
56
|
+
language: `es`,
|
|
57
|
+
});
|
|
58
|
+
}).toThrow(`[i18n] Translation object is undefined or null`);
|
|
59
|
+
});
|
|
60
|
+
it(`Warn: Missing languages`, () => {
|
|
61
|
+
const incompleteLanguages = [`en`, `de`];
|
|
62
|
+
const consoleWarnSpy = jest.spyOn(console, `warn`).mockImplementation();
|
|
63
|
+
getTranslation({
|
|
64
|
+
// @ts-expect-error incomplete languages
|
|
65
|
+
translation,
|
|
66
|
+
language: `sl`,
|
|
67
|
+
languages: incompleteLanguages,
|
|
68
|
+
fallbackLanguage: fallbackLanguage,
|
|
69
|
+
});
|
|
70
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(`[i18n] Missing languages: de in en,es,sl`);
|
|
71
|
+
});
|
|
72
|
+
it(`Warn: Missing translation for language, falling back to default`, () => {
|
|
73
|
+
const consoleWarnSpy = jest.spyOn(console, `warn`).mockImplementation();
|
|
74
|
+
getTranslation({
|
|
75
|
+
// @ts-expect-error testing translation for language
|
|
76
|
+
translation,
|
|
77
|
+
language: `pt`,
|
|
78
|
+
languages,
|
|
79
|
+
fallbackLanguage: fallbackLanguage,
|
|
80
|
+
});
|
|
81
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(`[i18n] Missing translation for language "pt". Falling back to default language "es"`);
|
|
82
|
+
});
|
|
83
|
+
it(`Warn: Missing translation for language and for default language`, () => {
|
|
84
|
+
const consoleWarnSpy = jest.spyOn(console, `warn`).mockImplementation();
|
|
85
|
+
getTranslation({
|
|
86
|
+
// @ts-expect-error testing translation for language and default language
|
|
87
|
+
translation,
|
|
88
|
+
language: `ru`,
|
|
89
|
+
languages,
|
|
90
|
+
fallbackLanguage: `nl`,
|
|
91
|
+
});
|
|
92
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(`[i18n] Missing translation for language "ru" and for default language "nl"`);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/env.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,MAAM,SACsD,CAAC"}
|
package/dist/esm/env.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createI18nContext } from "./createI18nContext";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/index.test.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-scoped-i18n",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A scoped internationalization (i18n) library for React applications",
|
|
5
|
+
"main": "./dist/cjs/index.js",
|
|
6
|
+
"module": "./dist/esm/index.js",
|
|
7
|
+
"types": "./dist/esm/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/esm/index.js",
|
|
11
|
+
"require": "./dist/cjs/index.js",
|
|
12
|
+
"types": "./dist/esm/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
20
|
+
"build:esm": "tsc -p tsconfig.esm.json",
|
|
21
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
22
|
+
"prepare": "husky",
|
|
23
|
+
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
|
24
|
+
"prettier": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,md}'",
|
|
25
|
+
"test": "jest --silent",
|
|
26
|
+
"test:coverage": "jest --coverage --silent",
|
|
27
|
+
"tsc": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=19",
|
|
31
|
+
"react-dom": ">=19"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@testing-library/react": "^16.3.1",
|
|
35
|
+
"@types/jest": "^30.0.0",
|
|
36
|
+
"@types/node": "^25.0.3",
|
|
37
|
+
"@types/react": "^19.2.7",
|
|
38
|
+
"@types/react-dom": "^19.2.3",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
40
|
+
"eslint": "^8.56.0",
|
|
41
|
+
"eslint-config-prettier": "^10.1.8",
|
|
42
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
43
|
+
"eslint-plugin-react": "^7.37.5",
|
|
44
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
45
|
+
"eslint-plugin-unused-imports": "^4.3.0",
|
|
46
|
+
"husky": "^9.1.7",
|
|
47
|
+
"jest": "^30.2.0",
|
|
48
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
49
|
+
"prettier": "3.5.3",
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0",
|
|
52
|
+
"ts-jest": "^29.4.5",
|
|
53
|
+
"typescript": "^5.9.2"
|
|
54
|
+
}
|
|
55
|
+
}
|