kotori 5.0.12 → 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
@@ -14,18 +14,14 @@
14
14
  🕊️ Kotori is a zero-config, fully type-safe, and modular internationalization library for React that compiles down to just 0.28kB. No JSON, no external CLI tools, no codegen—just live type inference from your strings.
15
15
  </p>
16
16
 
17
- ## Note
18
-
19
- ⚠️ Doesn't work with React Compiler!!
20
-
21
17
  ```ts
22
- const { dict, t } = kotori({
23
- primaryLanguageTag: 'en',
24
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
18
+ const { d, useT } = kotori({
19
+ primary: 'en',
20
+ secondaries: ['zh', 'ja', 'ms'],
25
21
  })
26
22
 
27
23
  // ❌ TypeScript error: missing japanese translation
28
- const intro = dict({
24
+ const intro = d({
29
25
  // ⭐ base string drives the type contract
30
26
  en: 'Hello {{name}}, is it {{time}} now?',
31
27
 
@@ -38,18 +34,21 @@ const intro = dict({
38
34
  // optional: type your arguments, by default it's `Record<'name'|'time', string | number>` in this example
39
35
  })<{name: string; time: `${number}:${number}`}>
40
36
 
37
+ const Component = () => {
38
+ const { t } = useT()
41
39
 
42
- // ✅ Works
43
- t(intro, { name: 'John', time: '12:25' })
40
+ // ✅ Works
41
+ t(intro, { name: 'John', time: '12:25' })
44
42
 
45
- // ❌ TypeScript error: missing { name }
46
- t(intro, { time: '12:25' })
43
+ // ❌ TypeScript error: missing { name }
44
+ t(intro, { time: '12:25' })
47
45
 
48
- // ❌ TypeScript error: unknown key 'nama'
49
- t(intro, { nama: 'John', time: '12:25' })
46
+ // ❌ TypeScript error: unknown key 'nama'
47
+ t(intro, { nama: 'John', time: '12:25' })
50
48
 
51
- // ❌ TypeScript error: invalid format for 'time'
52
- t(intro, { name: 'John', time: '12-00' })
49
+ // ❌ TypeScript error: invalid format for 'time'
50
+ t(intro, { name: 'John', time: '12-00' })
51
+ }
53
52
  ```
54
53
 
55
54
  - No codegen
@@ -77,20 +76,20 @@ npm i kotori
77
76
  ```ts
78
77
  import { kotori } from 'kotori'
79
78
 
80
- export const { useT, dict, setLanguage, t } = kotori({
81
- primaryLanguageTag: 'en',
82
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
79
+ export const { useT, d, setLanguage } = kotori({
80
+ primary: 'en',
81
+ secondaries: ['zh', 'ja', 'ms'],
83
82
  })
84
83
 
85
84
  // you can define your dicts in the same file or separate them by component, it's up to you
86
- export const intro = dict({
85
+ export const intro = d({
87
86
  en: 'my name is {{name}}, I am {{age}} years old.',
88
87
  zh: '我叫{{name}},我今年{{age}}岁了。',
89
88
  ja: '私の名前は{{name}}で、{{age}}歳です。',
90
89
  ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
91
90
  })
92
91
 
93
- export const time = dict({
92
+ export const time = d({
94
93
  en: 'time {{time}}',
95
94
  zh: '时间 {{time}}',
96
95
  ja: '時間 {{time}}',
@@ -102,11 +101,11 @@ export const time = dict({
102
101
  ### page1.tsx
103
102
 
104
103
  ```tsx
105
- import { useT, dict, setLanguage, t, intro, time } from './locales'
104
+ import { useT, setLanguage, intro, time } from './locales'
106
105
 
107
106
  export const Page1 = () => {
108
107
 
109
- const language = useT()
108
+ const { language, t } = useT()
110
109
 
111
110
  return (
112
111
  <>
@@ -130,17 +129,17 @@ export const Page1 = () => {
130
129
  ### page2.tsx
131
130
 
132
131
  ```tsx
133
- import { useT, dict, setLanguage, t, } from './locales'
132
+ import { useT, d } from './locales'
134
133
 
135
134
  // you can also define dicts in the same file as your components, it's up to you
136
- const weather = dict({
135
+ const weather = d({
137
136
  en: 'The weather in {{city}} has {{humidity}}% humidity.',
138
137
  zh: '{{city}}的天气湿度为{{humidity}}%。',
139
138
  ja: '{{city}}の湿度は{{humidity}}%です。',
140
139
  ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
141
140
  })<{ city: string; humidity: number }>
142
141
 
143
- const lastLogin = dict({
142
+ const lastLogin = d({
144
143
  en: 'Last login: {{date}} at {{time}}',
145
144
  zh: '上次登录:{{date}} {{time}}',
146
145
  ja: '最終ログイン:{{date}} {{time}}',
@@ -149,7 +148,7 @@ const lastLogin = dict({
149
148
 
150
149
  export const Page2 = () => {
151
150
 
152
- useT()
151
+ const { t } = useT()
153
152
 
154
153
  return (
155
154
  <>
@@ -171,31 +170,31 @@ Creates a scoped i18n instance.
171
170
  ```ts
172
171
  import { kotori } from 'kotori'
173
172
 
174
- export const { useT, dict, setLanguage } = kotori({
175
- primaryLanguageTag: 'en',
176
- secondaryLanguageTags: ['zh', 'ja', 'ms'],
173
+ export const { useT, d, setLanguage } = kotori({
174
+ primary: 'en',
175
+ secondaries: ['zh', 'ja', 'ms'],
177
176
  })
178
177
  ```
179
178
 
180
179
  | option | type | description |
181
180
  | --- | --- | --- |
182
- | `primaryLanguageTag` | `BCP47LanguageTag` | The source language. Drives variable inference. |
183
- | `secondaryLanguageTags` | `BCP47LanguageTag[]` | Additional supported languages. |
181
+ | `primary` | `BCP47LanguageTag` | The source language. Drives variable inference. |
182
+ | `secondaries` | `BCP47LanguageTag[]` | Additional supported languages. |
184
183
 
185
- Returns `{ dict, useT, setLanguage, t }`.
184
+ Returns `{ d, useT, setLanguage, r }`.
186
185
 
187
- ### `dict(translations)<argsType?>`
186
+ ### `d(translations)<argsType?>`
188
187
 
189
- Defines a translation unit. Takes one string per language.
188
+ Defines a translation unit. Takes one string per language. Return `dictionary` object.
190
189
 
191
190
  ```ts
192
- const time = dict({ en: '{{hour}}:{{minute}}' })
191
+ const time = d({ en: '{{hour}}:{{minute}}' })
193
192
  ```
194
193
 
195
194
  By default, variables are typed as `string | number`. Pass a generic to narrow them:
196
195
 
197
196
  ```ts
198
- const time = dict({ en: '{{hour}}:{{minute}}' })<{
197
+ const time = d({ en: '{{hour}}:{{minute}}' })<{
199
198
  hour: number
200
199
  minute: number
201
200
  }>
@@ -209,20 +208,32 @@ Updates the current language and rerenders all active `useT` consumers across al
209
208
  setLanguage('zh')
210
209
  ```
211
210
 
212
- ### `t(dict, args?)`
211
+ ### `r(dictionary, args?)`
213
212
 
214
- 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.
213
+ Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't.
214
+
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.
215
216
 
216
217
  ```tsx
217
- <p>{t(intro, { name: 'John', age: 30 })}</p>
218
+ r(intro, { name: 'John', age: 30 })}
218
219
  ```
219
220
 
220
221
  ### `useT()`
221
222
 
222
- 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 }`.
223
224
 
224
225
  ```ts
225
- 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>
226
237
  ```
227
238
 
228
239
  ## Language Tags
@@ -242,10 +253,9 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
242
253
  - Pluralization support
243
254
  - Gender support
244
255
  - Value formatting (date, number, currency)
245
- - Support for non-React frameworks (Vue, Svelte, Angular, etc.)
246
256
 
247
257
  ## Trivial
248
258
 
249
- 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.
250
260
 
251
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.12",
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",