kotori 5.0.12 → 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 +54 -44
- 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
|
@@ -14,18 +14,14 @@
|
|
|
14
14
|
🕊️ Kotori is a zero-config, fully type-safe, and modular internationalization library for React that compiles down to just 0.28kB. No JSON, no external CLI tools, no codegen—just live type inference from your strings.
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
## Note
|
|
18
|
-
|
|
19
|
-
⚠️ Doesn't work with React Compiler!!
|
|
20
|
-
|
|
21
17
|
```ts
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
const { d, useT } = kotori({
|
|
19
|
+
primary: 'en',
|
|
20
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
25
21
|
})
|
|
26
22
|
|
|
27
23
|
// ❌ TypeScript error: missing japanese translation
|
|
28
|
-
const intro =
|
|
24
|
+
const intro = d({
|
|
29
25
|
// ⭐ base string drives the type contract
|
|
30
26
|
en: 'Hello {{name}}, is it {{time}} now?',
|
|
31
27
|
|
|
@@ -38,18 +34,21 @@ const intro = dict({
|
|
|
38
34
|
// optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
|
|
39
35
|
})<{name: string; time: `${number}:${number}`}>
|
|
40
36
|
|
|
37
|
+
const Component = () => {
|
|
38
|
+
const { t } = useT()
|
|
41
39
|
|
|
42
|
-
// ✅ Works
|
|
43
|
-
t(intro, { name: 'John', time: '12:25' })
|
|
40
|
+
// ✅ Works
|
|
41
|
+
t(intro, { name: 'John', time: '12:25' })
|
|
44
42
|
|
|
45
|
-
// ❌ TypeScript error: missing { name }
|
|
46
|
-
t(intro, { time: '12:25' })
|
|
43
|
+
// ❌ TypeScript error: missing { name }
|
|
44
|
+
t(intro, { time: '12:25' })
|
|
47
45
|
|
|
48
|
-
// ❌ TypeScript error: unknown key 'nama'
|
|
49
|
-
t(intro, { nama: 'John', time: '12:25' })
|
|
46
|
+
// ❌ TypeScript error: unknown key 'nama'
|
|
47
|
+
t(intro, { nama: 'John', time: '12:25' })
|
|
50
48
|
|
|
51
|
-
// ❌ TypeScript error: invalid format for 'time'
|
|
52
|
-
t(intro, { name: 'John', time: '12-00' })
|
|
49
|
+
// ❌ TypeScript error: invalid format for 'time'
|
|
50
|
+
t(intro, { name: 'John', time: '12-00' })
|
|
51
|
+
}
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
- No codegen
|
|
@@ -77,20 +76,20 @@ npm i kotori
|
|
|
77
76
|
```ts
|
|
78
77
|
import { kotori } from 'kotori'
|
|
79
78
|
|
|
80
|
-
export const { useT,
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
export const { useT, d, setLanguage } = kotori({
|
|
80
|
+
primary: 'en',
|
|
81
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
83
82
|
})
|
|
84
83
|
|
|
85
84
|
// you can define your dicts in the same file or separate them by component, it's up to you
|
|
86
|
-
export const intro =
|
|
85
|
+
export const intro = d({
|
|
87
86
|
en: 'my name is {{name}}, I am {{age}} years old.',
|
|
88
87
|
zh: '我叫{{name}},我今年{{age}}岁了。',
|
|
89
88
|
ja: '私の名前は{{name}}で、{{age}}歳です。',
|
|
90
89
|
ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
|
|
91
90
|
})
|
|
92
91
|
|
|
93
|
-
export const time =
|
|
92
|
+
export const time = d({
|
|
94
93
|
en: 'time {{time}}',
|
|
95
94
|
zh: '时间 {{time}}',
|
|
96
95
|
ja: '時間 {{time}}',
|
|
@@ -102,11 +101,11 @@ export const time = dict({
|
|
|
102
101
|
### page1.tsx
|
|
103
102
|
|
|
104
103
|
```tsx
|
|
105
|
-
import { useT,
|
|
104
|
+
import { useT, setLanguage, intro, time } from './locales'
|
|
106
105
|
|
|
107
106
|
export const Page1 = () => {
|
|
108
107
|
|
|
109
|
-
const language
|
|
108
|
+
const { language, t } = useT()
|
|
110
109
|
|
|
111
110
|
return (
|
|
112
111
|
<>
|
|
@@ -130,17 +129,17 @@ export const Page1 = () => {
|
|
|
130
129
|
### page2.tsx
|
|
131
130
|
|
|
132
131
|
```tsx
|
|
133
|
-
import { useT,
|
|
132
|
+
import { useT, d } from './locales'
|
|
134
133
|
|
|
135
134
|
// you can also define dicts in the same file as your components, it's up to you
|
|
136
|
-
const weather =
|
|
135
|
+
const weather = d({
|
|
137
136
|
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
138
137
|
zh: '{{city}}的天气湿度为{{humidity}}%。',
|
|
139
138
|
ja: '{{city}}の湿度は{{humidity}}%です。',
|
|
140
139
|
ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
|
|
141
140
|
})<{ city: string; humidity: number }>
|
|
142
141
|
|
|
143
|
-
const lastLogin =
|
|
142
|
+
const lastLogin = d({
|
|
144
143
|
en: 'Last login: {{date}} at {{time}}',
|
|
145
144
|
zh: '上次登录:{{date}} {{time}}',
|
|
146
145
|
ja: '最終ログイン:{{date}} {{time}}',
|
|
@@ -149,7 +148,7 @@ const lastLogin = dict({
|
|
|
149
148
|
|
|
150
149
|
export const Page2 = () => {
|
|
151
150
|
|
|
152
|
-
useT()
|
|
151
|
+
const { t } = useT()
|
|
153
152
|
|
|
154
153
|
return (
|
|
155
154
|
<>
|
|
@@ -171,31 +170,31 @@ Creates a scoped i18n instance.
|
|
|
171
170
|
```ts
|
|
172
171
|
import { kotori } from 'kotori'
|
|
173
172
|
|
|
174
|
-
export const { useT,
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
export const { useT, d, setLanguage } = kotori({
|
|
174
|
+
primary: 'en',
|
|
175
|
+
secondaries: ['zh', 'ja', 'ms'],
|
|
177
176
|
})
|
|
178
177
|
```
|
|
179
178
|
|
|
180
179
|
| option | type | description |
|
|
181
180
|
| --- | --- | --- |
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
181
|
+
| `primary` | `BCP47LanguageTag` | The source language. Drives variable inference. |
|
|
182
|
+
| `secondaries` | `BCP47LanguageTag[]` | Additional supported languages. |
|
|
184
183
|
|
|
185
|
-
Returns `{
|
|
184
|
+
Returns `{ d, useT, setLanguage, r }`.
|
|
186
185
|
|
|
187
|
-
### `
|
|
186
|
+
### `d(translations)<argsType?>`
|
|
188
187
|
|
|
189
|
-
Defines a translation unit. Takes one string per language.
|
|
188
|
+
Defines a translation unit. Takes one string per language. Return `dictionary` object.
|
|
190
189
|
|
|
191
190
|
```ts
|
|
192
|
-
const time =
|
|
191
|
+
const time = d({ en: '{{hour}}:{{minute}}' })
|
|
193
192
|
```
|
|
194
193
|
|
|
195
194
|
By default, variables are typed as `string | number`. Pass a generic to narrow them:
|
|
196
195
|
|
|
197
196
|
```ts
|
|
198
|
-
const time =
|
|
197
|
+
const time = d({ en: '{{hour}}:{{minute}}' })<{
|
|
199
198
|
hour: number
|
|
200
199
|
minute: number
|
|
201
200
|
}>
|
|
@@ -209,20 +208,32 @@ Updates the current language and rerenders all active `useT` consumers across al
|
|
|
209
208
|
setLanguage('zh')
|
|
210
209
|
```
|
|
211
210
|
|
|
212
|
-
### `
|
|
211
|
+
### `r(dictionary, args?)`
|
|
213
212
|
|
|
214
|
-
Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
|
|
213
|
+
Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
|
|
214
|
+
|
|
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.
|
|
215
216
|
|
|
216
217
|
```tsx
|
|
217
|
-
|
|
218
|
+
r(intro, { name: 'John', age: 30 })}
|
|
218
219
|
```
|
|
219
220
|
|
|
220
221
|
### `useT()`
|
|
221
222
|
|
|
222
|
-
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 }`.
|
|
223
224
|
|
|
224
225
|
```ts
|
|
225
|
-
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>
|
|
226
237
|
```
|
|
227
238
|
|
|
228
239
|
## Language Tags
|
|
@@ -242,10 +253,9 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
|
|
|
242
253
|
- Pluralization support
|
|
243
254
|
- Gender support
|
|
244
255
|
- Value formatting (date, number, currency)
|
|
245
|
-
- Support for non-React frameworks (Vue, Svelte, Angular, etc.)
|
|
246
256
|
|
|
247
257
|
## Trivial
|
|
248
258
|
|
|
249
|
-
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.
|
|
250
260
|
|
|
251
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",
|