kotori 5.0.12 → 6.1.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
@@ -11,21 +11,17 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
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.
14
+ 🕊️ Kotori is a zero-config, fully type-safe, and modular internationalization library for React that compiles down to just 0.29kB. 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,31 +34,34 @@ 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
56
55
  - No JSON
57
56
  - No dependencies
58
57
  - No build step
59
- - 0.28kB minified and gzipped
58
+ - 0.29kB minified and gzipped
60
59
  - Modular and tree-shakeable
61
60
  - Language change in one page rerenders all pages
62
61
  - Variables typed and inferred from string literals — no more string typos
63
- - maximum type safety with minimum types
62
+ - Maximum type safety with minimum types
64
63
 
65
- Demo: <https://stackblitz.com/edit/vitejs-vite-nyxwmhre?file=src%2FApp.tsx>
64
+ Demo: <https://stackblitz.com/edit/kotori?file=src%2FApp.tsx>
66
65
 
67
66
  ## Installation
68
67
 
@@ -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
  <>
@@ -164,38 +163,38 @@ export const Page2 = () => {
164
163
 
165
164
  ![how kotori works](image.webp)
166
165
 
167
- ### `kotori(options)`
166
+ ### `kotori(options)` (0.29kB)
168
167
 
169
168
  Creates a scoped i18n instance.
170
169
 
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` | `BCP47LanguageTagWithSubtag` | The source language. Drives variable inference. |
182
+ | `secondaries` | `Exclude<BCP47LanguageTagWithSubtag, primary>[]` | Additional supported languages. Cannot include the primary language. |
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. Returns a `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,43 +208,101 @@ 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 }`.
224
+
225
+ ```ts
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
+ const Intro = () => {
237
+ const { t } = useT()
238
+
239
+ return (
240
+ <p>{t(intro, { name: 'John', age: 30 })}</p>
241
+ )
242
+ }
243
+ ```
244
+
245
+ ### `detectLanguage(instance, options?)` (0.12kB with Kotori or 0.2kB standalone)
246
+
247
+ Detects the user's preferred language from browser settings and sets it on the kotori instance. Iterates through the user's full language preference list in order, stopping at the first match.
223
248
 
224
249
  ```ts
225
- const language = useT()
250
+ import { detectLanguage } from 'kotori'
251
+ import { i18n } from './locales'
252
+
253
+ detectLanguage(i18n)
254
+ ```
255
+
256
+ | option | type | default | description |
257
+ | --- | --- | --- | --- |
258
+ | `fallbackToSubtag` | `boolean` | `true` | If no exact match is found (e.g. `'zh-CN'`), fall back to the subtag (e.g. `'zh'`). |
259
+
260
+ ```ts
261
+ // browser reports: ['zh-CN', 'en-US']
262
+ // declared languages: ['en', 'zh', 'ja', 'ms']
263
+
264
+ detectLanguage(i18n)
265
+ // 'zh-CN' → no exact match → subtag 'zh' → match → setLanguage('zh') ✅
266
+
267
+ detectLanguage(i18n, { fallbackToSubtag: false })
268
+ // 'zh-CN' → no exact match → 'en-US' → no exact match → no-op, stays 'en'
269
+ ```
270
+
271
+ `detectLanguage` also works with non-kotori instances. Any object with a `setLanguage` callback and a `config` object satisfies the interface:
272
+
273
+ ```ts
274
+ detectLanguage({
275
+ setLanguage: (lang: 'en' | 'zh') => mySetLang(lang),
276
+ config: { primary: 'en', secondaries: ['zh'] },
277
+ })
226
278
  ```
227
279
 
228
280
  ## Language Tags
229
281
 
230
- 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.
282
+ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) language tags. The type `BCP47LanguageTagWithSubtag` accepts both subtags (`en`, `zh`) and full tags (`en-US`, `zh-CN`), and is validated at the type level.
283
+
284
+ ```ts
285
+ kotori({ primary: 'en', secondaries: ['zh', 'ms-MY'] }) // ✅
286
+ kotori({ primary: 'klingon', secondaries: ['zh'] }) // ❌ compile error
287
+ ```
231
288
 
232
289
  ## Tips
233
290
 
234
291
  - 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.
235
- - 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.
292
+ - 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.
236
293
  - Both approaches are tree-shakeable — only the dicts imported by the current page are included in its bundle.
294
+ - The `primary` language is the source of truth for variable inference and validation. Write your primary language strings carefully — a variable rename in the primary string becomes a compile error across every secondary language, which is intentional.
237
295
 
238
296
  ## Roadmap
239
297
 
240
- - Auto detect locale from browser settings
298
+ - Auto detect locale from browser settings
241
299
  - Auto persist language selection to localStorage
242
300
  - Pluralization support
243
301
  - Gender support
244
302
  - Value formatting (date, number, currency)
245
- - Support for non-React frameworks (Vue, Svelte, Angular, etc.)
246
303
 
247
304
  ## Trivial
248
305
 
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.
306
+ 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
307
 
251
- *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
308
+ *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
package/dist/index.cjs CHANGED
@@ -1,11 +1,150 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let react = require("react");
3
- //#region src/index.ts
4
- const kotori = (props) => {
3
+ //#region src/detectLanguage.ts
4
+ /**
5
+ * Detects the user's preferred language from browser settings and sets it
6
+ * on the kotori instance. Iterates through the user's full language preference
7
+ * list in order, stopping at the first match.
8
+ *
9
+ * @param instance - An object with `setLanguage` and `config`. Compatible with
10
+ * the kotori instance returned by `kotori()`, or any custom object that satisfies
11
+ * the same shape — allowing use with non-kotori setups.
12
+ * @param instance.setLanguage - `(language: BCP47LanguageTagNameWithSubTag) => void` — Called with the
13
+ * first matched language. Can be any callback — a kotori `setLanguage`, a React
14
+ * state setter, a Zustand action, etc.
15
+ * @param instance.config - Language configuration.
16
+ * @param instance.config.primary - `BCP47LanguageTagNameWithSubTag` — The primary/fallback language tag.
17
+ * @param instance.config.secondaries - `Exclude<AllTags, primary>[]` — Additional supported language tags. Cannot include the primary language.
18
+ * @param options - Detection options.
19
+ * @param options.fallbackToSubtag - `boolean` — If no exact match is found (e.g. `'zh-CN'`),
20
+ * fall back to the subtag (e.g. `'zh'`). Defaults to `true`.
21
+ * @returns `void` — Calls `setLanguage` on the first match. No-op if no match is found.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // with kotori instance
26
+ * import { kotori, detectLanguage } from 'kotori'
27
+ *
28
+ * const i18n = kotori({
29
+ * primary: 'en',
30
+ * secondaries: ['zh', 'ja', 'ms'],
31
+ * })
32
+ *
33
+ * detectLanguage(i18n)
34
+ * // browser: ['zh-CN', 'en-US'] → subtag 'zh' → setLanguage('zh') ✅
35
+ *
36
+ * detectLanguage(i18n, { fallbackToSubtag: false })
37
+ * // browser: ['zh-CN'] → no exact match → no-op, stays 'en'
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * // ✅ setLanguage type exactly matches primary + secondaries union
43
+ * detectLanguage({
44
+ * setLanguage: (lang: 'en' | 'zh' | 'ja') => setLang(lang),
45
+ * config: {
46
+ * primary: 'en',
47
+ * secondaries: ['zh', 'ja'],
48
+ * },
49
+ * })
50
+ *
51
+ * // ❌ compile error — setLanguage accepts wider type than declared
52
+ * detectLanguage({
53
+ * setLanguage: (lang: string) => setLang(lang),
54
+ * config: { primary: 'en', secondaries: ['zh'] },
55
+ * })
56
+ * ```
57
+ */
58
+ const detectLanguage = (instance, options = { fallbackToSubtag: true }) => {
59
+ const languages = [instance.config.primary, ...instance.config.secondaries];
60
+ for (const browserLang of navigator.languages) {
61
+ if (languages.includes(browserLang)) return instance.setLanguage(browserLang);
62
+ if (options.fallbackToSubtag) {
63
+ const subtag = browserLang.split("-")[0];
64
+ if (languages.includes(subtag)) return instance.setLanguage(subtag);
65
+ }
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/kotori.ts
70
+ /**
71
+ * Creates a scoped i18n instance for your app.
72
+ * Call once and export the returned utilities.
73
+ *
74
+ * @param config - Configuration options.
75
+ * @param config.primary - `BCP47LanguageTag` — The source language. Variable inference and
76
+ * secondary string validation are both driven by this language's strings.
77
+ * @param config.secondaries - `Exclude<BCP47LanguageTag, primary>[]` — Additional supported languages.
78
+ * Cannot include the primary language.
79
+ * @returns `{ d, r, useT, setLanguage, config }`
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * // locales.ts
84
+ * import { kotori } from 'kotori'
85
+ *
86
+ * export const { d, r, useT, setLanguage } = kotori({
87
+ * primary: 'en',
88
+ * secondaries: ['zh', 'ja', 'ms'],
89
+ * })
90
+ * ```
91
+ */
92
+ const kotori = (config) => {
5
93
  const listeners = /* @__PURE__ */ new Set();
6
- let language = props.primaryLanguageTag;
7
- const setLanguage = (tag) => {
8
- language = tag;
94
+ /**
95
+ * Translates a dict to the current language and interpolates variables.
96
+ *
97
+ * @param dictionary - A dict created by `d()`.
98
+ * @param args - `Record<string, string | number>` — Variable values to interpolate. Required if the dict has variables, omitted if it doesn't.
99
+ * @returns `string` — The translated and interpolated string.
100
+ *
101
+ * @example
102
+ * const Intro = () => {
103
+ * const { t } = useT()
104
+ *
105
+ * return (
106
+ * <p>{t(greeting)}</p> // ✅ no args needed
107
+ * <p>{t(intro, { name: 'John', age: 30 })}</p> // ✅ typed args
108
+ * <p>{t(intro)}</p> // ❌ compile error: missing args
109
+ * <p>{t(intro, { name: 'John', x: 30 })}</p> // ❌ compile error: unknown key 'x'
110
+ * )
111
+ * }
112
+ */
113
+ const t = (dictionary, ...args) => (dictionary().d[snapshot.language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
114
+ let snapshot = {
115
+ language: config.primary,
116
+ t
117
+ };
118
+ /**
119
+ * Updates the current language and triggers a rerender in all active
120
+ * `useT()` consumers across all pages. Safe to call outside React.
121
+ *
122
+ * @param language - `Language` — Must be one of the tags declared in
123
+ * `primary` or `secondaries`. Any other value is a compile error.
124
+ * @returns `void`
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { setLanguage } from './locales'
129
+ *
130
+ * setLanguage('zh') // ✅
131
+ * setLanguage('ms') // ✅
132
+ * setLanguage('de') // ❌ compile error: 'de' was not declared
133
+ *
134
+ * // persist and restore language selection
135
+ * setLanguage('zh')
136
+ * localStorage.setItem('lang', 'zh')
137
+ *
138
+ * // restore on app startup
139
+ * const saved = localStorage.getItem('lang')
140
+ * if (saved) setLanguage(saved as 'en')
141
+ * ```
142
+ */
143
+ const setLanguage = (language) => {
144
+ snapshot = {
145
+ language,
146
+ t: (...args) => t(...args)
147
+ };
9
148
  listeners.forEach((listener) => {
10
149
  listener();
11
150
  });
@@ -16,13 +155,131 @@ const kotori = (props) => {
16
155
  listeners.delete(listener);
17
156
  };
18
157
  };
19
- const t = (dict, ...args) => (dict().translation[language] || "").replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, key) => String(args[0]?.[key]));
20
158
  return {
159
+ config,
21
160
  setLanguage,
22
- t,
23
- dict: (translation) => () => ({ translation }),
24
- useT: () => (0, react.useSyncExternalStore)(subscribe, () => language, () => language)
161
+ /**
162
+ * Translates a dict to the current language and interpolates variables.
163
+ * For use **outside** React components route guards, axios interceptors,
164
+ * utility functions, etc.
165
+ *
166
+ * ⚠️ Do not use inside React components. The React Compiler will memoize
167
+ * the result permanently since `r` is a stable module-level reference,
168
+ * causing stale translations after a language change. Use `t` from `useT()`
169
+ * inside components instead.
170
+ *
171
+ * @param dictionary - A dict created by `d()`.
172
+ * @param args - `Record<string, string | number>` — Variable values to interpolate.
173
+ * Required if the dict has variables, omitted if it doesn't.
174
+ * @returns `string` — The translated and interpolated string in the current language.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * import { r, greeting, intro } from './locales'
179
+ *
180
+ * r(greeting) // ✅ no args needed
181
+ * r(intro, { name: 'John', age: 30 }) // ✅ typed args
182
+ * r(intro) // ❌ compile error: missing args
183
+ * r(intro, { name: 'John', x: 1 }) // ❌ compile error: unknown key 'x'
184
+ *
185
+ * // route guard
186
+ * router.beforeEach(() => {
187
+ * document.title = r(pageTitle)
188
+ * })
189
+ *
190
+ * // axios interceptor
191
+ * axios.interceptors.response.use(null, () => {
192
+ * toast.error(r(errorMessage))
193
+ * })
194
+ * ```
195
+ */
196
+ r: t,
197
+ /**
198
+ * Defines a translation unit. The primary language string drives variable
199
+ * inference — all secondary strings are validated against it at compile time.
200
+ *
201
+ * Call with an optional generic to narrow variable types beyond the default
202
+ * `string | number`. Supports TypeScript template literal types.
203
+ *
204
+ * @param dictionary - `Record<Language, string>` — One string per language.
205
+ * Variables are declared with `{{variableName}}` syntax. Spaces inside braces
206
+ * are trimmed — `{{ name }}` and `{{name}}` are equivalent.
207
+ * Secondary strings must use exactly the same variable names as the primary.
208
+ * @returns A function optionally accepting a generic to narrow variable types,
209
+ * which returns the translation unit.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * // no variables
214
+ * const greeting = d({ en: 'Hello', zh: '你好', ja: 'こんにちは', ms: 'Helo' })()
215
+ *
216
+ * // variables — inferred as Record<'name' | 'age', string | number> by default
217
+ * const intro = d({
218
+ * en: 'My name is {{name}}, I am {{age}} years old.',
219
+ * zh: '我叫{{name}},我今年{{age}}岁了。',
220
+ * ja: '私の名前は{{name}}で、{{age}}歳です。',
221
+ * ms: 'Nama saya {{name}}, saya berumur {{age}} tahun.',
222
+ * })()
223
+ *
224
+ * // narrowed variable types via generic
225
+ * const clock = d({
226
+ * en: 'Current time: {{hour}}:{{minute}}',
227
+ * zh: '现在时间:{{hour}}:{{minute}}',
228
+ * ja: '現在時刻:{{hour}}:{{minute}}',
229
+ * ms: 'Masa semasa: {{hour}}:{{minute}}',
230
+ * })<{ hour: number; minute: number }>()
231
+ *
232
+ * // TypeScript template literal types work too
233
+ * const lastLogin = d({
234
+ * en: 'Last login: {{date}}',
235
+ * zh: '上次登录:{{date}}',
236
+ * ja: '最終ログイン:{{date}}',
237
+ * ms: 'Log masuk terakhir: {{date}}',
238
+ * })<{ date: `${number}-${number}-${number}` }>()
239
+ *
240
+ * // ❌ compile error — 'ja' translation missing
241
+ * const bad1 = d({ en: 'Hello', zh: '你好', ms: 'Helo' })()
242
+ *
243
+ * // ❌ compile error — secondary string has wrong variable name
244
+ * const bad2 = d({ en: 'Hello {{name}}', zh: '你好 {{naam}}', ja: 'こんにちは {{name}}', ms: 'Helo {{name}}' })()
245
+ *
246
+ * // ❌ compile error — secondary string missing a variable
247
+ * const bad3 = d({ en: '{{x}} {{y}}', zh: '{{x}}', ja: '{{x}} {{y}}', ms: '{{x}} {{y}}' })()
248
+ * ```
249
+ */
250
+ d: (dictionary) => () => ({ d: dictionary }),
251
+ /**
252
+ * React hook. Subscribes the component to language changes.
253
+ * Returns a snapshot containing the current `language` and a
254
+ * React Compiler-safe `t` function.
255
+ *
256
+ * Call in every component that renders translated strings.
257
+ * When `setLanguage` is called anywhere, all `useT()` consumers rerender.
258
+ *
259
+ * @returns `{ language: Language, t: TranslateFunction }`
260
+ * @returns `language` - `Language` — The current active language tag as a reactive value.
261
+ * @returns `t` - `TranslateFunction` — React Compiler-safe translate function. Use this inside components instead of the instance-level `t`.
262
+ *
263
+ * @example
264
+ * ```tsx
265
+ * import { useT, intro, time } from './locales'
266
+ *
267
+ * const Page = () => {
268
+ * const { t, language } = useT()
269
+ *
270
+ * return (
271
+ * <>
272
+ * <p>{t(intro, { name: 'John', age: 30 })}</p>
273
+ * <p>{t(time, { hour: 12, minute: 0 })}</p>
274
+ * <p>Current language: {language}</p>
275
+ * </>
276
+ * )
277
+ * }
278
+ * ```
279
+ */
280
+ useT: () => (0, react.useSyncExternalStore)(subscribe, () => snapshot, () => snapshot)
25
281
  };
26
282
  };
27
283
  //#endregion
284
+ exports.detectLanguage = detectLanguage;
28
285
  exports.kotori = kotori;