kotori 4.0.1 → 5.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
@@ -42,7 +42,7 @@ t(intro, { name: 'John', time: '12-00' })
42
42
  - No JSON
43
43
  - No dependencies
44
44
  - No build step
45
- - 0.33kb gzipped
45
+ - 0.31kb minified and gzipped
46
46
  - Modular and tree-shakeable
47
47
  - Language change in one page rerenders all pages
48
48
  - Variables typed and inferred from string literals — no more string typos
@@ -58,7 +58,7 @@ npm i kotori
58
58
 
59
59
  ## Quick Start
60
60
 
61
- **locales.ts**
61
+ ### locales.ts
62
62
 
63
63
  ```ts
64
64
  import { kotori } from 'kotori'
@@ -68,6 +68,7 @@ export const { useT, dict, setLanguage, t } = kotori({
68
68
  secondaryLanguageTags: ['zh', 'ja', 'ms'],
69
69
  })
70
70
 
71
+ // you can define your dicts in the same file or separate them by component, it's up to you
71
72
  const intro = dict({
72
73
  en: 'my name is {{name}}, I am {{age}} years old.',
73
74
  zh: '我叫{{name}},我今年{{age}}岁了。',
@@ -82,37 +83,16 @@ const time = dict({
82
83
  ms: 'waktu {{time}}',
83
84
  // optional: type your arguments, by default it's `Record<'time', string | number>` in this example
84
85
  })<{ time: `${number}:${number}:${number}` }>
85
-
86
- const weather = dict({
87
- en: 'The weather in {{city}} has {{humidity}}% humidity.',
88
- zh: '{{city}}的天气湿度为{{humidity}}%。',
89
- ja: '{{city}}の湿度は{{humidity}}%です。',
90
- ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
91
- })<{ city: string; humidity: number }>
92
-
93
- const score = dict({
94
- en: 'Your score is {{score}} out of {{total}}.',
95
- zh: '你的得分是 {{total}} 分中的 {{score}} 分。',
96
- ja: 'あなたのスコアは {{total}} 点中 {{score}} 点です。',
97
- ms: 'Markah anda ialah {{score}} daripada {{total}}.',
98
- })<{ score: number; total: number }>
99
-
100
- const lastLogin = dict({
101
- en: 'Last login: {{date}} at {{time}}',
102
- zh: '上次登录:{{date}} {{time}}',
103
- ja: '最終ログイン:{{date}} {{time}}',
104
- ms: 'Log masuk terakhir: {{date}} pada {{time}}',
105
- })<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
106
86
  ```
107
87
 
108
- **page1.tsx**
88
+ ### page1.tsx
109
89
 
110
90
  ```tsx
111
91
  import { useT, dict, setLanguage, t, intro, time } from './locales'
112
92
 
113
93
  export const Page1 = () => {
114
94
 
115
- const { language } = useT()
95
+ const language = useT()
116
96
 
117
97
  return (
118
98
  <>
@@ -133,10 +113,25 @@ export const Page1 = () => {
133
113
  }
134
114
  ```
135
115
 
136
- **page2.tsx**
116
+ ### page2.tsx
137
117
 
138
118
  ```tsx
139
- import { useT, dict, setLanguage, t, weather, score, lastLogin } from './locales'
119
+ import { useT, dict, setLanguage, t, } from './locales'
120
+
121
+ // you can also define dicts in the same file as your components, it's up to you
122
+ const weather = dict({
123
+ en: 'The weather in {{city}} has {{humidity}}% humidity.',
124
+ zh: '{{city}}的天气湿度为{{humidity}}%。',
125
+ ja: '{{city}}の湿度は{{humidity}}%です。',
126
+ ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
127
+ })<{ city: string; humidity: number }>
128
+
129
+ const lastLogin = dict({
130
+ en: 'Last login: {{date}} at {{time}}',
131
+ zh: '上次登录:{{date}} {{time}}',
132
+ ja: '最終ログイン:{{date}} {{time}}',
133
+ ms: 'Log masuk terakhir: {{date}} pada {{time}}',
134
+ })<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
140
135
 
141
136
  export const Page2 = () => {
142
137
 
@@ -145,7 +140,6 @@ export const Page2 = () => {
145
140
  return (
146
141
  <>
147
142
  <p>{t(weather, { city: 'Kuala Lumpur', humidity: 80 })}</p>
148
- <p>{t(score, { score: 87, total: 100 })}</p>
149
143
  <p>{t(lastLogin, { date: '2024-04-24', time: '09:30' })}</p>
150
144
  </>
151
145
  )
@@ -203,15 +197,17 @@ Returns the translated string for the current language. `args` is required if th
203
197
 
204
198
  ### `useT()`
205
199
 
206
- React hook. Returns `{ language }`.
207
-
208
- | return | type | description |
209
- | --- | --- | --- |
210
- | `language` | `primaryLanguageTag` \| `secondaryLanguageTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
200
+ React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called.
211
201
 
212
202
  ## Language Tags
213
203
 
214
- 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.
204
+ 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-CN`) are accepted and validated at the type level.
205
+
206
+ ## Tips
207
+
208
+ - If you plan to add new languages frequently, consider colocating all your dicts in a single file. It is easier to copy the entire file and hand it to an AI to translate.
209
+ - If your supported languages are fixed, consider splitting dicts by page or component. Translations stay close to the code that uses them and are easier to maintain. This approach also pairs well with TypeScript — every time you add a new language, type errors will guide you to every dict that needs updating.
210
+ - Both approaches are tree-shakeable — only the dicts imported by the current page are included in its bundle.
215
211
 
216
212
  ## Roadmap
217
213
 
@@ -227,4 +223,3 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
227
223
  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.
228
224
 
229
225
  *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
230
-
package/dist/index.cjs CHANGED
@@ -6,10 +6,6 @@ const kotori = (props) => {
6
6
  let language = props.primaryLanguageTag;
7
7
  const setLanguage = (tag) => {
8
8
  language = tag;
9
- snapshot = {
10
- ...snapshot,
11
- language
12
- };
13
9
  listeners.forEach((listener) => {
14
10
  listener();
15
11
  });
@@ -21,16 +17,15 @@ const kotori = (props) => {
21
17
  };
22
18
  };
23
19
  const t = (dict, ...args) => {
24
- let locale = dict().translation[language] || "unable_to_load_translations";
20
+ let locale = dict().translation[language] ?? "unable_to_load_translations";
25
21
  for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
26
22
  return locale;
27
23
  };
28
- let snapshot = { language };
29
24
  return {
30
25
  setLanguage,
31
26
  t,
32
27
  dict: (translation) => () => ({ translation }),
33
- useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
28
+ useT: () => (0, react.useSyncExternalStore)(subscribe, () => language, () => language)
34
29
  };
35
30
  };
36
31
  //#endregion
package/dist/index.d.cts CHANGED
@@ -21,9 +21,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
21
  translation: typeof translation;
22
22
  [_args]?: ArgsType;
23
23
  }>;
24
- useT: () => {
25
- language: PrimaryTag | SecondaryTags;
26
- };
24
+ useT: () => PrimaryTag | SecondaryTags;
27
25
  };
28
26
  //#endregion
29
27
  export { AllTags, SubTags, Tags, kotori };
package/dist/index.d.mts CHANGED
@@ -21,9 +21,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
21
  translation: typeof translation;
22
22
  [_args]?: ArgsType;
23
23
  }>;
24
- useT: () => {
25
- language: PrimaryTag | SecondaryTags;
26
- };
24
+ useT: () => PrimaryTag | SecondaryTags;
27
25
  };
28
26
  //#endregion
29
27
  export { AllTags, SubTags, Tags, kotori };
package/dist/index.mjs CHANGED
@@ -5,10 +5,6 @@ const kotori = (props) => {
5
5
  let language = props.primaryLanguageTag;
6
6
  const setLanguage = (tag) => {
7
7
  language = tag;
8
- snapshot = {
9
- ...snapshot,
10
- language
11
- };
12
8
  listeners.forEach((listener) => {
13
9
  listener();
14
10
  });
@@ -20,16 +16,15 @@ const kotori = (props) => {
20
16
  };
21
17
  };
22
18
  const t = (dict, ...args) => {
23
- let locale = dict().translation[language] || "unable_to_load_translations";
19
+ let locale = dict().translation[language] ?? "unable_to_load_translations";
24
20
  for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
25
21
  return locale;
26
22
  };
27
- let snapshot = { language };
28
23
  return {
29
24
  setLanguage,
30
25
  t,
31
26
  dict: (translation) => () => ({ translation }),
32
- useT: () => useSyncExternalStore(subscribe, () => snapshot, () => snapshot)
27
+ useT: () => useSyncExternalStore(subscribe, () => language, () => language)
33
28
  };
34
29
  };
35
30
  //#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": "4.0.1",
4
+ "version": "5.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",
@@ -16,7 +16,8 @@
16
16
  ],
17
17
  "lint-staged": {
18
18
  "*": [
19
- "npm run lint"
19
+ "npm run lint",
20
+ "npm test"
20
21
  ]
21
22
  },
22
23
  "type": "module",
@@ -43,7 +44,6 @@
43
44
  ],
44
45
  "devDependencies": {
45
46
  "@biomejs/biome": "^2.4.12",
46
- "@types/node": "^25.6.0",
47
47
  "@types/react": "^19.2.14",
48
48
  "@types/react-dom": "^19.2.3",
49
49
  "@vitejs/plugin-react": "^6.0.1",
@@ -54,7 +54,6 @@
54
54
  "react": "^19.2.5",
55
55
  "react-dom": "^19.2.5",
56
56
  "tsdown": "^0.21.10",
57
- "tsx": "^4.21.0",
58
57
  "typescript": "^6.0.3",
59
58
  "vitest": "^4.1.5"
60
59
  },
@@ -68,6 +67,6 @@
68
67
  "author": "tylim88",
69
68
  "license": "MIT",
70
69
  "peerDependencies": {
71
- "react": ">=19.2.5"
70
+ "react": ">=18"
72
71
  }
73
72
  }