kotori 2.0.2 → 3.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 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
 
@@ -56,7 +63,7 @@ npm i kotori
56
63
  ```ts
57
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,7 +160,7 @@ 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
 
@@ -175,15 +171,11 @@ Creates a scoped i18n instance.
175
171
  | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
176
172
  | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
177
173
 
178
- Returns `{ dict, createT, setLanguage }`.
174
+ Returns `{ dict, useT, setLanguage }`.
179
175
 
180
176
  ### `dict(translations)<argsType?>`
181
177
 
182
- Defines a translation unit. Takes one string per language
183
-
184
- ```ts
185
- const time = dict({ en: '{{hour}}:{{minute}}' })
186
- ```
178
+ Defines a translation unit. Takes one string per language. Optionally takes a generic to narrow the interpolated variable types.
187
179
 
188
180
  By default, variables are typed as `string | number`. Pass a generic to narrow them:
189
181
 
@@ -194,10 +186,6 @@ const time = dict({ en: '{{hour}}:{{minute}}' })<{
194
186
  }>
195
187
  ```
196
188
 
197
- ### `createT(dicts)`
198
-
199
- Registers a set of dicts and returns `{ useT }`. Call once per page or feature module.
200
-
201
189
  ### `setLanguage(tag)`
202
190
 
203
191
  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 +196,7 @@ React hook. Returns `{ t, language, setLanguage }`.
208
196
 
209
197
  | return | type | description |
210
198
  | --- | --- | --- |
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. |
199
+ | `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
200
  | `language` | `primaryLanguageTag` \| `secondaryLanguageTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
213
201
  | `setLanguage(tag)` | `void` | Updates the language and rerenders all active `useT` consumers. |
214
202
 
@@ -216,8 +204,17 @@ React hook. Returns `{ t, language, setLanguage }`.
216
204
 
217
205
  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
206
 
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
+
219
215
  ## Trivial
220
216
 
221
217
  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
218
 
223
219
  *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
220
+
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.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",