kotori 5.0.11 → 6.0.0
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 +56 -42
- package/dist/index.cjs +13 -7
- package/dist/index.d.cts +16 -10
- package/dist/index.d.mts +16 -10
- package/dist/index.mjs +13 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const { d, useT } = kotori({
|
|
19
|
+
primary: 'en',
|
|
20
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
// ❌ TypeScript error: missing japanese translation
|
|
24
|
-
const intro =
|
|
24
|
+
const intro = d({
|
|
25
25
|
// ⭐ base string drives the type contract
|
|
26
26
|
en: 'Hello {{name}}, is it {{time}} now?',
|
|
27
27
|
|
|
@@ -34,18 +34,21 @@ const intro = dict({
|
|
|
34
34
|
// optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
|
|
35
35
|
})<{name: string; time: `${number}:${number}`}>
|
|
36
36
|
|
|
37
|
+
const Component = () => {
|
|
38
|
+
const { t } = useT()
|
|
37
39
|
|
|
38
|
-
// ✅ Works
|
|
39
|
-
t(intro, { name: 'John', time: '12:25' })
|
|
40
|
+
// ✅ Works
|
|
41
|
+
t(intro, { name: 'John', time: '12:25' })
|
|
40
42
|
|
|
41
|
-
// ❌ TypeScript error: missing { name }
|
|
42
|
-
t(intro, { time: '12:25' })
|
|
43
|
+
// ❌ TypeScript error: missing { name }
|
|
44
|
+
t(intro, { time: '12:25' })
|
|
43
45
|
|
|
44
|
-
// ❌ TypeScript error: unknown key 'nama'
|
|
45
|
-
t(intro, { nama: 'John', time: '12:25' })
|
|
46
|
+
// ❌ TypeScript error: unknown key 'nama'
|
|
47
|
+
t(intro, { nama: 'John', time: '12:25' })
|
|
46
48
|
|
|
47
|
-
// ❌ TypeScript error: invalid format for 'time'
|
|
48
|
-
t(intro, { name: 'John', time: '12-00' })
|
|
49
|
+
// ❌ TypeScript error: invalid format for 'time'
|
|
50
|
+
t(intro, { name: 'John', time: '12-00' })
|
|
51
|
+
}
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
- No codegen
|
|
@@ -73,20 +76,20 @@ npm i kotori
|
|
|
73
76
|
```ts
|
|
74
77
|
import { kotori } from 'kotori'
|
|
75
78
|
|
|
76
|
-
export const { useT,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
export const { useT, d, setLanguage } = kotori({
|
|
80
|
+
primary: 'en',
|
|
81
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
79
82
|
})
|
|
80
83
|
|
|
81
84
|
// you can define your dicts in the same file or separate them by component, it's up to you
|
|
82
|
-
export const intro =
|
|
85
|
+
export const intro = d({
|
|
83
86
|
en: 'my name is {{name}}, I am {{age}} years old.',
|
|
84
87
|
zh: '我叫{{name}},我今年{{age}}岁了。',
|
|
85
88
|
ja: '私の名前は{{name}}で、{{age}}歳です。',
|
|
86
89
|
ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
|
|
87
90
|
})
|
|
88
91
|
|
|
89
|
-
export const time =
|
|
92
|
+
export const time = d({
|
|
90
93
|
en: 'time {{time}}',
|
|
91
94
|
zh: '时间 {{time}}',
|
|
92
95
|
ja: '時間 {{time}}',
|
|
@@ -98,11 +101,11 @@ export const time = dict({
|
|
|
98
101
|
### page1.tsx
|
|
99
102
|
|
|
100
103
|
```tsx
|
|
101
|
-
import { useT,
|
|
104
|
+
import { useT, setLanguage, intro, time } from './locales'
|
|
102
105
|
|
|
103
106
|
export const Page1 = () => {
|
|
104
107
|
|
|
105
|
-
const language
|
|
108
|
+
const { language, t } = useT()
|
|
106
109
|
|
|
107
110
|
return (
|
|
108
111
|
<>
|
|
@@ -126,17 +129,17 @@ export const Page1 = () => {
|
|
|
126
129
|
### page2.tsx
|
|
127
130
|
|
|
128
131
|
```tsx
|
|
129
|
-
import { useT,
|
|
132
|
+
import { useT, d } from './locales'
|
|
130
133
|
|
|
131
134
|
// you can also define dicts in the same file as your components, it's up to you
|
|
132
|
-
const weather =
|
|
135
|
+
const weather = d({
|
|
133
136
|
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
134
137
|
zh: '{{city}}的天气湿度为{{humidity}}%。',
|
|
135
138
|
ja: '{{city}}の湿度は{{humidity}}%です。',
|
|
136
139
|
ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
|
|
137
140
|
})<{ city: string; humidity: number }>
|
|
138
141
|
|
|
139
|
-
const lastLogin =
|
|
142
|
+
const lastLogin = d({
|
|
140
143
|
en: 'Last login: {{date}} at {{time}}',
|
|
141
144
|
zh: '上次登录:{{date}} {{time}}',
|
|
142
145
|
ja: '最終ログイン:{{date}} {{time}}',
|
|
@@ -145,7 +148,7 @@ const lastLogin = dict({
|
|
|
145
148
|
|
|
146
149
|
export const Page2 = () => {
|
|
147
150
|
|
|
148
|
-
useT()
|
|
151
|
+
const { t } = useT()
|
|
149
152
|
|
|
150
153
|
return (
|
|
151
154
|
<>
|
|
@@ -167,31 +170,31 @@ Creates a scoped i18n instance.
|
|
|
167
170
|
```ts
|
|
168
171
|
import { kotori } from 'kotori'
|
|
169
172
|
|
|
170
|
-
export const { useT,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
export const { useT, d, setLanguage } = kotori({
|
|
174
|
+
primary: 'en',
|
|
175
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
173
176
|
})
|
|
174
177
|
```
|
|
175
178
|
|
|
176
179
|
| option | type | description |
|
|
177
180
|
| --- | --- | --- |
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
181
|
+
| `primary` | `BCP47LanguageTag` | The source language. Drives variable inference. |
|
|
182
|
+
| `secondaries` | `BCP47LanguageTag[]` | Additional supported languages. |
|
|
180
183
|
|
|
181
|
-
Returns `{
|
|
184
|
+
Returns `{ d, useT, setLanguage, r }`.
|
|
182
185
|
|
|
183
|
-
### `
|
|
186
|
+
### `d(translations)<argsType?>`
|
|
184
187
|
|
|
185
|
-
Defines a translation unit. Takes one string per language.
|
|
188
|
+
Defines a translation unit. Takes one string per language. Return `dictionary` object.
|
|
186
189
|
|
|
187
190
|
```ts
|
|
188
|
-
const time =
|
|
191
|
+
const time = d({ en: '{{hour}}:{{minute}}' })
|
|
189
192
|
```
|
|
190
193
|
|
|
191
194
|
By default, variables are typed as `string | number`. Pass a generic to narrow them:
|
|
192
195
|
|
|
193
196
|
```ts
|
|
194
|
-
const time =
|
|
197
|
+
const time = d({ en: '{{hour}}:{{minute}}' })<{
|
|
195
198
|
hour: number
|
|
196
199
|
minute: number
|
|
197
200
|
}>
|
|
@@ -205,20 +208,32 @@ Updates the current language and rerenders all active `useT` consumers across al
|
|
|
205
208
|
setLanguage('zh')
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
### `
|
|
211
|
+
### `r(dictionary, args?)`
|
|
212
|
+
|
|
213
|
+
Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
|
|
209
214
|
|
|
210
|
-
|
|
215
|
+
⚠️ Do not call this inside React components (it will break React Compiler optimization rules). Use this exclusively in raw JS/TS environments like router guards, API interceptors, or state utilities.
|
|
211
216
|
|
|
212
217
|
```tsx
|
|
213
|
-
|
|
218
|
+
r(intro, { name: 'John', age: 30 })}
|
|
214
219
|
```
|
|
215
220
|
|
|
216
221
|
### `useT()`
|
|
217
222
|
|
|
218
|
-
React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called.
|
|
223
|
+
React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called. Returns `{ t, language }`.
|
|
219
224
|
|
|
220
225
|
```ts
|
|
221
|
-
const language = useT()
|
|
226
|
+
const { t, language } = useT()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `t(dictionary, args?)`
|
|
230
|
+
|
|
231
|
+
Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
|
|
232
|
+
|
|
233
|
+
React version of `r(dictionary, args?)`, works with React Compiler.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<p>{t(intro, { name: 'John', age: 30 })}</p>
|
|
222
237
|
```
|
|
223
238
|
|
|
224
239
|
## Language Tags
|
|
@@ -227,8 +242,8 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
|
|
|
227
242
|
|
|
228
243
|
## Tips
|
|
229
244
|
|
|
230
|
-
- 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.
|
|
231
|
-
- 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.
|
|
245
|
+
- If you plan to add new languages frequently, consider colocating all your dicts in a single file or multiple files in one folder. It is easier to copy the entire file and hand it to an AI to translate.
|
|
246
|
+
- 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.
|
|
232
247
|
- Both approaches are tree-shakeable — only the dicts imported by the current page are included in its bundle.
|
|
233
248
|
|
|
234
249
|
## Roadmap
|
|
@@ -238,10 +253,9 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
|
|
|
238
253
|
- Pluralization support
|
|
239
254
|
- Gender support
|
|
240
255
|
- Value formatting (date, number, currency)
|
|
241
|
-
- Support for non-React frameworks (Vue, Svelte, Angular, etc.)
|
|
242
256
|
|
|
243
257
|
## Trivial
|
|
244
258
|
|
|
245
|
-
There are already
|
|
259
|
+
There are already many 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.
|
|
246
260
|
|
|
247
261
|
*Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
|
package/dist/index.cjs
CHANGED
|
@@ -3,9 +3,16 @@ let react = require("react");
|
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const kotori = (props) => {
|
|
5
5
|
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
language
|
|
6
|
+
const t = (dictionary, ...args) => (dictionary().d[snapshot.language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
|
|
7
|
+
let snapshot = {
|
|
8
|
+
language: props.primary,
|
|
9
|
+
t
|
|
10
|
+
};
|
|
11
|
+
const setLanguage = (language) => {
|
|
12
|
+
snapshot = {
|
|
13
|
+
language,
|
|
14
|
+
t: (...args) => t(...args)
|
|
15
|
+
};
|
|
9
16
|
listeners.forEach((listener) => {
|
|
10
17
|
listener();
|
|
11
18
|
});
|
|
@@ -16,12 +23,11 @@ const kotori = (props) => {
|
|
|
16
23
|
listeners.delete(listener);
|
|
17
24
|
};
|
|
18
25
|
};
|
|
19
|
-
const t = (dict, ...args) => (dict().translation[language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
|
|
20
26
|
return {
|
|
21
27
|
setLanguage,
|
|
22
|
-
t,
|
|
23
|
-
|
|
24
|
-
useT: () => (0, react.useSyncExternalStore)(subscribe, () =>
|
|
28
|
+
r: t,
|
|
29
|
+
d: (dictionary) => () => ({ d: dictionary }),
|
|
30
|
+
useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
|
|
25
31
|
};
|
|
26
32
|
};
|
|
27
33
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -8,20 +8,26 @@ type AllTags = Tags | SubTags;
|
|
|
8
8
|
type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
|
|
9
9
|
type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
|
|
10
10
|
declare const _args: unique symbol;
|
|
11
|
-
declare const kotori: <const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
declare const kotori: <const Primary extends AllTags, const Secondary extends Exclude<AllTags, Primary>>(props: {
|
|
12
|
+
primary: Primary;
|
|
13
|
+
secondaries: Secondary[];
|
|
14
14
|
}) => {
|
|
15
|
-
setLanguage: (
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
setLanguage: (language: Primary | Secondary) => void;
|
|
16
|
+
r: <Dictionary extends () => Readonly<{
|
|
17
|
+
d: Record<Primary | Secondary, string>;
|
|
18
18
|
[_args]?: Record<string, string | number>;
|
|
19
|
-
}>>(
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
}>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
|
|
20
|
+
d: <const PrimaryString extends string, const SecondaryObject extends { [Key in Secondary]: ExtractVariables<PrimaryString> extends infer PrimaryVariables ? ExtractVariables<SecondaryObject[Key] & string> extends infer SecondaryVariables ? PrimaryVariables[] extends SecondaryVariables[] ? SecondaryVariables[] extends PrimaryVariables[] ? SecondaryObject[Key] : "variables not match!" : "variables not match!!" : never : never }>(dictionary: { [Key in Primary]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
|
|
21
|
+
d: typeof dictionary;
|
|
22
22
|
[_args]?: ArgsType;
|
|
23
23
|
}>;
|
|
24
|
-
useT: () =>
|
|
24
|
+
useT: () => {
|
|
25
|
+
language: Primary | Secondary;
|
|
26
|
+
t: <Dictionary extends () => Readonly<{
|
|
27
|
+
d: Record<Primary | Secondary, string>;
|
|
28
|
+
[_args]?: Record<string, string | number>;
|
|
29
|
+
}>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
|
|
30
|
+
};
|
|
25
31
|
};
|
|
26
32
|
//#endregion
|
|
27
33
|
export { AllTags, SubTags, Tags, kotori };
|
package/dist/index.d.mts
CHANGED
|
@@ -8,20 +8,26 @@ type AllTags = Tags | SubTags;
|
|
|
8
8
|
type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
|
|
9
9
|
type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
|
|
10
10
|
declare const _args: unique symbol;
|
|
11
|
-
declare const kotori: <const
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
declare const kotori: <const Primary extends AllTags, const Secondary extends Exclude<AllTags, Primary>>(props: {
|
|
12
|
+
primary: Primary;
|
|
13
|
+
secondaries: Secondary[];
|
|
14
14
|
}) => {
|
|
15
|
-
setLanguage: (
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
setLanguage: (language: Primary | Secondary) => void;
|
|
16
|
+
r: <Dictionary extends () => Readonly<{
|
|
17
|
+
d: Record<Primary | Secondary, string>;
|
|
18
18
|
[_args]?: Record<string, string | number>;
|
|
19
|
-
}>>(
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
}>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
|
|
20
|
+
d: <const PrimaryString extends string, const SecondaryObject extends { [Key in Secondary]: ExtractVariables<PrimaryString> extends infer PrimaryVariables ? ExtractVariables<SecondaryObject[Key] & string> extends infer SecondaryVariables ? PrimaryVariables[] extends SecondaryVariables[] ? SecondaryVariables[] extends PrimaryVariables[] ? SecondaryObject[Key] : "variables not match!" : "variables not match!!" : never : never }>(dictionary: { [Key in Primary]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
|
|
21
|
+
d: typeof dictionary;
|
|
22
22
|
[_args]?: ArgsType;
|
|
23
23
|
}>;
|
|
24
|
-
useT: () =>
|
|
24
|
+
useT: () => {
|
|
25
|
+
language: Primary | Secondary;
|
|
26
|
+
t: <Dictionary extends () => Readonly<{
|
|
27
|
+
d: Record<Primary | Secondary, string>;
|
|
28
|
+
[_args]?: Record<string, string | number>;
|
|
29
|
+
}>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
|
|
30
|
+
};
|
|
25
31
|
};
|
|
26
32
|
//#endregion
|
|
27
33
|
export { AllTags, SubTags, Tags, kotori };
|
package/dist/index.mjs
CHANGED
|
@@ -2,9 +2,16 @@ import { useSyncExternalStore } from "react";
|
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
const kotori = (props) => {
|
|
4
4
|
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
language
|
|
5
|
+
const t = (dictionary, ...args) => (dictionary().d[snapshot.language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
|
|
6
|
+
let snapshot = {
|
|
7
|
+
language: props.primary,
|
|
8
|
+
t
|
|
9
|
+
};
|
|
10
|
+
const setLanguage = (language) => {
|
|
11
|
+
snapshot = {
|
|
12
|
+
language,
|
|
13
|
+
t: (...args) => t(...args)
|
|
14
|
+
};
|
|
8
15
|
listeners.forEach((listener) => {
|
|
9
16
|
listener();
|
|
10
17
|
});
|
|
@@ -15,12 +22,11 @@ const kotori = (props) => {
|
|
|
15
22
|
listeners.delete(listener);
|
|
16
23
|
};
|
|
17
24
|
};
|
|
18
|
-
const t = (dict, ...args) => (dict().translation[language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
|
|
19
25
|
return {
|
|
20
26
|
setLanguage,
|
|
21
|
-
t,
|
|
22
|
-
|
|
23
|
-
useT: () => useSyncExternalStore(subscribe, () =>
|
|
27
|
+
r: t,
|
|
28
|
+
d: (dictionary) => () => ({ d: dictionary }),
|
|
29
|
+
useT: () => useSyncExternalStore(subscribe, () => snapshot, () => snapshot)
|
|
24
30
|
};
|
|
25
31
|
};
|
|
26
32
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kotori",
|
|
3
3
|
"description": "0.28kB Strongly-typed and tree-shakeable internationalization library for React",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0",
|
|
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",
|