kotori 1.0.2 → 2.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,26 +14,19 @@ 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
-
18
17
  // ❌ TypeScript error: missing key 'name'
19
18
  zh: '你好,现在是 {{time}} 吗?',
20
-
21
19
  // ❌ TypeScript error: unknown key 'nam'
22
20
  ms: 'Hai {{nam}}, adakah pukul {{time}} sekarang?'
23
-
24
21
  // optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
25
22
  })<{name: string; time: `${number}:${number}`}>
26
23
 
27
-
28
24
  // ✅ Works
29
25
  t('intro', { name: 'John', time: '12:25' })
30
-
31
26
  // ❌ TypeScript error: missing { name }
32
27
  t('intro', { time: '12:25' })
33
-
34
28
  // ❌ TypeScript error: unknown key 'nama'
35
29
  t('intro', { nama: 'John', time: '12:25' })
36
-
37
30
  // ❌ TypeScript error: invalid format for 'time'
38
31
  t('intro', { name: 'John', time: '12-00' })
39
32
  ```
@@ -42,11 +35,11 @@ t('intro', { name: 'John', time: '12-00' })
42
35
  - No JSON
43
36
  - No dependencies
44
37
  - No build step
45
- - 0.34kb gzipped
38
+ - 0.38kb gzipped
46
39
  - Modular and tree-shakeable
47
40
  - Language change in one page rerenders all pages
48
- - Variables typed and inferred from string literals — no more string typos
49
- - maximum type safety with minimum types
41
+ - Translation keys are typed — no more string typos
42
+ - Variables typed and inferred from string literals
50
43
 
51
44
  Demo: <https://stackblitz.com/edit/vitejs-vite-nyxwmhre?file=src%2FApp.tsx>
52
45
 
@@ -63,7 +56,7 @@ npm i kotori
63
56
  ```ts
64
57
  import { kotori } from './kotori'
65
58
 
66
- export const { useT, dict, setLanguage } = kotori({
59
+ export const { createT, dict, setLanguage } = kotori({
67
60
  primaryLanguageTag: 'en',
68
61
  secondaryLanguageTags: ['zh', 'ja', 'ms'],
69
62
  })
@@ -72,7 +65,7 @@ export const { useT, dict, setLanguage } = kotori({
72
65
  **page1.tsx**
73
66
 
74
67
  ```tsx
75
- import { useT, dict } from './utils'
68
+ import { createT, dict } from './utils'
76
69
 
77
70
  const intro = dict({
78
71
  en: 'my name is {{name}}, I am {{age}} years old.',
@@ -89,6 +82,11 @@ const time = dict({
89
82
  // optional: type your arguments, by default it's `Record<'time', string | number>` in this example
90
83
  })<{ time: `${number}:${number}:${number}` }>
91
84
 
85
+ const { useT } = createT({
86
+ intro,
87
+ time,
88
+ })
89
+
92
90
  export const Page1 = () => {
93
91
  const { t, language, setLanguage } = useT()
94
92
  return (
@@ -113,7 +111,7 @@ export const Page1 = () => {
113
111
  **page2.tsx**
114
112
 
115
113
  ```tsx
116
- import { useT, dict } from './utils'
114
+ import { createT, dict } from './utils'
117
115
 
118
116
  const weather = dict({
119
117
  en: 'The weather in {{city}} has {{humidity}}% humidity.',
@@ -136,6 +134,12 @@ const lastLogin = dict({
136
134
  ms: 'Log masuk terakhir: {{date}} pada {{time}}',
137
135
  })<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
138
136
 
137
+ const { useT } = createT({
138
+ weather,
139
+ score,
140
+ lastLogin,
141
+ })
142
+
139
143
  export const Page2 = () => {
140
144
  const { t, language, setLanguage } = useT()
141
145
  return (
@@ -150,9 +154,9 @@ export const Page2 = () => {
150
154
  <option value="ja">Japanese</option>
151
155
  <option value="ms">Malay</option>
152
156
  </select>
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>
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>
156
160
  </>
157
161
  )
158
162
  }
@@ -160,7 +164,7 @@ export const Page2 = () => {
160
164
 
161
165
  ## API
162
166
 
163
- ![how kotori works](image.webp)
167
+ ![API](image.webp)
164
168
 
165
169
  ### `kotori(options)`
166
170
 
@@ -171,11 +175,15 @@ Creates a scoped i18n instance.
171
175
  | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
172
176
  | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
173
177
 
174
- Returns `{ dict, useT, setLanguage }`.
178
+ Returns `{ dict, createT, setLanguage }`.
175
179
 
176
180
  ### `dict(translations)<argsType?>`
177
181
 
178
- Defines a translation unit. Takes one string per language. Optionally takes a generic to narrow the interpolated variable types.
182
+ Defines a translation unit. Takes one string per language
183
+
184
+ ```ts
185
+ const time = dict({ en: '{{hour}}:{{minute}}' })
186
+ ```
179
187
 
180
188
  By default, variables are typed as `string | number`. Pass a generic to narrow them:
181
189
 
@@ -186,6 +194,10 @@ const time = dict({ en: '{{hour}}:{{minute}}' })<{
186
194
  }>
187
195
  ```
188
196
 
197
+ ### `createT(dicts)`
198
+
199
+ Registers a set of dicts and returns `{ useT }`. Call once per page or feature module.
200
+
189
201
  ### `setLanguage(tag)`
190
202
 
191
203
  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.).
@@ -196,7 +208,7 @@ React hook. Returns `{ t, language, setLanguage }`.
196
208
 
197
209
  | return | type | description |
198
210
  | --- | --- | --- |
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. |
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. |
200
212
  | `language` | `primaryLanguageTag` \| `secondaryLanguageTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
201
213
  | `setLanguage(tag)` | `void` | Updates the language and rerenders all active `useT` consumers. |
202
214
 
@@ -204,17 +216,8 @@ React hook. Returns `{ t, language, setLanguage }`.
204
216
 
205
217
  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.
206
218
 
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
-
215
219
  ## Trivial
216
220
 
217
221
  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.
218
222
 
219
223
  *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
220
-
package/dist/index.cjs CHANGED
@@ -3,10 +3,10 @@ let react = require("react");
3
3
  //#region src/index.ts
4
4
  const kotori = (props) => {
5
5
  const listeners = /* @__PURE__ */ new Set();
6
- let language = props.primaryLanguageTag;
6
+ let languageTag = props.primaryLanguageTag;
7
7
  const snapshots = /* @__PURE__ */ new Map();
8
8
  const setLanguage = (tag) => {
9
- language = tag;
9
+ languageTag = tag;
10
10
  snapshots.forEach((snapshot, key) => {
11
11
  snapshots.set(key, {
12
12
  ...snapshot,
@@ -23,19 +23,23 @@ const kotori = (props) => {
23
23
  listeners.delete(listener);
24
24
  };
25
25
  };
26
- const snapshot = {
27
- language,
28
- setLanguage,
29
- t: (dict, ...args) => {
30
- let locale = dict().translation[language] || "unable_to_load_translations";
31
- for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
32
- return locale;
33
- }
34
- };
35
26
  return {
36
27
  setLanguage,
37
28
  dict: (translation) => () => ({ translation }),
38
- useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
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
+ }
39
43
  };
40
44
  };
41
45
  //#endregion
package/dist/index.d.cts CHANGED
@@ -17,13 +17,15 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
17
17
  translation: typeof translation;
18
18
  [_args]?: ArgsType;
19
19
  }>;
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;
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
+ };
27
29
  };
28
30
  };
29
31
  //#endregion
package/dist/index.d.mts CHANGED
@@ -17,13 +17,15 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
17
17
  translation: typeof translation;
18
18
  [_args]?: ArgsType;
19
19
  }>;
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;
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
+ };
27
29
  };
28
30
  };
29
31
  //#endregion
package/dist/index.mjs CHANGED
@@ -2,10 +2,10 @@ import { useSyncExternalStore } from "react";
2
2
  //#region src/index.ts
3
3
  const kotori = (props) => {
4
4
  const listeners = /* @__PURE__ */ new Set();
5
- let language = props.primaryLanguageTag;
5
+ let languageTag = props.primaryLanguageTag;
6
6
  const snapshots = /* @__PURE__ */ new Map();
7
7
  const setLanguage = (tag) => {
8
- language = tag;
8
+ languageTag = tag;
9
9
  snapshots.forEach((snapshot, key) => {
10
10
  snapshots.set(key, {
11
11
  ...snapshot,
@@ -22,19 +22,23 @@ const kotori = (props) => {
22
22
  listeners.delete(listener);
23
23
  };
24
24
  };
25
- const snapshot = {
26
- language,
27
- setLanguage,
28
- t: (dict, ...args) => {
29
- let locale = dict().translation[language] || "unable_to_load_translations";
30
- for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
31
- return locale;
32
- }
33
- };
34
25
  return {
35
26
  setLanguage,
36
27
  dict: (translation) => () => ({ translation }),
37
- useT: () => useSyncExternalStore(subscribe, () => snapshot, () => snapshot)
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
+ }
38
42
  };
39
43
  };
40
44
  //#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": "1.0.2",
4
+ "version": "2.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",