kotori 4.0.1 → 5.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/README.md +30 -35
- package/dist/index.cjs +2 -7
- package/dist/index.d.cts +1 -3
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +2 -7
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ t(intro, { name: 'John', time: '12-00' })
|
|
|
42
42
|
- No JSON
|
|
43
43
|
- No dependencies
|
|
44
44
|
- No build step
|
|
45
|
-
- 0.
|
|
45
|
+
- 0.31kb minified and gzipped
|
|
46
46
|
- Modular and tree-shakeable
|
|
47
47
|
- Language change in one page rerenders all pages
|
|
48
48
|
- Variables typed and inferred from string literals — no more string typos
|
|
@@ -58,7 +58,7 @@ npm i kotori
|
|
|
58
58
|
|
|
59
59
|
## Quick Start
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
### locales.ts
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
64
|
import { kotori } from 'kotori'
|
|
@@ -68,6 +68,7 @@ export const { useT, dict, setLanguage, t } = kotori({
|
|
|
68
68
|
secondaryLanguageTags: ['zh', 'ja', 'ms'],
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
// you can define your dicts in the same file or separate them by component, it's up to you
|
|
71
72
|
const intro = dict({
|
|
72
73
|
en: 'my name is {{name}}, I am {{age}} years old.',
|
|
73
74
|
zh: '我叫{{name}},我今年{{age}}岁了。',
|
|
@@ -82,37 +83,16 @@ const time = dict({
|
|
|
82
83
|
ms: 'waktu {{time}}',
|
|
83
84
|
// optional: type your arguments, by default it's `Record<'time', string | number>` in this example
|
|
84
85
|
})<{ time: `${number}:${number}:${number}` }>
|
|
85
|
-
|
|
86
|
-
const weather = dict({
|
|
87
|
-
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
88
|
-
zh: '{{city}}的天气湿度为{{humidity}}%。',
|
|
89
|
-
ja: '{{city}}の湿度は{{humidity}}%です。',
|
|
90
|
-
ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
|
|
91
|
-
})<{ city: string; humidity: number }>
|
|
92
|
-
|
|
93
|
-
const score = dict({
|
|
94
|
-
en: 'Your score is {{score}} out of {{total}}.',
|
|
95
|
-
zh: '你的得分是 {{total}} 分中的 {{score}} 分。',
|
|
96
|
-
ja: 'あなたのスコアは {{total}} 点中 {{score}} 点です。',
|
|
97
|
-
ms: 'Markah anda ialah {{score}} daripada {{total}}.',
|
|
98
|
-
})<{ score: number; total: number }>
|
|
99
|
-
|
|
100
|
-
const lastLogin = dict({
|
|
101
|
-
en: 'Last login: {{date}} at {{time}}',
|
|
102
|
-
zh: '上次登录:{{date}} {{time}}',
|
|
103
|
-
ja: '最終ログイン:{{date}} {{time}}',
|
|
104
|
-
ms: 'Log masuk terakhir: {{date}} pada {{time}}',
|
|
105
|
-
})<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
|
|
106
86
|
```
|
|
107
87
|
|
|
108
|
-
|
|
88
|
+
### page1.tsx
|
|
109
89
|
|
|
110
90
|
```tsx
|
|
111
91
|
import { useT, dict, setLanguage, t, intro, time } from './locales'
|
|
112
92
|
|
|
113
93
|
export const Page1 = () => {
|
|
114
94
|
|
|
115
|
-
const
|
|
95
|
+
const language = useT()
|
|
116
96
|
|
|
117
97
|
return (
|
|
118
98
|
<>
|
|
@@ -133,10 +113,25 @@ export const Page1 = () => {
|
|
|
133
113
|
}
|
|
134
114
|
```
|
|
135
115
|
|
|
136
|
-
|
|
116
|
+
### page2.tsx
|
|
137
117
|
|
|
138
118
|
```tsx
|
|
139
|
-
import { useT, dict, setLanguage, t,
|
|
119
|
+
import { useT, dict, setLanguage, t, } from './locales'
|
|
120
|
+
|
|
121
|
+
// you can also define dicts in the same file as your components, it's up to you
|
|
122
|
+
const weather = dict({
|
|
123
|
+
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
124
|
+
zh: '{{city}}的天气湿度为{{humidity}}%。',
|
|
125
|
+
ja: '{{city}}の湿度は{{humidity}}%です。',
|
|
126
|
+
ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
|
|
127
|
+
})<{ city: string; humidity: number }>
|
|
128
|
+
|
|
129
|
+
const lastLogin = dict({
|
|
130
|
+
en: 'Last login: {{date}} at {{time}}',
|
|
131
|
+
zh: '上次登录:{{date}} {{time}}',
|
|
132
|
+
ja: '最終ログイン:{{date}} {{time}}',
|
|
133
|
+
ms: 'Log masuk terakhir: {{date}} pada {{time}}',
|
|
134
|
+
})<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
|
|
140
135
|
|
|
141
136
|
export const Page2 = () => {
|
|
142
137
|
|
|
@@ -145,7 +140,6 @@ export const Page2 = () => {
|
|
|
145
140
|
return (
|
|
146
141
|
<>
|
|
147
142
|
<p>{t(weather, { city: 'Kuala Lumpur', humidity: 80 })}</p>
|
|
148
|
-
<p>{t(score, { score: 87, total: 100 })}</p>
|
|
149
143
|
<p>{t(lastLogin, { date: '2024-04-24', time: '09:30' })}</p>
|
|
150
144
|
</>
|
|
151
145
|
)
|
|
@@ -203,15 +197,17 @@ Returns the translated string for the current language. `args` is required if th
|
|
|
203
197
|
|
|
204
198
|
### `useT()`
|
|
205
199
|
|
|
206
|
-
React hook. Returns
|
|
207
|
-
|
|
208
|
-
| return | type | description |
|
|
209
|
-
| --- | --- | --- |
|
|
210
|
-
| `language` | `primaryLanguageTag` \| `secondaryLanguageTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
|
|
200
|
+
React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called.
|
|
211
201
|
|
|
212
202
|
## Language Tags
|
|
213
203
|
|
|
214
|
-
kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) language tags. Both subtags (`en`, `zh`) and full tags (`en-US`, `zh-
|
|
204
|
+
kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) language tags. Both subtags (`en`, `zh`) and full tags (`en-US`, `zh-CN`) are accepted and validated at the type level.
|
|
205
|
+
|
|
206
|
+
## Tips
|
|
207
|
+
|
|
208
|
+
- If you plan to add new languages frequently, consider colocating all your dicts in a single file. It is easier to copy the entire file and hand it to an AI to translate.
|
|
209
|
+
- If your supported languages are fixed, consider splitting dicts by page or component. Translations stay close to the code that uses them and are easier to maintain. This approach also pairs well with TypeScript — every time you add a new language, type errors will guide you to every dict that needs updating.
|
|
210
|
+
- Both approaches are tree-shakeable — only the dicts imported by the current page are included in its bundle.
|
|
215
211
|
|
|
216
212
|
## Roadmap
|
|
217
213
|
|
|
@@ -227,4 +223,3 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
|
|
|
227
223
|
There are already a lot of i18n libraries, and the good names are mostly taken. The original plan was *kotoba* (言葉), the Japanese word for "words" — also taken. Claude suggested *kotori* as an alternative, and it stuck.
|
|
228
224
|
|
|
229
225
|
*Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
|
|
230
|
-
|
package/dist/index.cjs
CHANGED
|
@@ -6,10 +6,6 @@ const kotori = (props) => {
|
|
|
6
6
|
let language = props.primaryLanguageTag;
|
|
7
7
|
const setLanguage = (tag) => {
|
|
8
8
|
language = tag;
|
|
9
|
-
snapshot = {
|
|
10
|
-
...snapshot,
|
|
11
|
-
language
|
|
12
|
-
};
|
|
13
9
|
listeners.forEach((listener) => {
|
|
14
10
|
listener();
|
|
15
11
|
});
|
|
@@ -21,16 +17,15 @@ const kotori = (props) => {
|
|
|
21
17
|
};
|
|
22
18
|
};
|
|
23
19
|
const t = (dict, ...args) => {
|
|
24
|
-
let locale = dict().translation[language]
|
|
20
|
+
let locale = dict().translation[language] ?? "unable_to_load_translations";
|
|
25
21
|
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
26
22
|
return locale;
|
|
27
23
|
};
|
|
28
|
-
let snapshot = { language };
|
|
29
24
|
return {
|
|
30
25
|
setLanguage,
|
|
31
26
|
t,
|
|
32
27
|
dict: (translation) => () => ({ translation }),
|
|
33
|
-
useT: () => (0, react.useSyncExternalStore)(subscribe, () =>
|
|
28
|
+
useT: () => (0, react.useSyncExternalStore)(subscribe, () => language, () => language)
|
|
34
29
|
};
|
|
35
30
|
};
|
|
36
31
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -21,9 +21,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
21
21
|
translation: typeof translation;
|
|
22
22
|
[_args]?: ArgsType;
|
|
23
23
|
}>;
|
|
24
|
-
useT: () =>
|
|
25
|
-
language: PrimaryTag | SecondaryTags;
|
|
26
|
-
};
|
|
24
|
+
useT: () => PrimaryTag | SecondaryTags;
|
|
27
25
|
};
|
|
28
26
|
//#endregion
|
|
29
27
|
export { AllTags, SubTags, Tags, kotori };
|
package/dist/index.d.mts
CHANGED
|
@@ -21,9 +21,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
21
21
|
translation: typeof translation;
|
|
22
22
|
[_args]?: ArgsType;
|
|
23
23
|
}>;
|
|
24
|
-
useT: () =>
|
|
25
|
-
language: PrimaryTag | SecondaryTags;
|
|
26
|
-
};
|
|
24
|
+
useT: () => PrimaryTag | SecondaryTags;
|
|
27
25
|
};
|
|
28
26
|
//#endregion
|
|
29
27
|
export { AllTags, SubTags, Tags, kotori };
|
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,6 @@ const kotori = (props) => {
|
|
|
5
5
|
let language = props.primaryLanguageTag;
|
|
6
6
|
const setLanguage = (tag) => {
|
|
7
7
|
language = tag;
|
|
8
|
-
snapshot = {
|
|
9
|
-
...snapshot,
|
|
10
|
-
language
|
|
11
|
-
};
|
|
12
8
|
listeners.forEach((listener) => {
|
|
13
9
|
listener();
|
|
14
10
|
});
|
|
@@ -20,16 +16,15 @@ const kotori = (props) => {
|
|
|
20
16
|
};
|
|
21
17
|
};
|
|
22
18
|
const t = (dict, ...args) => {
|
|
23
|
-
let locale = dict().translation[language]
|
|
19
|
+
let locale = dict().translation[language] ?? "unable_to_load_translations";
|
|
24
20
|
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
25
21
|
return locale;
|
|
26
22
|
};
|
|
27
|
-
let snapshot = { language };
|
|
28
23
|
return {
|
|
29
24
|
setLanguage,
|
|
30
25
|
t,
|
|
31
26
|
dict: (translation) => () => ({ translation }),
|
|
32
|
-
useT: () => useSyncExternalStore(subscribe, () =>
|
|
27
|
+
useT: () => useSyncExternalStore(subscribe, () => language, () => language)
|
|
33
28
|
};
|
|
34
29
|
};
|
|
35
30
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kotori",
|
|
3
3
|
"description": "Strongly-typed and composable internationalization library for React",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.1",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"setup": "rm -rf node_modules && npm i && git init && husky",
|
|
7
7
|
"prepublishOnly": "npm i && npx tsc && npm run build",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"lint-staged": {
|
|
18
18
|
"*": [
|
|
19
|
-
"npm run lint"
|
|
19
|
+
"npm run lint",
|
|
20
|
+
"npm test"
|
|
20
21
|
]
|
|
21
22
|
},
|
|
22
23
|
"type": "module",
|
|
@@ -43,7 +44,6 @@
|
|
|
43
44
|
],
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@biomejs/biome": "^2.4.12",
|
|
46
|
-
"@types/node": "^25.6.0",
|
|
47
47
|
"@types/react": "^19.2.14",
|
|
48
48
|
"@types/react-dom": "^19.2.3",
|
|
49
49
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
"react": "^19.2.5",
|
|
55
55
|
"react-dom": "^19.2.5",
|
|
56
56
|
"tsdown": "^0.21.10",
|
|
57
|
-
"tsx": "^4.21.0",
|
|
58
57
|
"typescript": "^6.0.3",
|
|
59
58
|
"vitest": "^4.1.5"
|
|
60
59
|
},
|
|
@@ -68,6 +67,6 @@
|
|
|
68
67
|
"author": "tylim88",
|
|
69
68
|
"license": "MIT",
|
|
70
69
|
"peerDependencies": {
|
|
71
|
-
"react": ">=
|
|
70
|
+
"react": ">=18"
|
|
72
71
|
}
|
|
73
72
|
}
|