kotori 2.0.2 → 3.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 +41 -31
- package/dist/index.cjs +16 -23
- package/dist/index.d.cts +7 -9
- package/dist/index.d.mts +7 -9
- package/dist/index.mjs +16 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,19 +14,26 @@ const { dict } = kotori({
|
|
|
14
14
|
const intro = dict({
|
|
15
15
|
// ⭐ base string drives the type contract
|
|
16
16
|
en: 'Hello {{name}}, is it {{time}} now?',
|
|
17
|
+
|
|
17
18
|
// ❌ TypeScript error: missing key 'name'
|
|
18
19
|
zh: '你好,现在是 {{time}} 吗?',
|
|
20
|
+
|
|
19
21
|
// ❌ TypeScript error: unknown key 'nam'
|
|
20
22
|
ms: 'Hai {{nam}}, adakah pukul {{time}} sekarang?'
|
|
23
|
+
|
|
21
24
|
// optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
|
|
22
25
|
})<{name: string; time: `${number}:${number}`}>
|
|
23
26
|
|
|
27
|
+
|
|
24
28
|
// ✅ Works
|
|
25
29
|
t('intro', { name: 'John', time: '12:25' })
|
|
30
|
+
|
|
26
31
|
// ❌ TypeScript error: missing { name }
|
|
27
32
|
t('intro', { time: '12:25' })
|
|
33
|
+
|
|
28
34
|
// ❌ TypeScript error: unknown key 'nama'
|
|
29
35
|
t('intro', { nama: 'John', time: '12:25' })
|
|
36
|
+
|
|
30
37
|
// ❌ TypeScript error: invalid format for 'time'
|
|
31
38
|
t('intro', { name: 'John', time: '12-00' })
|
|
32
39
|
```
|
|
@@ -35,11 +42,11 @@ t('intro', { name: 'John', time: '12-00' })
|
|
|
35
42
|
- No JSON
|
|
36
43
|
- No dependencies
|
|
37
44
|
- No build step
|
|
38
|
-
- 0.
|
|
45
|
+
- 0.33kb gzipped
|
|
39
46
|
- Modular and tree-shakeable
|
|
40
47
|
- Language change in one page rerenders all pages
|
|
41
|
-
-
|
|
42
|
-
-
|
|
48
|
+
- Variables typed and inferred from string literals — no more string typos
|
|
49
|
+
- maximum type safety with minimum types
|
|
43
50
|
|
|
44
51
|
Demo: <https://stackblitz.com/edit/vitejs-vite-nyxwmhre?file=src%2FApp.tsx>
|
|
45
52
|
|
|
@@ -54,9 +61,9 @@ npm i kotori
|
|
|
54
61
|
**utils.ts**
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
|
-
import { kotori } from '
|
|
64
|
+
import { kotori } from 'kotori'
|
|
58
65
|
|
|
59
|
-
export const {
|
|
66
|
+
export const { useT, dict, setLanguage } = kotori({
|
|
60
67
|
primaryLanguageTag: 'en',
|
|
61
68
|
secondaryLanguageTags: ['zh', 'ja', 'ms'],
|
|
62
69
|
})
|
|
@@ -65,7 +72,7 @@ export const { createT, dict, setLanguage } = kotori({
|
|
|
65
72
|
**page1.tsx**
|
|
66
73
|
|
|
67
74
|
```tsx
|
|
68
|
-
import {
|
|
75
|
+
import { useT, dict } from './utils'
|
|
69
76
|
|
|
70
77
|
const intro = dict({
|
|
71
78
|
en: 'my name is {{name}}, I am {{age}} years old.',
|
|
@@ -82,11 +89,6 @@ const time = dict({
|
|
|
82
89
|
// optional: type your arguments, by default it's `Record<'time', string | number>` in this example
|
|
83
90
|
})<{ time: `${number}:${number}:${number}` }>
|
|
84
91
|
|
|
85
|
-
const { useT } = createT({
|
|
86
|
-
intro,
|
|
87
|
-
time,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
92
|
export const Page1 = () => {
|
|
91
93
|
const { t, language, setLanguage } = useT()
|
|
92
94
|
return (
|
|
@@ -111,7 +113,7 @@ export const Page1 = () => {
|
|
|
111
113
|
**page2.tsx**
|
|
112
114
|
|
|
113
115
|
```tsx
|
|
114
|
-
import {
|
|
116
|
+
import { useT, dict } from './utils'
|
|
115
117
|
|
|
116
118
|
const weather = dict({
|
|
117
119
|
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
@@ -134,12 +136,6 @@ const lastLogin = dict({
|
|
|
134
136
|
ms: 'Log masuk terakhir: {{date}} pada {{time}}',
|
|
135
137
|
})<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
|
|
136
138
|
|
|
137
|
-
const { useT } = createT({
|
|
138
|
-
weather,
|
|
139
|
-
score,
|
|
140
|
-
lastLogin,
|
|
141
|
-
})
|
|
142
|
-
|
|
143
139
|
export const Page2 = () => {
|
|
144
140
|
const { t, language, setLanguage } = useT()
|
|
145
141
|
return (
|
|
@@ -154,9 +150,9 @@ export const Page2 = () => {
|
|
|
154
150
|
<option value="ja">Japanese</option>
|
|
155
151
|
<option value="ms">Malay</option>
|
|
156
152
|
</select>
|
|
157
|
-
<p>{t(
|
|
158
|
-
<p>{t(
|
|
159
|
-
<p>{t(
|
|
153
|
+
<p>{t(weather, { city: 'Kuala Lumpur', humidity: 80 })}</p>
|
|
154
|
+
<p>{t(score, { score: 87, total: 100 })}</p>
|
|
155
|
+
<p>{t(lastLogin, { date: '2024-04-24', time: '09:30' })}</p>
|
|
160
156
|
</>
|
|
161
157
|
)
|
|
162
158
|
}
|
|
@@ -164,22 +160,31 @@ export const Page2 = () => {
|
|
|
164
160
|
|
|
165
161
|
## API
|
|
166
162
|
|
|
167
|
-

|
|
168
164
|
|
|
169
165
|
### `kotori(options)`
|
|
170
166
|
|
|
171
167
|
Creates a scoped i18n instance.
|
|
172
168
|
|
|
169
|
+
```ts
|
|
170
|
+
import { kotori } from 'kotori'
|
|
171
|
+
|
|
172
|
+
export const { useT, dict, setLanguage } = kotori({
|
|
173
|
+
primaryLanguageTag: 'en',
|
|
174
|
+
secondaryLanguageTags: ['zh', 'ja', 'ms'],
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
173
178
|
| option | type | description |
|
|
174
179
|
| --- | --- | --- |
|
|
175
|
-
| `primaryLanguageTag` | `
|
|
176
|
-
| `secondaryLanguageTags` | `
|
|
180
|
+
| `primaryLanguageTag` | `BCP47LanguageTag` | The source language. Drives variable inference. |
|
|
181
|
+
| `secondaryLanguageTags` | `BCP47LanguageTag[]` | Additional supported languages. |
|
|
177
182
|
|
|
178
|
-
Returns `{ dict,
|
|
183
|
+
Returns `{ dict, useT, setLanguage }`.
|
|
179
184
|
|
|
180
185
|
### `dict(translations)<argsType?>`
|
|
181
186
|
|
|
182
|
-
Defines a translation unit. Takes one string per language
|
|
187
|
+
Defines a translation unit. Takes one string per language.
|
|
183
188
|
|
|
184
189
|
```ts
|
|
185
190
|
const time = dict({ en: '{{hour}}:{{minute}}' })
|
|
@@ -194,10 +199,6 @@ const time = dict({ en: '{{hour}}:{{minute}}' })<{
|
|
|
194
199
|
}>
|
|
195
200
|
```
|
|
196
201
|
|
|
197
|
-
### `createT(dicts)`
|
|
198
|
-
|
|
199
|
-
Registers a set of dicts and returns `{ useT }`. Call once per page or feature module.
|
|
200
|
-
|
|
201
202
|
### `setLanguage(tag)`
|
|
202
203
|
|
|
203
204
|
Updates the current language and rerenders all active `useT` consumers across all pages. Available directly on the `kotori` instance — useful for calling outside of React (route guards, axios interceptors, etc.).
|
|
@@ -208,7 +209,7 @@ React hook. Returns `{ t, language, setLanguage }`.
|
|
|
208
209
|
|
|
209
210
|
| return | type | description |
|
|
210
211
|
| --- | --- | --- |
|
|
211
|
-
| `t(
|
|
212
|
+
| `t(dict, args?)` | `string` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. |
|
|
212
213
|
| `language` | `primaryLanguageTag` \| `secondaryLanguageTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
|
|
213
214
|
| `setLanguage(tag)` | `void` | Updates the language and rerenders all active `useT` consumers. |
|
|
214
215
|
|
|
@@ -216,8 +217,17 @@ React hook. Returns `{ t, language, setLanguage }`.
|
|
|
216
217
|
|
|
217
218
|
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-Hans`) are accepted and validated at the type level.
|
|
218
219
|
|
|
220
|
+
## Roadmap
|
|
221
|
+
|
|
222
|
+
- Auto detect locale from browser settings
|
|
223
|
+
- Auto persist language selection to localStorage
|
|
224
|
+
- Pluralization support
|
|
225
|
+
- Gender support
|
|
226
|
+
- Value formatting (date, number, currency)
|
|
227
|
+
|
|
219
228
|
## Trivial
|
|
220
229
|
|
|
221
230
|
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.
|
|
222
231
|
|
|
223
232
|
*Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
|
|
233
|
+
|
package/dist/index.cjs
CHANGED
|
@@ -3,16 +3,13 @@ let react = require("react");
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const kotori = (props) => {
|
|
5
5
|
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
-
let
|
|
7
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
6
|
+
let language = props.primaryLanguageTag;
|
|
8
7
|
const setLanguage = (tag) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
});
|
|
8
|
+
language = tag;
|
|
9
|
+
snapshot = {
|
|
10
|
+
...snapshot,
|
|
11
|
+
language
|
|
12
|
+
};
|
|
16
13
|
listeners.forEach((listener) => {
|
|
17
14
|
listener();
|
|
18
15
|
});
|
|
@@ -23,23 +20,19 @@ const kotori = (props) => {
|
|
|
23
20
|
listeners.delete(listener);
|
|
24
21
|
};
|
|
25
22
|
};
|
|
23
|
+
let snapshot = {
|
|
24
|
+
language,
|
|
25
|
+
setLanguage,
|
|
26
|
+
t: (dict, ...args) => {
|
|
27
|
+
let locale = dict().translation[language] || "unable_to_load_translations";
|
|
28
|
+
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
29
|
+
return locale;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
26
32
|
return {
|
|
27
33
|
setLanguage,
|
|
28
34
|
dict: (translation) => () => ({ translation }),
|
|
29
|
-
|
|
30
|
-
const s = Symbol();
|
|
31
|
-
const snapshot = {
|
|
32
|
-
language: languageTag,
|
|
33
|
-
setLanguage,
|
|
34
|
-
t: (key, ...args) => {
|
|
35
|
-
let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
|
|
36
|
-
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
37
|
-
return locale;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
snapshots.set(s, snapshot);
|
|
41
|
-
return { useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshots.get(s), () => snapshot) };
|
|
42
|
-
}
|
|
35
|
+
useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
|
|
43
36
|
};
|
|
44
37
|
};
|
|
45
38
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -17,15 +17,13 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
17
17
|
translation: typeof translation;
|
|
18
18
|
[_args]?: ArgsType;
|
|
19
19
|
}>;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => string;
|
|
28
|
-
};
|
|
20
|
+
useT: () => {
|
|
21
|
+
language: PrimaryTag | SecondaryTags;
|
|
22
|
+
setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
|
|
23
|
+
t: <DictCallback extends () => Readonly<{
|
|
24
|
+
translation: Record<PrimaryTag | SecondaryTags, string>;
|
|
25
|
+
[_args]?: Record<string, string | number>;
|
|
26
|
+
}>>(dict: DictCallback, ...args: keyof NonNullable<ReturnType<DictCallback>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallback>[typeof _args]>]) => string;
|
|
29
27
|
};
|
|
30
28
|
};
|
|
31
29
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -17,15 +17,13 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
17
17
|
translation: typeof translation;
|
|
18
18
|
[_args]?: ArgsType;
|
|
19
19
|
}>;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => string;
|
|
28
|
-
};
|
|
20
|
+
useT: () => {
|
|
21
|
+
language: PrimaryTag | SecondaryTags;
|
|
22
|
+
setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
|
|
23
|
+
t: <DictCallback extends () => Readonly<{
|
|
24
|
+
translation: Record<PrimaryTag | SecondaryTags, string>;
|
|
25
|
+
[_args]?: Record<string, string | number>;
|
|
26
|
+
}>>(dict: DictCallback, ...args: keyof NonNullable<ReturnType<DictCallback>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallback>[typeof _args]>]) => string;
|
|
29
27
|
};
|
|
30
28
|
};
|
|
31
29
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -2,16 +2,13 @@ import { useSyncExternalStore } from "react";
|
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
const kotori = (props) => {
|
|
4
4
|
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
-
let
|
|
6
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
5
|
+
let language = props.primaryLanguageTag;
|
|
7
6
|
const setLanguage = (tag) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
});
|
|
7
|
+
language = tag;
|
|
8
|
+
snapshot = {
|
|
9
|
+
...snapshot,
|
|
10
|
+
language
|
|
11
|
+
};
|
|
15
12
|
listeners.forEach((listener) => {
|
|
16
13
|
listener();
|
|
17
14
|
});
|
|
@@ -22,23 +19,19 @@ const kotori = (props) => {
|
|
|
22
19
|
listeners.delete(listener);
|
|
23
20
|
};
|
|
24
21
|
};
|
|
22
|
+
let snapshot = {
|
|
23
|
+
language,
|
|
24
|
+
setLanguage,
|
|
25
|
+
t: (dict, ...args) => {
|
|
26
|
+
let locale = dict().translation[language] || "unable_to_load_translations";
|
|
27
|
+
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
28
|
+
return locale;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
25
31
|
return {
|
|
26
32
|
setLanguage,
|
|
27
33
|
dict: (translation) => () => ({ translation }),
|
|
28
|
-
|
|
29
|
-
const s = Symbol();
|
|
30
|
-
const snapshot = {
|
|
31
|
-
language: languageTag,
|
|
32
|
-
setLanguage,
|
|
33
|
-
t: (key, ...args) => {
|
|
34
|
-
let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
|
|
35
|
-
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
36
|
-
return locale;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
snapshots.set(s, snapshot);
|
|
40
|
-
return { useT: () => useSyncExternalStore(subscribe, () => snapshots.get(s), () => snapshot) };
|
|
41
|
-
}
|
|
34
|
+
useT: () => useSyncExternalStore(subscribe, () => snapshot, () => snapshot)
|
|
42
35
|
};
|
|
43
36
|
};
|
|
44
37
|
//#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": "3.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",
|