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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Amer Kočan
|
|
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
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# react-scoped-i18n 🌐
|
|
2
|
+
|
|
3
|
+
`react-scoped-i18n 🌐` is a **fully type-safe** i18n solution for **React**.
|
|
4
|
+
|
|
5
|
+
It encourages writing translations right next to the components that use them.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Getting started:
|
|
10
|
+
|
|
11
|
+
### [Installation & Usage](/docs/usage.md)
|
|
12
|
+
|
|
13
|
+
### [API](/docs/api.md)
|
|
14
|
+
|
|
15
|
+
###### Note: You can use this with both React and React Native (+ Expo) projects. 🚀
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why `react-scoped-i18n 🌐`?
|
|
21
|
+
|
|
22
|
+
### Key features:
|
|
23
|
+
- Very minimal setup with out-of-the-box number & date formatting
|
|
24
|
+
- Fully type-safe:
|
|
25
|
+
- - missing translations or unsupported languages are compile-time errors
|
|
26
|
+
- - return types of `t()` are inferred from translation values
|
|
27
|
+
- Utilize the widely supported [Internationalization API (Intl)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) for number, currency, date and time formatting
|
|
28
|
+
- Usage is entirely in the runtime; no build-time transforms, no new syntax is required for string interpolation or dynamic translations generated at runtime, everything is plain JS/TS
|
|
29
|
+
|
|
30
|
+
### The focus is on dev experience:
|
|
31
|
+
- Translations are colocated with the components that use them; looking up translations in the codebase always immediately leads to the relevant component code
|
|
32
|
+
- No context switching between component code and translation files when developing UI
|
|
33
|
+
- No tedious naming of translation keys, as they usually provide little value
|
|
34
|
+
- Ditch restrictive translation file formats (JSON, YAML); use the full power of JS/TS
|
|
35
|
+
- Runs within React's context system. No additional build steps, changes can be hot-reloaded, language switches reflected immediately
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## What does it look like?
|
|
40
|
+
|
|
41
|
+
##### Very Basic Example:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useI18n } from "@/i18n";
|
|
45
|
+
import { Heading, Button } from "@/components";
|
|
46
|
+
|
|
47
|
+
export const WelcomeMessage = () => {
|
|
48
|
+
const { t, commons } = useI18n();
|
|
49
|
+
|
|
50
|
+
const name = `John`;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<Heading>
|
|
55
|
+
{t({
|
|
56
|
+
// all fully type-safe for all configured languages
|
|
57
|
+
en: `Welcome to the website, ${name}!`,
|
|
58
|
+
es: `¡Bienvenido al sitio web, ${name}!`,
|
|
59
|
+
sl: `Dobrodošli na spletno stran, ${name}!`,
|
|
60
|
+
})}
|
|
61
|
+
</Heading>
|
|
62
|
+
<Button>{
|
|
63
|
+
// "commons" object is used for commonly used, shared translations
|
|
64
|
+
t(commons.continue)
|
|
65
|
+
}</Button>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>
|
|
73
|
+
Number Formatting Basic Example:
|
|
74
|
+
</summary>
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { useI18n } from "@/i18n";
|
|
78
|
+
import { Text } from "@/components";
|
|
79
|
+
|
|
80
|
+
export const PriceTag = () => {
|
|
81
|
+
const { t, format } = useI18n();
|
|
82
|
+
|
|
83
|
+
const price = 19.99;
|
|
84
|
+
|
|
85
|
+
const currency = `USD`;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Text>
|
|
89
|
+
{t({
|
|
90
|
+
en: `The price is ${format.currency(price, currency)}.`,
|
|
91
|
+
es: `El precio es ${format.currency(price, currency)}.`,
|
|
92
|
+
sl: `Cena je ${format.currency(price, currency)}.`,
|
|
93
|
+
})}
|
|
94
|
+
</Text>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
</details>
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary>
|
|
103
|
+
Pluralization Basic Example:
|
|
104
|
+
</summary>
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useI18n } from "@/i18n";
|
|
108
|
+
import { Text } from "@/components";
|
|
109
|
+
|
|
110
|
+
export const Apples = () => {
|
|
111
|
+
const { tPlural } = useI18n();
|
|
112
|
+
|
|
113
|
+
const count = 12;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<Text>
|
|
117
|
+
{tPlural(count, {
|
|
118
|
+
en: {
|
|
119
|
+
negative: `You are in apple debt...`,
|
|
120
|
+
one: `You have one apple.`,
|
|
121
|
+
many: `You have ${count} apples.`,
|
|
122
|
+
42: `You have the perfect number of apples!`, // ‼️ you can target specific numbers
|
|
123
|
+
},
|
|
124
|
+
es: {
|
|
125
|
+
one: `Tienes una manzana.`,
|
|
126
|
+
many: `Tienes ${count} manzanas.`,
|
|
127
|
+
},
|
|
128
|
+
sl: {
|
|
129
|
+
one: `Imaš eno jabolko.`,
|
|
130
|
+
two: `Imaš dve jabolki.`, // ‼️ handling dual form in Slovenian that English and Spanish don't have
|
|
131
|
+
many: `Imaš ${count} jabolk.`,
|
|
132
|
+
}
|
|
133
|
+
})}
|
|
134
|
+
</Text>
|
|
135
|
+
)
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
</details>
|
|
139
|
+
|
|
140
|
+
----
|
|
141
|
+
|
|
142
|
+
`react-scoped-i18n 🌐` shines most when devs are the ones adding translations into the app, and when the number of supported languages is small-to-medium sized.
|
|
143
|
+
|
|
144
|
+
You can find more in-depth examples in the [Installation & Usage](/docs/usage.md) and the API definitions in [API](/docs/api.md)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
#### [License: MIT](/LICENSE)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { TranslationMap } from "../type";
|
|
2
|
+
export declare const createCommons: <const Languages extends readonly string[], const Commons extends Record<string, TranslationMap<Languages[number], string>>>({ commons, currentLanguage, fallbackLanguage, }: {
|
|
3
|
+
commons: Commons;
|
|
4
|
+
currentLanguage: Languages[number];
|
|
5
|
+
fallbackLanguage: Languages[number];
|
|
6
|
+
}) => { [K in keyof Commons]?: string | undefined; };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCommons = void 0;
|
|
4
|
+
const env_1 = require("../../env");
|
|
5
|
+
const const_1 = require("../const");
|
|
6
|
+
const createCommons = ({ commons, currentLanguage, fallbackLanguage, }) => {
|
|
7
|
+
const result = {};
|
|
8
|
+
for (const key in commons) {
|
|
9
|
+
const translationMap = commons[key];
|
|
10
|
+
const targetTranslation = translationMap[currentLanguage];
|
|
11
|
+
if (targetTranslation) {
|
|
12
|
+
result[key] = targetTranslation;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const fallbackTranslation = translationMap[fallbackLanguage];
|
|
16
|
+
if (fallbackTranslation) {
|
|
17
|
+
if (env_1.IS_DEV) {
|
|
18
|
+
console.warn(`[i18n] Missing translation for key "${key}" in current language "${currentLanguage}" in "commons". Defaulting to fallback language "${fallbackLanguage}".`);
|
|
19
|
+
}
|
|
20
|
+
result[key] = fallbackTranslation;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (env_1.IS_DEV) {
|
|
24
|
+
console.warn(`[i18n] Missing translation for key "${key}" in both current language "${currentLanguage}" and fallback language "${fallbackLanguage}" in "commons".`);
|
|
25
|
+
}
|
|
26
|
+
result[key] = const_1.MISSING_TRANSLATION;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
exports.createCommons = createCommons;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createCommons_1 = require("./createCommons");
|
|
4
|
+
describe(`createCommons`, () => {
|
|
5
|
+
it(`returns the correct common translations based on current language and fallback`, () => {
|
|
6
|
+
const result = (0, createCommons_1.createCommons)({
|
|
7
|
+
commons: {
|
|
8
|
+
welcome: {
|
|
9
|
+
en: `Welcome`,
|
|
10
|
+
es: `Bienvenido`,
|
|
11
|
+
fr: `Bienvenue`,
|
|
12
|
+
},
|
|
13
|
+
goodbye: {
|
|
14
|
+
en: `Goodbye`,
|
|
15
|
+
es: `Adiós`,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
currentLanguage: `fr`,
|
|
19
|
+
fallbackLanguage: `en`,
|
|
20
|
+
});
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
welcome: `Bienvenue`,
|
|
23
|
+
goodbye: `Goodbye`,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const createFormat: <const Languages extends readonly string[]>({ currentLanguage, }: {
|
|
2
|
+
currentLanguage: Languages[number];
|
|
3
|
+
}) => {
|
|
4
|
+
number: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
5
|
+
date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
6
|
+
time: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
7
|
+
currency: (value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, `style` | `currency`>) => string;
|
|
8
|
+
percentage: (value: number, options?: Omit<Intl.NumberFormatOptions, `style`>) => string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFormat = void 0;
|
|
4
|
+
const createFormat = ({ currentLanguage, }) => {
|
|
5
|
+
const defaultNumberFormatter = new Intl.NumberFormat(currentLanguage);
|
|
6
|
+
const defaultDateFormatter = new Intl.DateTimeFormat(currentLanguage);
|
|
7
|
+
const defaultTimeFormatter = new Intl.DateTimeFormat(currentLanguage, {
|
|
8
|
+
hour: `numeric`,
|
|
9
|
+
minute: `numeric`,
|
|
10
|
+
second: `numeric`,
|
|
11
|
+
});
|
|
12
|
+
const defaultPercentageFormatter = new Intl.NumberFormat(currentLanguage, {
|
|
13
|
+
style: `percent`,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
number: (value, options) => {
|
|
17
|
+
if (options) {
|
|
18
|
+
return new Intl.NumberFormat(currentLanguage, options).format(value);
|
|
19
|
+
}
|
|
20
|
+
return defaultNumberFormatter.format(value);
|
|
21
|
+
},
|
|
22
|
+
date: (value, options) => {
|
|
23
|
+
if (options) {
|
|
24
|
+
return new Intl.DateTimeFormat(currentLanguage, options).format(value);
|
|
25
|
+
}
|
|
26
|
+
return defaultDateFormatter.format(value);
|
|
27
|
+
},
|
|
28
|
+
time: (value, options) => {
|
|
29
|
+
if (options) {
|
|
30
|
+
return new Intl.DateTimeFormat(currentLanguage, {
|
|
31
|
+
...options,
|
|
32
|
+
}).format(value);
|
|
33
|
+
}
|
|
34
|
+
return defaultTimeFormatter.format(value);
|
|
35
|
+
},
|
|
36
|
+
currency: (value, currency, options) => {
|
|
37
|
+
return new Intl.NumberFormat(currentLanguage, {
|
|
38
|
+
style: `currency`,
|
|
39
|
+
currency,
|
|
40
|
+
...options,
|
|
41
|
+
}).format(value);
|
|
42
|
+
},
|
|
43
|
+
percentage: (value, options) => {
|
|
44
|
+
if (options) {
|
|
45
|
+
return new Intl.NumberFormat(currentLanguage, {
|
|
46
|
+
style: `percent`,
|
|
47
|
+
...options,
|
|
48
|
+
}).format(value);
|
|
49
|
+
}
|
|
50
|
+
return defaultPercentageFormatter.format(value);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
exports.createFormat = createFormat;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createFormat_1 = require("./createFormat");
|
|
4
|
+
describe(`createFormat`, () => {
|
|
5
|
+
const format = (0, createFormat_1.createFormat)({
|
|
6
|
+
currentLanguage: `en-US`,
|
|
7
|
+
});
|
|
8
|
+
it(`number formatter returns a formatted value`, () => {
|
|
9
|
+
const formattedNumber = format.number(1234567.89);
|
|
10
|
+
expect(formattedNumber).toBe(`1,234,567.89`);
|
|
11
|
+
});
|
|
12
|
+
it(`number formatter returns a formatted value with options`, () => {
|
|
13
|
+
const formattedNumber = format.number(0.1234, {
|
|
14
|
+
style: `percent`,
|
|
15
|
+
minimumFractionDigits: 2,
|
|
16
|
+
});
|
|
17
|
+
expect(formattedNumber).toBe(`12.34%`);
|
|
18
|
+
});
|
|
19
|
+
it(`date formatter returns a formatted value`, () => {
|
|
20
|
+
const date = new Date(`2024-01-01T12:00:00Z`);
|
|
21
|
+
const formattedDate = format.date(date);
|
|
22
|
+
expect(formattedDate).toBe(`1/1/2024`);
|
|
23
|
+
});
|
|
24
|
+
it(`date formatter returns a formatted value with options`, () => {
|
|
25
|
+
const date = new Date(`1999-01-01T12:00:00Z`);
|
|
26
|
+
const formattedDate = format.date(date, {
|
|
27
|
+
year: `numeric`,
|
|
28
|
+
month: `long`,
|
|
29
|
+
day: `numeric`,
|
|
30
|
+
});
|
|
31
|
+
expect(formattedDate).toBe(`January 1, 1999`);
|
|
32
|
+
});
|
|
33
|
+
it(`time formatter returns a formatted value`, () => {
|
|
34
|
+
const date = new Date(`1912-06-23T02:15:00`);
|
|
35
|
+
const formattedTime = format.time(date);
|
|
36
|
+
expect(formattedTime).toBe(`2:15:00 AM`);
|
|
37
|
+
});
|
|
38
|
+
it(`time formatter returns a formatted value with options`, () => {
|
|
39
|
+
const date = new Date(`2000-12-31T23:45:30`);
|
|
40
|
+
const formattedTime = format.time(date, {
|
|
41
|
+
hour12: false,
|
|
42
|
+
hour: `2-digit`,
|
|
43
|
+
minute: `2-digit`,
|
|
44
|
+
});
|
|
45
|
+
expect(formattedTime).toBe(`23:45`);
|
|
46
|
+
});
|
|
47
|
+
it(`currency formatter returns a formatted value`, () => {
|
|
48
|
+
const formattedCurrency = format.currency(1234.567, `USD`);
|
|
49
|
+
expect(formattedCurrency).toBe(`$1,234.57`);
|
|
50
|
+
});
|
|
51
|
+
it(`currency formatter returns a formatted value with options`, () => {
|
|
52
|
+
const formattedCurrency = format.currency(1234.567, `EUR`, {
|
|
53
|
+
maximumFractionDigits: 1,
|
|
54
|
+
});
|
|
55
|
+
expect(formattedCurrency).toBe(`€1,234.6`);
|
|
56
|
+
});
|
|
57
|
+
it(`percentage formatter returns a formatted value`, () => {
|
|
58
|
+
const formattedPercentage = format.percentage(0.1234);
|
|
59
|
+
expect(formattedPercentage).toBe(`12%`);
|
|
60
|
+
});
|
|
61
|
+
it(`percentage formatter returns a formatted value `, () => {
|
|
62
|
+
const formattedPercentage = format.percentage(0.1234);
|
|
63
|
+
expect(formattedPercentage).toBe(`12%`);
|
|
64
|
+
});
|
|
65
|
+
it(`percentage formatter returns a formatted value with options`, () => {
|
|
66
|
+
const formattedPercentage = format.percentage(0.1234, {
|
|
67
|
+
minimumFractionDigits: 2,
|
|
68
|
+
});
|
|
69
|
+
expect(formattedPercentage).toBe(`12.34%`);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { TranslationMap, TranslationValue } from "../type";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
export declare const createT: <const Languages extends readonly string[]>({ currentLanguage, fallbackLanguage, languages, }: {
|
|
4
|
+
currentLanguage: Languages[number];
|
|
5
|
+
fallbackLanguage: Languages[number];
|
|
6
|
+
languages: Readonly<Languages>;
|
|
7
|
+
}) => <Value extends TranslationValue = ReactNode>(translation: TranslationMap<Languages[number], Value>) => Value;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createT = void 0;
|
|
4
|
+
const getTranslation_1 = require("../util/getTranslation");
|
|
5
|
+
const createT = ({ currentLanguage, fallbackLanguage, languages, }) => {
|
|
6
|
+
return (translation) => {
|
|
7
|
+
return (0, getTranslation_1.getTranslation)({
|
|
8
|
+
translation,
|
|
9
|
+
language: currentLanguage,
|
|
10
|
+
languages,
|
|
11
|
+
fallbackLanguage,
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
exports.createT = createT;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const createT_1 = require("./createT");
|
|
5
|
+
describe(`createT`, () => {
|
|
6
|
+
const t = (0, createT_1.createT)({
|
|
7
|
+
currentLanguage: `fr`,
|
|
8
|
+
fallbackLanguage: `en`,
|
|
9
|
+
languages: [`en`, `es`, `fr`],
|
|
10
|
+
});
|
|
11
|
+
it(`t helper returns a translation`, () => {
|
|
12
|
+
expect(t({
|
|
13
|
+
en: `Hello`,
|
|
14
|
+
es: `Hola`,
|
|
15
|
+
fr: `Bonjour`,
|
|
16
|
+
})).toBe(`Bonjour`);
|
|
17
|
+
expect(t({
|
|
18
|
+
en: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {}),
|
|
19
|
+
es: `Hola`,
|
|
20
|
+
fr: `Bonjour`,
|
|
21
|
+
})).toBe(`Bonjour`);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TranslationMap } from "../type";
|
|
2
|
+
export declare const createTGlobal: <const Language extends string>({ currentLanguage, languages, fallbackLanguage, }: {
|
|
3
|
+
currentLanguage: {
|
|
4
|
+
current: Language;
|
|
5
|
+
};
|
|
6
|
+
languages: readonly Language[];
|
|
7
|
+
fallbackLanguage: Language;
|
|
8
|
+
}) => (translation: TranslationMap<Language>) => import("../type").TranslationValue;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTGlobal = void 0;
|
|
4
|
+
const type_1 = require("../type");
|
|
5
|
+
const getTranslation_1 = require("../util/getTranslation");
|
|
6
|
+
const env_1 = require("../../env");
|
|
7
|
+
const createTGlobal = ({ currentLanguage, languages, fallbackLanguage, }) => {
|
|
8
|
+
/**
|
|
9
|
+
* Module-scoped translation function, to be used outside React components.
|
|
10
|
+
* Do not use inside React components, use `useI18n` hook instead.
|
|
11
|
+
*/
|
|
12
|
+
return (translation) => {
|
|
13
|
+
if (!(0, type_1.isLanguage)(currentLanguage.current, languages)) {
|
|
14
|
+
throw new Error(`tGlobal cannot be invoked before <I18nProvider> is mounted`);
|
|
15
|
+
}
|
|
16
|
+
const isValidLanguage = (0, type_1.isLanguage)(currentLanguage.current, languages);
|
|
17
|
+
if (!isValidLanguage) {
|
|
18
|
+
throw new Error(`Invalid current language "${currentLanguage}"`);
|
|
19
|
+
}
|
|
20
|
+
if (env_1.IS_DEV) {
|
|
21
|
+
console.info(`[i18n] Friendly reminder: Make sure you are not using "tGlobal" inside React components. Use "useI18n" hook instead. `);
|
|
22
|
+
}
|
|
23
|
+
return (0, getTranslation_1.getTranslation)({
|
|
24
|
+
translation: translation,
|
|
25
|
+
language: currentLanguage.current,
|
|
26
|
+
languages,
|
|
27
|
+
fallbackLanguage: fallbackLanguage,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
exports.createTGlobal = createTGlobal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createTGlobal_1 = require("./createTGlobal");
|
|
4
|
+
describe(`createTGlobal`, () => {
|
|
5
|
+
const fakeCurrentLanguageRef = { current: `de` };
|
|
6
|
+
const tGlobal = (0, createTGlobal_1.createTGlobal)({
|
|
7
|
+
currentLanguage: fakeCurrentLanguageRef,
|
|
8
|
+
fallbackLanguage: `en`,
|
|
9
|
+
languages: [`en`, `es`, `fr`, `de`],
|
|
10
|
+
});
|
|
11
|
+
it(`tGlobal helper returns a translation`, () => {
|
|
12
|
+
const translation = {
|
|
13
|
+
en: `Goodbye`,
|
|
14
|
+
es: `Adiós`,
|
|
15
|
+
fr: `Au revoir`,
|
|
16
|
+
de: `Auf Wiedersehen`,
|
|
17
|
+
};
|
|
18
|
+
expect(tGlobal(translation)).toBe(`Auf Wiedersehen`);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { PluralTranslationMap } from "../type";
|
|
2
|
+
export declare const createTPlural: <const Languages extends readonly string[]>({ currentLanguage, fallbackLanguage, }: {
|
|
3
|
+
currentLanguage: Languages[number];
|
|
4
|
+
fallbackLanguage: Languages[number];
|
|
5
|
+
}) => (count: number, translations: PluralTranslationMap<Languages[number]>) => 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>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTPlural = void 0;
|
|
4
|
+
const getPluralTranslation_1 = require("../util/getPluralTranslation");
|
|
5
|
+
const createTPlural = ({ currentLanguage, fallbackLanguage, }) => {
|
|
6
|
+
/**
|
|
7
|
+
* Pluralization helper function.
|
|
8
|
+
* Selects the appropriate translation based on the count provided.
|
|
9
|
+
*/
|
|
10
|
+
return (count, translations) => {
|
|
11
|
+
return (0, getPluralTranslation_1.getPluralTranslation)({
|
|
12
|
+
currentLanguage,
|
|
13
|
+
fallbackLanguage: fallbackLanguage,
|
|
14
|
+
count,
|
|
15
|
+
translations,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
exports.createTPlural = createTPlural;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const createTPlural_1 = require("./createTPlural");
|
|
4
|
+
describe(`createTPlural`, () => {
|
|
5
|
+
const tPlural = (0, createTPlural_1.createTPlural)({
|
|
6
|
+
currentLanguage: `en`,
|
|
7
|
+
fallbackLanguage: `en`,
|
|
8
|
+
});
|
|
9
|
+
it(`tPlural helper returns a translation for specific number`, () => {
|
|
10
|
+
const count = 2;
|
|
11
|
+
const translation = tPlural(count, {
|
|
12
|
+
en: {
|
|
13
|
+
0: `You have no messages`,
|
|
14
|
+
one: `You have 1 message`,
|
|
15
|
+
many: `You have ${count} messages`,
|
|
16
|
+
},
|
|
17
|
+
sl: {
|
|
18
|
+
0: `Nimate sporočil`,
|
|
19
|
+
one: `Imate 1 sporočilo`,
|
|
20
|
+
two: `Imate 2 sporočili`,
|
|
21
|
+
many: `Imate ${count} sporočil`,
|
|
22
|
+
},
|
|
23
|
+
pt: {
|
|
24
|
+
0: `Você não tem mensagens`,
|
|
25
|
+
one: `Você tem 1 mensagem`,
|
|
26
|
+
many: `Você tem ${count} mensagens`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
expect(translation).toBe(`You have 2 messages`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { I18nContextType, TranslationMap } from "../type";
|
|
2
|
+
import { Context } from "react";
|
|
3
|
+
export declare const createUseI18n: <const Languages extends readonly string[], const Commons extends Record<string, TranslationMap<Languages[number], string>>>({ I18nContext, languages, fallbackLanguage, commons, }: {
|
|
4
|
+
I18nContext: Context<I18nContextType<Languages[number]> | null>;
|
|
5
|
+
languages: Readonly<Languages>;
|
|
6
|
+
fallbackLanguage: Languages[number];
|
|
7
|
+
commons?: Commons;
|
|
8
|
+
}) => () => {
|
|
9
|
+
t: <Value extends import("../type").TranslationValue = import("react").ReactNode>(translation: TranslationMap<Languages[number], Value>) => Value;
|
|
10
|
+
tPlural: (count: number, translations: import("../type").PluralTranslationMap<Languages[number]>) => 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>;
|
|
11
|
+
setCurrentLanguage: import("react").Dispatch<import("react").SetStateAction<Languages[number]>>;
|
|
12
|
+
currentLanguage: Languages[number];
|
|
13
|
+
format: {
|
|
14
|
+
number: (value: number, options?: Intl.NumberFormatOptions) => string;
|
|
15
|
+
date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
16
|
+
time: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string;
|
|
17
|
+
currency: (value: number, currency: string, options?: Omit<Intl.NumberFormatOptions, `style` | `currency`>) => string;
|
|
18
|
+
percentage: (value: number, options?: Omit<Intl.NumberFormatOptions, `style`>) => string;
|
|
19
|
+
};
|
|
20
|
+
commons: { [K in keyof Commons]?: string | undefined; };
|
|
21
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUseI18n = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const createT_1 = require("./createT");
|
|
6
|
+
const createTPlural_1 = require("./createTPlural");
|
|
7
|
+
const createFormat_1 = require("./createFormat");
|
|
8
|
+
const createCommons_1 = require("./createCommons");
|
|
9
|
+
const createUseI18n = ({ I18nContext, languages, fallbackLanguage, commons, }) => {
|
|
10
|
+
/**
|
|
11
|
+
* Hook to access i18n functions and current language.
|
|
12
|
+
*/
|
|
13
|
+
return () => {
|
|
14
|
+
const context = (0, react_1.useContext)(I18nContext);
|
|
15
|
+
if (!context) {
|
|
16
|
+
throw new Error(`useI18n must be used inside <Provider>`);
|
|
17
|
+
}
|
|
18
|
+
const { currentLanguage, setCurrentLanguage } = context;
|
|
19
|
+
const t = (0, react_1.useMemo)(() => {
|
|
20
|
+
return (0, createT_1.createT)({
|
|
21
|
+
currentLanguage,
|
|
22
|
+
languages,
|
|
23
|
+
fallbackLanguage: fallbackLanguage,
|
|
24
|
+
});
|
|
25
|
+
}, [currentLanguage]);
|
|
26
|
+
const tPlural = (0, react_1.useMemo)(() => {
|
|
27
|
+
return (0, createTPlural_1.createTPlural)({
|
|
28
|
+
currentLanguage,
|
|
29
|
+
fallbackLanguage: fallbackLanguage,
|
|
30
|
+
});
|
|
31
|
+
}, [currentLanguage]);
|
|
32
|
+
const format = (0, react_1.useMemo)(() => {
|
|
33
|
+
return (0, createFormat_1.createFormat)({ currentLanguage });
|
|
34
|
+
}, [currentLanguage]);
|
|
35
|
+
const _commons = (0, react_1.useMemo)(() => {
|
|
36
|
+
return (0, createCommons_1.createCommons)({
|
|
37
|
+
commons: commons || {},
|
|
38
|
+
currentLanguage,
|
|
39
|
+
fallbackLanguage,
|
|
40
|
+
});
|
|
41
|
+
}, [currentLanguage]);
|
|
42
|
+
return {
|
|
43
|
+
t,
|
|
44
|
+
tPlural,
|
|
45
|
+
setCurrentLanguage,
|
|
46
|
+
currentLanguage,
|
|
47
|
+
format,
|
|
48
|
+
commons: _commons,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
exports.createUseI18n = createUseI18n;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|