kotori 0.0.1 → 0.0.5

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<string, string>`
59
70
  })<{ time: `${number}:${number}:${number}` }>
60
71
 
61
72
  const { useTranslations } = createTranslations({
@@ -143,7 +154,7 @@ export const Page2 = () => {
143
154
 
144
155
  `kotori` holds the language state. All `createTranslations` calls share that state — changing the language anywhere rerenders everywhere.
145
156
 
146
- ### One `createTranslations` per page/feature
157
+ ### One `createTranslations` per page/component/feature
147
158
 
148
159
  Translations are colocated with the component that uses them. Bundlers naturally code-split them, so each page only loads what it needs.
149
160
 
@@ -179,16 +190,18 @@ const time = dict({ en: '{{hour}}:{{minute}}' })<{
179
190
  Creates a scoped i18n instance.
180
191
 
181
192
  | option | type | description |
182
- |---|---|---|
193
+ | --- | --- | --- |
183
194
  | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
184
195
  | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
185
196
 
186
197
  Returns `{ dict, createTranslations }`.
187
198
 
188
- ### `dict(translations)(argsType?)`
199
+ ### `dict(translations)<argsType?>`
189
200
 
190
201
  Defines a translation unit. Takes one string per language. Optionally takes a generic to type the interpolated variables.
191
202
 
203
+ Returns `() => { translations: Record<string, string> }`.
204
+
192
205
  ### `createTranslations(dicts)`
193
206
 
194
207
  Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
@@ -198,7 +211,7 @@ Registers a set of dicts and returns `{ useTranslations }`. Call once per page o
198
211
  React hook. Returns `{ t, getLanguage, setLanguage }`.
199
212
 
200
213
  | return | description |
201
- |---|---|
214
+ | --- | --- |
202
215
  | `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
216
  | `getLanguage()` | Returns the current language tag. |
204
217
  | `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
@@ -211,4 +224,4 @@ kotori uses [BCP 47](https://www.iana.org/assignments/language-subtag-registry/l
211
224
 
212
225
  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
226
 
214
- *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
227
+ *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,65 @@
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.5",
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
+ }