kotori 0.0.1 → 0.0.6

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
@@ -3,17 +3,28 @@
3
3
  Strongly-typed, modular i18n for React. Variables are inferred directly from your strings — no codegen, no JSON, no schema files.
4
4
 
5
5
  ```ts
6
- const intro = dict({ en: 'Hello {{name}}', zh: '你好 {{name}}' })
6
+ const { dict } = kotori({
7
+ primaryLanguageTag: 'en',
8
+ secondaryLanguageTags: ['zh', 'ja', 'ms'],
9
+ })
7
10
 
8
- t('intro', { name: 'John' }) // ✅ typed
9
- t('intro') // compile error: missing { name }
10
- t('intro', { nama: 'John' }) // compile error: unknown key 'nama'
11
+ // compile error: missing japanese translation
12
+ const intro = dict({
13
+ en: 'Hello {{name}}, is it {{time}} now?', // base string drives the type contract
14
+ zh: '你好,现在是 {{time}} 吗?', // ❌ compile error: missing key 'nam'
15
+ ms: 'Hai {{nam}}, adakah pukul {{time}} sekarang?' // ❌ compile error: unknown key 'nam'
16
+ })<{name: string; time: `${number}:${number}`}> // optional: type your arguments, by default it's `Record<'name'|'time', string>` in this example
17
+
18
+ t('intro', { name: 'John', time: '12:25' }) // ✅
19
+ t('intro', { time: '12:25' }) // ❌ compile error: missing { name }
20
+ t('intro', { nama: 'John', time: '12:25' }) // ❌ compile error: unknown key 'nama'
21
+ t('intro', { name: 'John', time: '12-00' }) // ❌ compile error: invalid format for 'time'
11
22
  ```
12
23
 
13
24
  - No codegen
14
25
  - No JSON
15
26
  - No dependencies
16
- - 0.31kb gzipped
27
+ - 0.39kb gzipped
17
28
  - Modular and tree-shakeable
18
29
  - Language change in one page rerenders all pages
19
30
  - Variables typed and inferred from string literals
@@ -55,7 +66,7 @@ const time = dict({
55
66
  zh: '时间 {{time}}',
56
67
  ja: '時間 {{time}}',
57
68
  ms: 'waktu {{time}}',
58
- // type your arguments, by default it's `Record<string, string>`
69
+ // optional: type your arguments, by default it's `Record<time, string>` in this example
59
70
  })<{ time: `${number}:${number}:${number}` }>
60
71
 
61
72
  const { useTranslations } = createTranslations({
@@ -139,11 +150,13 @@ export const Page2 = () => {
139
150
 
140
151
  ## How It Works
141
152
 
153
+ ![how kotori works](image.png)
154
+
142
155
  ### One `kotori` instance per app
143
156
 
144
157
  `kotori` holds the language state. All `createTranslations` calls share that state — changing the language anywhere rerenders everywhere.
145
158
 
146
- ### One `createTranslations` per page/feature
159
+ ### One `createTranslations` per page/component/feature
147
160
 
148
161
  Translations are colocated with the component that uses them. Bundlers naturally code-split them, so each page only loads what it needs.
149
162
 
@@ -179,16 +192,18 @@ const time = dict({ en: '{{hour}}:{{minute}}' })<{
179
192
  Creates a scoped i18n instance.
180
193
 
181
194
  | option | type | description |
182
- |---|---|---|
195
+ | --- | --- | --- |
183
196
  | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
184
197
  | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
185
198
 
186
199
  Returns `{ dict, createTranslations }`.
187
200
 
188
- ### `dict(translations)(argsType?)`
201
+ ### `dict(translations)<argsType?>`
189
202
 
190
203
  Defines a translation unit. Takes one string per language. Optionally takes a generic to type the interpolated variables.
191
204
 
205
+ Returns `() => { translations: Record<string, string> }`.
206
+
192
207
  ### `createTranslations(dicts)`
193
208
 
194
209
  Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
@@ -198,7 +213,7 @@ Registers a set of dicts and returns `{ useTranslations }`. Call once per page o
198
213
  React hook. Returns `{ t, getLanguage, setLanguage }`.
199
214
 
200
215
  | return | description |
201
- |---|---|
216
+ | --- | --- |
202
217
  | `t(key, args?)` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. |
203
218
  | `getLanguage()` | Returns the current language tag. |
204
219
  | `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
@@ -211,4 +226,4 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
211
226
 
212
227
  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.
213
228
 
214
- *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
229
+ *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
package/dist/index.cjs CHANGED
@@ -1 +1,51 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`react`);const t=t=>{let n=new Set,r=t.primaryLanguageTag,i=new Map;return{dict:e=>()=>({translation:e}),createTranslations:t=>{let a=Symbol();return i.set(a,{getLanguage:()=>r,setLanguage:e=>{r=e,i.forEach((e,t)=>{i.set(t,{...e})}),n.forEach(e=>{e()})},t:(e,...n)=>{let i=t[e]?.().translation[r];if(i){for(let e in n[0])i=i.replace(RegExp(`\\{\\{\\s*${e}\\s*\\}\\}`,`g`),()=>String(n[0]?.[e]));return i}}}),{useTranslations:()=>(0,e.useSyncExternalStore)(e=>(n.add(e),()=>n.delete(e)),()=>i.get(a))}}}};exports.kotori=t;
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let react = require("react");
3
+
4
+ //#region src/index.ts
5
+ const kotori = (props) => {
6
+ const listeners = /* @__PURE__ */ new Set();
7
+ let languageTag = props.primaryLanguageTag;
8
+ const snapshots = /* @__PURE__ */ new Map();
9
+ const languageTagMethod = {
10
+ getLanguage: () => languageTag,
11
+ setLanguage: (tag) => {
12
+ languageTag = tag;
13
+ snapshots.forEach((snapshot, key) => {
14
+ snapshots.set(key, { ...snapshot });
15
+ });
16
+ listeners.forEach((listener) => {
17
+ listener();
18
+ });
19
+ }
20
+ };
21
+ return {
22
+ dict: (translation) => () => ({ translation }),
23
+ createTranslations: (dictCallbacks) => {
24
+ const s = Symbol();
25
+ let refCount = 0;
26
+ const snapshot = {
27
+ ...languageTagMethod,
28
+ t: (key, ...args) => {
29
+ let locale = dictCallbacks[key]?.().translation[languageTag];
30
+ if (!locale) return;
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
+ snapshots.set(s, snapshot);
36
+ return { useTranslations: () => (0, react.useSyncExternalStore)((listener) => {
37
+ if (refCount === 0) snapshots.set(s, snapshot);
38
+ refCount++;
39
+ listeners.add(listener);
40
+ return () => {
41
+ refCount--;
42
+ if (refCount === 0) snapshots.delete(s);
43
+ listeners.delete(listener);
44
+ };
45
+ }, () => snapshots.get(s)) };
46
+ }
47
+ };
48
+ };
49
+
50
+ //#endregion
51
+ exports.kotori = kotori;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  //#region node_modules/bcp47-language-tags/dist/zh.d.ts
2
2
  type BCP47LanguageTagName = "zh-CN" | "zh-TW" | "zh-HK" | "zh-MO" | "zh-SG" | "zh-CHS" | "zh-CHT" | "en-US" | "en-GB" | "en-CA" | "en-AU" | "en-IN" | "en-ZA" | "en-NZ" | "en-IE" | "en-PH" | "en-ZW" | "en-BZ" | "en-CB" | "en-JM" | "en-TT" | "hi-IN" | "es-ES" | "es-MX" | "es-AR" | "es-CO" | "es-PE" | "es-VE" | "es-CL" | "es-EC" | "es-GT" | "es-CU" | "es-BO" | "es-DO" | "es-HN" | "es-PY" | "es-SV" | "es-NI" | "es-PR" | "es-UY" | "es-PA" | "es-CR" | "ar-EG" | "ar-SA" | "ar-DZ" | "ar-MA" | "ar-IQ" | "ar-SD" | "ar-YE" | "ar-SY" | "ar-TN" | "ar-LY" | "ar-JO" | "ar-LB" | "ar-KW" | "ar-AE" | "ar-BH" | "ar-QA" | "ar-OM" | "pt-BR" | "pt-PT" | "ru-RU" | "ja-JP" | "de-DE" | "de-AT" | "de-CH" | "fr-FR" | "fr-CA" | "fr-BE" | "fr-CH" | "fr-LU" | "fr-MC" | "ko-KR" | "it-IT" | "it-CH" | "tr-TR" | "th-TH" | "el-GR" | "cs-CZ" | "sv-SE" | "sv-FI" | "hu-HU" | "fi-FI" | "da-DK" | "nb-NO" | "nn-NO" | "he-IL" | "id-ID" | "ms-MY" | "ms-BN" | "ro-RO" | "bg-BG" | "uk-UA" | "sk-SK" | "sl-SI" | "hr-HR" | "ca-ES" | "lt-LT" | "lv-LV" | "et-EE" | "sq-AL" | "mk-MK" | "be-BY" | "is-IS" | "gl-ES" | "eu-ES" | "af-ZA" | "sw-KE" | "ta-IN" | "te-IN" | "kn-IN" | "mr-IN" | "gu-IN" | "pa-IN" | "kok-IN" | "sa-IN" | "ur-PK" | "fa-IR" | "syr-SY" | "div-MV" | "ka-GE";
3
3
  //#endregion
4
- //#region src/kotori.d.ts
4
+ //#region src/index.d.ts
5
5
  type Tags = BCP47LanguageTagName;
6
6
  type SubTags = BCP47LanguageTagName extends `${infer SubTag}-${string}` ? SubTag : never;
7
7
  type AllTags = Tags | SubTags;
@@ -21,11 +21,11 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
21
  [_args]?: Record<string, string | number>;
22
22
  }>>>(dictCallbacks: DictCallbacks) => {
23
23
  useTranslations: () => {
24
+ t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => Record<PrimaryTag | SecondaryTags, string>[PrimaryTag | SecondaryTags] | undefined;
24
25
  getLanguage: () => PrimaryTag | SecondaryTags;
25
26
  setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
26
- t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => Record<PrimaryTag | SecondaryTags, string>[PrimaryTag | SecondaryTags] | undefined;
27
27
  };
28
28
  };
29
29
  };
30
30
  //#endregion
31
- export { AllTags, ExtractVariables, SubTags, Tags, kotori };
31
+ export { AllTags, SubTags, Tags, kotori };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  //#region node_modules/bcp47-language-tags/dist/zh.d.ts
2
2
  type BCP47LanguageTagName = "zh-CN" | "zh-TW" | "zh-HK" | "zh-MO" | "zh-SG" | "zh-CHS" | "zh-CHT" | "en-US" | "en-GB" | "en-CA" | "en-AU" | "en-IN" | "en-ZA" | "en-NZ" | "en-IE" | "en-PH" | "en-ZW" | "en-BZ" | "en-CB" | "en-JM" | "en-TT" | "hi-IN" | "es-ES" | "es-MX" | "es-AR" | "es-CO" | "es-PE" | "es-VE" | "es-CL" | "es-EC" | "es-GT" | "es-CU" | "es-BO" | "es-DO" | "es-HN" | "es-PY" | "es-SV" | "es-NI" | "es-PR" | "es-UY" | "es-PA" | "es-CR" | "ar-EG" | "ar-SA" | "ar-DZ" | "ar-MA" | "ar-IQ" | "ar-SD" | "ar-YE" | "ar-SY" | "ar-TN" | "ar-LY" | "ar-JO" | "ar-LB" | "ar-KW" | "ar-AE" | "ar-BH" | "ar-QA" | "ar-OM" | "pt-BR" | "pt-PT" | "ru-RU" | "ja-JP" | "de-DE" | "de-AT" | "de-CH" | "fr-FR" | "fr-CA" | "fr-BE" | "fr-CH" | "fr-LU" | "fr-MC" | "ko-KR" | "it-IT" | "it-CH" | "tr-TR" | "th-TH" | "el-GR" | "cs-CZ" | "sv-SE" | "sv-FI" | "hu-HU" | "fi-FI" | "da-DK" | "nb-NO" | "nn-NO" | "he-IL" | "id-ID" | "ms-MY" | "ms-BN" | "ro-RO" | "bg-BG" | "uk-UA" | "sk-SK" | "sl-SI" | "hr-HR" | "ca-ES" | "lt-LT" | "lv-LV" | "et-EE" | "sq-AL" | "mk-MK" | "be-BY" | "is-IS" | "gl-ES" | "eu-ES" | "af-ZA" | "sw-KE" | "ta-IN" | "te-IN" | "kn-IN" | "mr-IN" | "gu-IN" | "pa-IN" | "kok-IN" | "sa-IN" | "ur-PK" | "fa-IR" | "syr-SY" | "div-MV" | "ka-GE";
3
3
  //#endregion
4
- //#region src/kotori.d.ts
4
+ //#region src/index.d.ts
5
5
  type Tags = BCP47LanguageTagName;
6
6
  type SubTags = BCP47LanguageTagName extends `${infer SubTag}-${string}` ? SubTag : never;
7
7
  type AllTags = Tags | SubTags;
@@ -21,11 +21,11 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
21
  [_args]?: Record<string, string | number>;
22
22
  }>>>(dictCallbacks: DictCallbacks) => {
23
23
  useTranslations: () => {
24
+ t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => Record<PrimaryTag | SecondaryTags, string>[PrimaryTag | SecondaryTags] | undefined;
24
25
  getLanguage: () => PrimaryTag | SecondaryTags;
25
26
  setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
26
- t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => Record<PrimaryTag | SecondaryTags, string>[PrimaryTag | SecondaryTags] | undefined;
27
27
  };
28
28
  };
29
29
  };
30
30
  //#endregion
31
- export { AllTags, ExtractVariables, SubTags, Tags, kotori };
31
+ export { AllTags, SubTags, Tags, kotori };
package/dist/index.mjs CHANGED
@@ -1 +1,50 @@
1
- import{useSyncExternalStore as e}from"react";const t=t=>{let n=new Set,r=t.primaryLanguageTag,i=new Map;return{dict:e=>()=>({translation:e}),createTranslations:t=>{let a=Symbol();return i.set(a,{getLanguage:()=>r,setLanguage:e=>{r=e,i.forEach((e,t)=>{i.set(t,{...e})}),n.forEach(e=>{e()})},t:(e,...n)=>{let i=t[e]?.().translation[r];if(i){for(let e in n[0])i=i.replace(RegExp(`\\{\\{\\s*${e}\\s*\\}\\}`,`g`),()=>String(n[0]?.[e]));return i}}}),{useTranslations:()=>e(e=>(n.add(e),()=>n.delete(e)),()=>i.get(a))}}}};export{t as kotori};
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ //#region src/index.ts
4
+ const kotori = (props) => {
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ let languageTag = props.primaryLanguageTag;
7
+ const snapshots = /* @__PURE__ */ new Map();
8
+ const languageTagMethod = {
9
+ getLanguage: () => languageTag,
10
+ setLanguage: (tag) => {
11
+ languageTag = tag;
12
+ snapshots.forEach((snapshot, key) => {
13
+ snapshots.set(key, { ...snapshot });
14
+ });
15
+ listeners.forEach((listener) => {
16
+ listener();
17
+ });
18
+ }
19
+ };
20
+ return {
21
+ dict: (translation) => () => ({ translation }),
22
+ createTranslations: (dictCallbacks) => {
23
+ const s = Symbol();
24
+ let refCount = 0;
25
+ const snapshot = {
26
+ ...languageTagMethod,
27
+ t: (key, ...args) => {
28
+ let locale = dictCallbacks[key]?.().translation[languageTag];
29
+ if (!locale) return;
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
+ snapshots.set(s, snapshot);
35
+ return { useTranslations: () => useSyncExternalStore((listener) => {
36
+ if (refCount === 0) snapshots.set(s, snapshot);
37
+ refCount++;
38
+ listeners.add(listener);
39
+ return () => {
40
+ refCount--;
41
+ if (refCount === 0) snapshots.delete(s);
42
+ listeners.delete(listener);
43
+ };
44
+ }, () => snapshots.get(s)) };
45
+ }
46
+ };
47
+ };
48
+
49
+ //#endregion
50
+ export { kotori };
package/package.json CHANGED
@@ -1,65 +1,72 @@
1
- {
2
- "name": "kotori",
3
- "description": "Strongly-typed and composable internationalization library for React",
4
- "version": "0.0.1",
5
- "scripts": {
6
- "setup": "rm -rf node_modules && npm i && git init && husky",
7
- "prepublishOnly": "npm run build",
8
- "build": "tsdown",
9
- "test": "vitest",
10
- "lint": "npx @biomejs/biome check --write",
11
- "dev": "vite --host"
12
- },
13
- "files": [
14
- "dist"
15
- ],
16
- "lint-staged": {
17
- "*": [
18
- "npm run lint"
19
- ]
20
- },
21
- "type": "module",
22
- "main": "./dist/index.cjs",
23
- "types": "./dist/index.d.cts",
24
- "exports": {
25
- ".": {
26
- "require": {
27
- "types": "./dist/index.d.cts",
28
- "default": "./dist/index.cjs"
29
- },
30
- "import": {
31
- "types": "./dist/index.d.mts",
32
- "default": "./dist/index.mjs"
33
- }
34
- }
35
- },
36
- "devDependencies": {
37
- "@biomejs/biome": "^2.4.12",
38
- "@types/node": "^25.6.0",
39
- "@types/react": "^19.2.14",
40
- "@types/react-dom": "^19.2.3",
41
- "@vitejs/plugin-react": "^6.0.1",
42
- "@vitest/coverage-v8": "^4.1.5",
43
- "bcp47-language-tags": "^1.1.0",
44
- "husky": "^9.1.7",
45
- "lint-staged": "^16.4.0",
46
- "react": "^19.2.5",
47
- "react-dom": "^19.2.5",
48
- "tsdown": "^0.21.10",
49
- "tsx": "^4.21.0",
50
- "typescript": "^6.0.3",
51
- "vitest": "^4.1.5"
52
- },
53
- "repository": {
54
- "type": "git",
55
- "url": "git+https://github.com/tylim88/kotori.git"
56
- },
57
- "bugs": {
58
- "url": "https://github.com/tylim88/kotori/issues"
59
- },
60
- "author": "tylim88",
61
- "license": "MIT",
62
- "peerDependencies": {
63
- "react": ">=19.2.5"
64
- }
65
- }
1
+ {
2
+ "name": "kotori",
3
+ "description": "Strongly-typed and composable internationalization library for React",
4
+ "version": "0.0.6",
5
+ "scripts": {
6
+ "setup": "rm -rf node_modules && npm i && git init && husky",
7
+ "prepublishOnly": "npm run build",
8
+ "build": "tsdown",
9
+ "test": "vitest",
10
+ "lint": "npx @biomejs/biome check --write",
11
+ "dev": "vite --host"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "lint-staged": {
17
+ "*": [
18
+ "npm run lint"
19
+ ]
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.cjs",
23
+ "types": "./dist/index.d.cts",
24
+ "exports": {
25
+ ".": {
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ },
30
+ "import": {
31
+ "types": "./dist/index.d.mts",
32
+ "default": "./dist/index.mjs"
33
+ }
34
+ }
35
+ },
36
+ "keywords": [
37
+ "i18n",
38
+ "internationalization",
39
+ "react",
40
+ "typescript",
41
+ "strongly-typed"
42
+ ],
43
+ "devDependencies": {
44
+ "@biomejs/biome": "^2.4.12",
45
+ "@types/node": "^25.6.0",
46
+ "@types/react": "^19.2.14",
47
+ "@types/react-dom": "^19.2.3",
48
+ "@vitejs/plugin-react": "^6.0.1",
49
+ "@vitest/coverage-v8": "^4.1.5",
50
+ "bcp47-language-tags": "^1.1.0",
51
+ "husky": "^9.1.7",
52
+ "lint-staged": "^16.4.0",
53
+ "react": "^19.2.5",
54
+ "react-dom": "^19.2.5",
55
+ "tsdown": "^0.21.10",
56
+ "tsx": "^4.21.0",
57
+ "typescript": "^6.0.3",
58
+ "vitest": "^4.1.5"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/tylim88/kotori.git"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/tylim88/kotori/issues"
66
+ },
67
+ "author": "tylim88",
68
+ "license": "MIT",
69
+ "peerDependencies": {
70
+ "react": ">=19.2.5"
71
+ }
72
+ }