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

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