kotori 5.0.11 → 6.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
@@ -15,13 +15,13 @@
15
15
  </p>
16
16
 
17
17
  ```ts
18
- const { dict, t } = kotori({
19
- primaryLanguageTag: 'en',
20
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
18
+ const { d, useT } = kotori({
19
+ primary: 'en',
20
+ secondaries: ['zh', 'ja', 'ms'],
21
21
  })
22
22
 
23
23
  // ❌ TypeScript error: missing japanese translation
24
- const intro = dict({
24
+ const intro = d({
25
25
  // ⭐ base string drives the type contract
26
26
  en: 'Hello {{name}}, is it {{time}} now?',
27
27
 
@@ -34,18 +34,21 @@ const intro = dict({
34
34
  // optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
35
35
  })<{name: string; time: `${number}:${number}`}>
36
36
 
37
+ const Component = () => {
38
+ const { t } = useT()
37
39
 
38
- // ✅ Works
39
- t(intro, { name: 'John', time: '12:25' })
40
+ // ✅ Works
41
+ t(intro, { name: 'John', time: '12:25' })
40
42
 
41
- // ❌ TypeScript error: missing { name }
42
- t(intro, { time: '12:25' })
43
+ // ❌ TypeScript error: missing { name }
44
+ t(intro, { time: '12:25' })
43
45
 
44
- // ❌ TypeScript error: unknown key 'nama'
45
- t(intro, { nama: 'John', time: '12:25' })
46
+ // ❌ TypeScript error: unknown key 'nama'
47
+ t(intro, { nama: 'John', time: '12:25' })
46
48
 
47
- // ❌ TypeScript error: invalid format for 'time'
48
- t(intro, { name: 'John', time: '12-00' })
49
+ // ❌ TypeScript error: invalid format for 'time'
50
+ t(intro, { name: 'John', time: '12-00' })
51
+ }
49
52
  ```
50
53
 
51
54
  - No codegen
@@ -73,20 +76,20 @@ npm i kotori
73
76
  ```ts
74
77
  import { kotori } from 'kotori'
75
78
 
76
- export const { useT, dict, setLanguage, t } = kotori({
77
- primaryLanguageTag: 'en',
78
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
79
+ export const { useT, d, setLanguage } = kotori({
80
+ primary: 'en',
81
+ secondaries: ['zh', 'ja', 'ms'],
79
82
  })
80
83
 
81
84
  // you can define your dicts in the same file or separate them by component, it's up to you
82
- export const intro = dict({
85
+ export const intro = d({
83
86
  en: 'my name is {{name}}, I am {{age}} years old.',
84
87
  zh: '我叫{{name}},我今年{{age}}岁了。',
85
88
  ja: '私の名前は{{name}}で、{{age}}歳です。',
86
89
  ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
87
90
  })
88
91
 
89
- export const time = dict({
92
+ export const time = d({
90
93
  en: 'time {{time}}',
91
94
  zh: '时间 {{time}}',
92
95
  ja: '時間 {{time}}',
@@ -98,11 +101,11 @@ export const time = dict({
98
101
  ### page1.tsx
99
102
 
100
103
  ```tsx
101
- import { useT, dict, setLanguage, t, intro, time } from './locales'
104
+ import { useT, setLanguage, intro, time } from './locales'
102
105
 
103
106
  export const Page1 = () => {
104
107
 
105
- const language = useT()
108
+ const { language, t } = useT()
106
109
 
107
110
  return (
108
111
  <>
@@ -126,17 +129,17 @@ export const Page1 = () => {
126
129
  ### page2.tsx
127
130
 
128
131
  ```tsx
129
- import { useT, dict, setLanguage, t, } from './locales'
132
+ import { useT, d } from './locales'
130
133
 
131
134
  // you can also define dicts in the same file as your components, it's up to you
132
- const weather = dict({
135
+ const weather = d({
133
136
  en: 'The weather in {{city}} has {{humidity}}% humidity.',
134
137
  zh: '{{city}}的天气湿度为{{humidity}}%。',
135
138
  ja: '{{city}}の湿度は{{humidity}}%です。',
136
139
  ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
137
140
  })<{ city: string; humidity: number }>
138
141
 
139
- const lastLogin = dict({
142
+ const lastLogin = d({
140
143
  en: 'Last login: {{date}} at {{time}}',
141
144
  zh: '上次登录:{{date}} {{time}}',
142
145
  ja: '最終ログイン:{{date}} {{time}}',
@@ -145,7 +148,7 @@ const lastLogin = dict({
145
148
 
146
149
  export const Page2 = () => {
147
150
 
148
- useT()
151
+ const { t } = useT()
149
152
 
150
153
  return (
151
154
  <>
@@ -167,31 +170,31 @@ Creates a scoped i18n instance.
167
170
  ```ts
168
171
  import { kotori } from 'kotori'
169
172
 
170
- export const { useT, dict, setLanguage } = kotori({
171
- primaryLanguageTag: 'en',
172
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
173
+ export const { useT, d, setLanguage } = kotori({
174
+ primary: 'en',
175
+ secondaries: ['zh', 'ja', 'ms'],
173
176
  })
174
177
  ```
175
178
 
176
179
  | option | type | description |
177
180
  | --- | --- | --- |
178
- | `primaryLanguageTag` | `BCP47LanguageTag` | The source language. Drives variable inference. |
179
- | `secondaryLanguageTags` | `BCP47LanguageTag[]` | Additional supported languages. |
181
+ | `primary` | `BCP47LanguageTag` | The source language. Drives variable inference. |
182
+ | `secondaries` | `BCP47LanguageTag[]` | Additional supported languages. |
180
183
 
181
- Returns `{ dict, useT, setLanguage, t }`.
184
+ Returns `{ d, useT, setLanguage, r }`.
182
185
 
183
- ### `dict(translations)<argsType?>`
186
+ ### `d(translations)<argsType?>`
184
187
 
185
- Defines a translation unit. Takes one string per language.
188
+ Defines a translation unit. Takes one string per language. Return `dictionary` object.
186
189
 
187
190
  ```ts
188
- const time = dict({ en: '{{hour}}:{{minute}}' })
191
+ const time = d({ en: '{{hour}}:{{minute}}' })
189
192
  ```
190
193
 
191
194
  By default, variables are typed as `string | number`. Pass a generic to narrow them:
192
195
 
193
196
  ```ts
194
- const time = dict({ en: '{{hour}}:{{minute}}' })<{
197
+ const time = d({ en: '{{hour}}:{{minute}}' })<{
195
198
  hour: number
196
199
  minute: number
197
200
  }>
@@ -205,20 +208,32 @@ Updates the current language and rerenders all active `useT` consumers across al
205
208
  setLanguage('zh')
206
209
  ```
207
210
 
208
- ### `t(dict, args?)`
211
+ ### `r(dictionary, args?)`
212
+
213
+ Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
209
214
 
210
- Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. Available directly on the `kotori` instance for non-React usage.
215
+ ⚠️ Do not call this inside React components (it will break React Compiler optimization rules). Use this exclusively in raw JS/TS environments like router guards, API interceptors, or state utilities.
211
216
 
212
217
  ```tsx
213
- <p>{t(intro, { name: 'John', age: 30 })}</p>
218
+ r(intro, { name: 'John', age: 30 })}
214
219
  ```
215
220
 
216
221
  ### `useT()`
217
222
 
218
- React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called.
223
+ React hook. Returns the current language tag as a reactive value. Updates when `setLanguage` is called. Returns `{ t, language }`.
219
224
 
220
225
  ```ts
221
- const language = useT()
226
+ const { t, language } = useT()
227
+ ```
228
+
229
+ ### `t(dictionary, args?)`
230
+
231
+ Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
232
+
233
+ React version of `r(dictionary, args?)`, works with React Compiler.
234
+
235
+ ```tsx
236
+ <p>{t(intro, { name: 'John', age: 30 })}</p>
222
237
  ```
223
238
 
224
239
  ## Language Tags
@@ -227,8 +242,8 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
227
242
 
228
243
  ## Tips
229
244
 
230
- - 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.
231
- - 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.
245
+ - If you plan to add new languages frequently, consider colocating all your dicts in a single file or multiple files in one folder. It is easier to copy the entire file and hand it to an AI to translate.
246
+ - 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.
232
247
  - Both approaches are tree-shakeable — only the dicts imported by the current page are included in its bundle.
233
248
 
234
249
  ## Roadmap
@@ -238,10 +253,9 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
238
253
  - Pluralization support
239
254
  - Gender support
240
255
  - Value formatting (date, number, currency)
241
- - Support for non-React frameworks (Vue, Svelte, Angular, etc.)
242
256
 
243
257
  ## Trivial
244
258
 
245
- 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.
259
+ There are already many 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.
246
260
 
247
261
  *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
package/dist/index.cjs CHANGED
@@ -3,9 +3,16 @@ 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;
7
- const setLanguage = (tag) => {
8
- language = tag;
6
+ const t = (dictionary, ...args) => (dictionary().d[snapshot.language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
7
+ let snapshot = {
8
+ language: props.primary,
9
+ t
10
+ };
11
+ const setLanguage = (language) => {
12
+ snapshot = {
13
+ language,
14
+ t: (...args) => t(...args)
15
+ };
9
16
  listeners.forEach((listener) => {
10
17
  listener();
11
18
  });
@@ -16,12 +23,11 @@ const kotori = (props) => {
16
23
  listeners.delete(listener);
17
24
  };
18
25
  };
19
- const t = (dict, ...args) => (dict().translation[language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
20
26
  return {
21
27
  setLanguage,
22
- t,
23
- dict: (translation) => () => ({ translation }),
24
- useT: () => (0, react.useSyncExternalStore)(subscribe, () => language, () => language)
28
+ r: t,
29
+ d: (dictionary) => () => ({ d: dictionary }),
30
+ useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
25
31
  };
26
32
  };
27
33
  //#endregion
package/dist/index.d.cts CHANGED
@@ -8,20 +8,26 @@ type AllTags = Tags | SubTags;
8
8
  type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
9
9
  type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
10
10
  declare const _args: unique symbol;
11
- declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags extends Exclude<AllTags, PrimaryTag>>(props: {
12
- primaryLanguageTag: PrimaryTag;
13
- secondaryLanguageTags: SecondaryTags[];
11
+ declare const kotori: <const Primary extends AllTags, const Secondary extends Exclude<AllTags, Primary>>(props: {
12
+ primary: Primary;
13
+ secondaries: Secondary[];
14
14
  }) => {
15
- setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
16
- t: <Dict extends () => Readonly<{
17
- translation: Record<PrimaryTag | SecondaryTags, string>;
15
+ setLanguage: (language: Primary | Secondary) => void;
16
+ r: <Dictionary extends () => Readonly<{
17
+ d: Record<Primary | Secondary, string>;
18
18
  [_args]?: Record<string, string | number>;
19
- }>>(dict: Dict, ...args: keyof NonNullable<ReturnType<Dict>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dict>[typeof _args]>]) => string;
20
- 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<{
21
- translation: typeof translation;
19
+ }>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
20
+ d: <const PrimaryString extends string, const SecondaryObject extends { [Key in Secondary]: 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 }>(dictionary: { [Key in Primary]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
21
+ d: typeof dictionary;
22
22
  [_args]?: ArgsType;
23
23
  }>;
24
- useT: () => PrimaryTag | SecondaryTags;
24
+ useT: () => {
25
+ language: Primary | Secondary;
26
+ t: <Dictionary extends () => Readonly<{
27
+ d: Record<Primary | Secondary, string>;
28
+ [_args]?: Record<string, string | number>;
29
+ }>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
30
+ };
25
31
  };
26
32
  //#endregion
27
33
  export { AllTags, SubTags, Tags, kotori };
package/dist/index.d.mts CHANGED
@@ -8,20 +8,26 @@ type AllTags = Tags | SubTags;
8
8
  type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
9
9
  type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
10
10
  declare const _args: unique symbol;
11
- declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags extends Exclude<AllTags, PrimaryTag>>(props: {
12
- primaryLanguageTag: PrimaryTag;
13
- secondaryLanguageTags: SecondaryTags[];
11
+ declare const kotori: <const Primary extends AllTags, const Secondary extends Exclude<AllTags, Primary>>(props: {
12
+ primary: Primary;
13
+ secondaries: Secondary[];
14
14
  }) => {
15
- setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
16
- t: <Dict extends () => Readonly<{
17
- translation: Record<PrimaryTag | SecondaryTags, string>;
15
+ setLanguage: (language: Primary | Secondary) => void;
16
+ r: <Dictionary extends () => Readonly<{
17
+ d: Record<Primary | Secondary, string>;
18
18
  [_args]?: Record<string, string | number>;
19
- }>>(dict: Dict, ...args: keyof NonNullable<ReturnType<Dict>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dict>[typeof _args]>]) => string;
20
- 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<{
21
- translation: typeof translation;
19
+ }>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
20
+ d: <const PrimaryString extends string, const SecondaryObject extends { [Key in Secondary]: 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 }>(dictionary: { [Key in Primary]: PrimaryString } & SecondaryObject) => <const ArgsType extends Record<ExtractVariables<PrimaryString>, string | number> = Record<ExtractVariables<PrimaryString>, string | number>>() => Readonly<{
21
+ d: typeof dictionary;
22
22
  [_args]?: ArgsType;
23
23
  }>;
24
- useT: () => PrimaryTag | SecondaryTags;
24
+ useT: () => {
25
+ language: Primary | Secondary;
26
+ t: <Dictionary extends () => Readonly<{
27
+ d: Record<Primary | Secondary, string>;
28
+ [_args]?: Record<string, string | number>;
29
+ }>>(dictionary: Dictionary, ...args: keyof NonNullable<ReturnType<Dictionary>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<Dictionary>[typeof _args]>]) => string;
30
+ };
25
31
  };
26
32
  //#endregion
27
33
  export { AllTags, SubTags, Tags, kotori };
package/dist/index.mjs CHANGED
@@ -2,9 +2,16 @@ 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;
6
- const setLanguage = (tag) => {
7
- language = tag;
5
+ const t = (dictionary, ...args) => (dictionary().d[snapshot.language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
6
+ let snapshot = {
7
+ language: props.primary,
8
+ t
9
+ };
10
+ const setLanguage = (language) => {
11
+ snapshot = {
12
+ language,
13
+ t: (...args) => t(...args)
14
+ };
8
15
  listeners.forEach((listener) => {
9
16
  listener();
10
17
  });
@@ -15,12 +22,11 @@ const kotori = (props) => {
15
22
  listeners.delete(listener);
16
23
  };
17
24
  };
18
- const t = (dict, ...args) => (dict().translation[language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
19
25
  return {
20
26
  setLanguage,
21
- t,
22
- dict: (translation) => () => ({ translation }),
23
- useT: () => useSyncExternalStore(subscribe, () => language, () => language)
27
+ r: t,
28
+ d: (dictionary) => () => ({ d: dictionary }),
29
+ useT: () => useSyncExternalStore(subscribe, () => snapshot, () => snapshot)
24
30
  };
25
31
  };
26
32
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kotori",
3
3
  "description": "0.28kB Strongly-typed and tree-shakeable internationalization library for React",
4
- "version": "5.0.11",
4
+ "version": "6.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",