kotori 0.0.13 → 0.1.2
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 +20 -17
- package/dist/index.cjs +17 -23
- package/dist/index.d.cts +3 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +16 -22
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ npm i kotori
|
|
|
52
52
|
```ts
|
|
53
53
|
import { kotori } from './kotori'
|
|
54
54
|
|
|
55
|
-
export const { createTranslations, dict } = kotori({
|
|
55
|
+
export const { createTranslations, dict, setLanguage } = kotori({
|
|
56
56
|
primaryLanguageTag: 'en',
|
|
57
57
|
secondaryLanguageTags: ['zh', 'ja', 'ms'],
|
|
58
58
|
})
|
|
@@ -83,13 +83,13 @@ const { useTranslations } = createTranslations({
|
|
|
83
83
|
time,
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
|
|
87
86
|
export const Page1 = () => {
|
|
88
|
-
const { t, setLanguage } = useTranslations()
|
|
87
|
+
const { t, language, setLanguage } = useTranslations()
|
|
89
88
|
return (
|
|
90
89
|
<>
|
|
91
90
|
<select
|
|
92
91
|
name="language"
|
|
92
|
+
value={language}
|
|
93
93
|
onChange={(e) => setLanguage(e.target.value as 'en')}
|
|
94
94
|
>
|
|
95
95
|
<option value="en">English</option>
|
|
@@ -131,17 +131,18 @@ const lastLogin = dict({
|
|
|
131
131
|
})<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
|
|
132
132
|
|
|
133
133
|
const { useTranslations } = createTranslations({
|
|
134
|
-
|
|
134
|
+
weather,
|
|
135
135
|
score,
|
|
136
136
|
lastLogin,
|
|
137
137
|
})
|
|
138
138
|
|
|
139
139
|
export const Page2 = () => {
|
|
140
|
-
const { t, setLanguage } = useTranslations()
|
|
140
|
+
const { t, language, setLanguage } = useTranslations()
|
|
141
141
|
return (
|
|
142
142
|
<>
|
|
143
143
|
<select
|
|
144
144
|
name="language"
|
|
145
|
+
value={language}
|
|
145
146
|
onChange={(e) => setLanguage(e.target.value as 'en')}
|
|
146
147
|
>
|
|
147
148
|
<option value="en">English</option>
|
|
@@ -185,7 +186,7 @@ const mismatch = dict({ en: 'Hi {{name}}', zh: '你好 {{other}}' })
|
|
|
185
186
|
|
|
186
187
|
### Custom argument types
|
|
187
188
|
|
|
188
|
-
By default, variables are typed as `string`. Pass a generic to narrow them:
|
|
189
|
+
By default, variables are typed as `string | number`. Pass a generic to narrow them:
|
|
189
190
|
|
|
190
191
|
```ts
|
|
191
192
|
const time = dict({ en: '{{hour}}:{{minute}}' })<{
|
|
@@ -205,33 +206,35 @@ Creates a scoped i18n instance.
|
|
|
205
206
|
| `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
|
|
206
207
|
| `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
|
|
207
208
|
|
|
208
|
-
Returns `{ dict, createTranslations }`.
|
|
209
|
+
Returns `{ dict, createTranslations, setLanguage }`.
|
|
209
210
|
|
|
210
211
|
### `dict(translations)<argsType?>`
|
|
211
212
|
|
|
212
|
-
Defines a translation unit. Takes one string per language. Optionally takes a generic to
|
|
213
|
-
|
|
214
|
-
Returns `() => { translations: Record<string, string> }`.
|
|
213
|
+
Defines a translation unit. Takes one string per language. Optionally takes a generic to narrow the interpolated variable types.
|
|
215
214
|
|
|
216
215
|
### `createTranslations(dicts)`
|
|
217
216
|
|
|
218
217
|
Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
|
|
219
218
|
|
|
219
|
+
### `setLanguage(tag)`
|
|
220
|
+
|
|
221
|
+
Updates the current language and rerenders all active `useTranslations` consumers across all pages. Available directly on the `kotori` instance — useful for calling outside of React (route guards, axios interceptors, etc.).
|
|
222
|
+
|
|
220
223
|
### `useTranslations()`
|
|
221
224
|
|
|
222
|
-
React hook. Returns `{ t,
|
|
225
|
+
React hook. Returns `{ t, language, setLanguage }`.
|
|
223
226
|
|
|
224
|
-
| return | description |
|
|
225
|
-
| --- | --- |
|
|
226
|
-
| `t(key, args?)` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. |
|
|
227
|
-
| `
|
|
228
|
-
| `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
|
|
227
|
+
| return | type | description |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| `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. |
|
|
230
|
+
| `language` | `WorkingTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
|
|
231
|
+
| `setLanguage(tag)` | `void` | Updates the language and rerenders all active `useTranslations` consumers. |
|
|
229
232
|
|
|
230
233
|
## Language Tags
|
|
231
234
|
|
|
232
235
|
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.
|
|
233
236
|
|
|
234
|
-
## Trivial
|
|
237
|
+
## Trivial
|
|
235
238
|
|
|
236
239
|
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.
|
|
237
240
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,51 +1,45 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let react = require("react");
|
|
3
|
-
|
|
4
3
|
//#region src/index.ts
|
|
5
4
|
const kotori = (props) => {
|
|
6
5
|
const listeners = /* @__PURE__ */ new Set();
|
|
7
6
|
let languageTag = props.primaryLanguageTag;
|
|
8
7
|
const snapshots = /* @__PURE__ */ new Map();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const setLanguage = (tag) => {
|
|
9
|
+
languageTag = tag;
|
|
10
|
+
snapshots.forEach((snapshot, key) => {
|
|
11
|
+
snapshots.set(key, {
|
|
12
|
+
...snapshot,
|
|
13
|
+
language: tag
|
|
15
14
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
15
|
+
});
|
|
16
|
+
listeners.forEach((listener) => {
|
|
17
|
+
listener();
|
|
18
|
+
});
|
|
20
19
|
};
|
|
21
20
|
return {
|
|
21
|
+
setLanguage,
|
|
22
22
|
dict: (translation) => () => ({ translation }),
|
|
23
23
|
createTranslations: (dictCallbacks) => {
|
|
24
24
|
const s = Symbol();
|
|
25
|
-
let refCount = 0;
|
|
26
25
|
const snapshot = {
|
|
27
|
-
|
|
26
|
+
language: languageTag,
|
|
27
|
+
setLanguage,
|
|
28
28
|
t: (key, ...args) => {
|
|
29
|
-
let locale = dictCallbacks[key]?.().translation[languageTag];
|
|
30
|
-
if (!locale) return;
|
|
29
|
+
let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
|
|
31
30
|
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
32
31
|
return locale;
|
|
33
32
|
}
|
|
34
33
|
};
|
|
35
34
|
snapshots.set(s, snapshot);
|
|
36
35
|
return { useTranslations: () => (0, react.useSyncExternalStore)((listener) => {
|
|
37
|
-
if (refCount === 0) snapshots.set(s, snapshot);
|
|
38
|
-
refCount++;
|
|
39
36
|
listeners.add(listener);
|
|
40
37
|
return () => {
|
|
41
|
-
refCount--;
|
|
42
|
-
if (refCount === 0) snapshots.delete(s);
|
|
43
38
|
listeners.delete(listener);
|
|
44
39
|
};
|
|
45
|
-
}, () => snapshots.get(s)) };
|
|
40
|
+
}, () => snapshots.get(s), () => snapshot) };
|
|
46
41
|
}
|
|
47
42
|
};
|
|
48
43
|
};
|
|
49
|
-
|
|
50
44
|
//#endregion
|
|
51
|
-
exports.kotori = kotori;
|
|
45
|
+
exports.kotori = kotori;
|
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
12
12
|
primaryLanguageTag: PrimaryTag;
|
|
13
13
|
secondaryLanguageTags: SecondaryTags[];
|
|
14
14
|
}) => {
|
|
15
|
+
setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
|
|
15
16
|
dict: <const PrimaryString extends string, const SecondaryObject extends { [Key in SecondaryTags]: 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 }>(translation: { [Key in PrimaryTag]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
|
|
16
17
|
translation: typeof translation;
|
|
17
18
|
[_args]?: ArgsType;
|
|
@@ -21,9 +22,9 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
21
22
|
[_args]?: Record<string, string | number>;
|
|
22
23
|
}>>>(dictCallbacks: DictCallbacks) => {
|
|
23
24
|
useTranslations: () => {
|
|
24
|
-
|
|
25
|
-
getLanguage: () => PrimaryTag | SecondaryTags;
|
|
25
|
+
language: PrimaryTag | SecondaryTags;
|
|
26
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;
|
|
27
28
|
};
|
|
28
29
|
};
|
|
29
30
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -12,6 +12,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
12
12
|
primaryLanguageTag: PrimaryTag;
|
|
13
13
|
secondaryLanguageTags: SecondaryTags[];
|
|
14
14
|
}) => {
|
|
15
|
+
setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
|
|
15
16
|
dict: <const PrimaryString extends string, const SecondaryObject extends { [Key in SecondaryTags]: 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 }>(translation: { [Key in PrimaryTag]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
|
|
16
17
|
translation: typeof translation;
|
|
17
18
|
[_args]?: ArgsType;
|
|
@@ -21,9 +22,9 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
|
|
|
21
22
|
[_args]?: Record<string, string | number>;
|
|
22
23
|
}>>>(dictCallbacks: DictCallbacks) => {
|
|
23
24
|
useTranslations: () => {
|
|
24
|
-
|
|
25
|
-
getLanguage: () => PrimaryTag | SecondaryTags;
|
|
25
|
+
language: PrimaryTag | SecondaryTags;
|
|
26
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;
|
|
27
28
|
};
|
|
28
29
|
};
|
|
29
30
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,50 +1,44 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
|
-
|
|
3
2
|
//#region src/index.ts
|
|
4
3
|
const kotori = (props) => {
|
|
5
4
|
const listeners = /* @__PURE__ */ new Set();
|
|
6
5
|
let languageTag = props.primaryLanguageTag;
|
|
7
6
|
const snapshots = /* @__PURE__ */ new Map();
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const setLanguage = (tag) => {
|
|
8
|
+
languageTag = tag;
|
|
9
|
+
snapshots.forEach((snapshot, key) => {
|
|
10
|
+
snapshots.set(key, {
|
|
11
|
+
...snapshot,
|
|
12
|
+
language: tag
|
|
14
13
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
14
|
+
});
|
|
15
|
+
listeners.forEach((listener) => {
|
|
16
|
+
listener();
|
|
17
|
+
});
|
|
19
18
|
};
|
|
20
19
|
return {
|
|
20
|
+
setLanguage,
|
|
21
21
|
dict: (translation) => () => ({ translation }),
|
|
22
22
|
createTranslations: (dictCallbacks) => {
|
|
23
23
|
const s = Symbol();
|
|
24
|
-
let refCount = 0;
|
|
25
24
|
const snapshot = {
|
|
26
|
-
|
|
25
|
+
language: languageTag,
|
|
26
|
+
setLanguage,
|
|
27
27
|
t: (key, ...args) => {
|
|
28
|
-
let locale = dictCallbacks[key]?.().translation[languageTag];
|
|
29
|
-
if (!locale) return;
|
|
28
|
+
let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
|
|
30
29
|
for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
|
|
31
30
|
return locale;
|
|
32
31
|
}
|
|
33
32
|
};
|
|
34
33
|
snapshots.set(s, snapshot);
|
|
35
34
|
return { useTranslations: () => useSyncExternalStore((listener) => {
|
|
36
|
-
if (refCount === 0) snapshots.set(s, snapshot);
|
|
37
|
-
refCount++;
|
|
38
35
|
listeners.add(listener);
|
|
39
36
|
return () => {
|
|
40
|
-
refCount--;
|
|
41
|
-
if (refCount === 0) snapshots.delete(s);
|
|
42
37
|
listeners.delete(listener);
|
|
43
38
|
};
|
|
44
|
-
}, () => snapshots.get(s)) };
|
|
39
|
+
}, () => snapshots.get(s), () => snapshot) };
|
|
45
40
|
}
|
|
46
41
|
};
|
|
47
42
|
};
|
|
48
|
-
|
|
49
43
|
//#endregion
|
|
50
|
-
export { kotori };
|
|
44
|
+
export { kotori };
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kotori",
|
|
3
3
|
"description": "Strongly-typed and composable internationalization library for React",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.2",
|
|
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",
|
|
8
8
|
"build": "tsdown",
|
|
9
|
+
"size": "tsdown --minify",
|
|
9
10
|
"test": "vitest",
|
|
10
11
|
"lint": "npx @biomejs/biome check --write",
|
|
11
12
|
"dev": "vite --host"
|