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 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.38kb gzipped
45
+ - 0.33kb gzipped
39
46
  - Modular and tree-shakeable
40
47
  - Language change in one page rerenders all pages
41
- - Translation keys are typed — no more string typos
42
- - Variables typed and inferred from string literals
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 './kotori'
64
+ import { kotori } from 'kotori'
58
65
 
59
- export const { createT, dict, setLanguage } = kotori({
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 { createT, dict } from './utils'
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 { createT, dict } from './utils'
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('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>
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
- ![API](image.webp)
163
+ ![how kotori works](image.webp)
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` | `AllTags` | The source language. Drives variable inference. |
176
- | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
180
+ | `primaryLanguageTag` | `BCP47LanguageTag` | The source language. Drives variable inference. |
181
+ | `secondaryLanguageTags` | `BCP47LanguageTag[]` | Additional supported languages. |
177
182
 
178
- Returns `{ dict, createT, setLanguage }`.
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(key, args?)` | `string` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn'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 languageTag = props.primaryLanguageTag;
7
- const snapshots = /* @__PURE__ */ new Map();
6
+ let language = props.primaryLanguageTag;
8
7
  const setLanguage = (tag) => {
9
- languageTag = tag;
10
- snapshots.forEach((snapshot, key) => {
11
- snapshots.set(key, {
12
- ...snapshot,
13
- language: tag
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
- 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
- }
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
- 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
- };
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
- 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
- };
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 languageTag = props.primaryLanguageTag;
6
- const snapshots = /* @__PURE__ */ new Map();
5
+ let language = props.primaryLanguageTag;
7
6
  const setLanguage = (tag) => {
8
- languageTag = tag;
9
- snapshots.forEach((snapshot, key) => {
10
- snapshots.set(key, {
11
- ...snapshot,
12
- language: tag
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
- 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
- }
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": "2.0.2",
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",