kotori 0.0.0 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Acid Coder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,7 +1,227 @@
1
- # kotori
1
+ # Kotori
2
2
 
3
- Strongly-typed and composable internationalization library for React
4
- no codegen
5
- no json
3
+ Strongly-typed, modular i18n for React. Variables are inferred directly from your strings — no codegen, no JSON, no schema files.
6
4
 
7
- building...
5
+ ```ts
6
+ const { dict } = kotori({
7
+ primaryLanguageTag: 'en',
8
+ secondaryLanguageTags: ['zh', 'ja', 'ms'],
9
+ })
10
+
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'
22
+ ```
23
+
24
+ - No codegen
25
+ - No JSON
26
+ - No dependencies
27
+ - 0.39kb gzipped
28
+ - Modular and tree-shakeable
29
+ - Language change in one page rerenders all pages
30
+ - Variables typed and inferred from string literals
31
+ - Translation keys are typed — no more string typos
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm i kotori
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ **utils.ts**
42
+
43
+ ```ts
44
+ import { kotori } from './kotori'
45
+
46
+ export const { createTranslations, dict } = kotori({
47
+ primaryLanguageTag: 'en',
48
+ secondaryLanguageTags: ['zh', 'ja', 'ms'],
49
+ })
50
+ ```
51
+
52
+ **page1.tsx**
53
+
54
+ ```tsx
55
+ import { createTranslations, dict } from './utils'
56
+
57
+ const intro = dict({
58
+ en: 'my name is {{name}}, I am {{age}} years old.',
59
+ zh: '我叫{{name}},我今年{{age}}岁了。',
60
+ ja: '私の名前は{{name}}で、{{age}}歳です。',
61
+ ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
62
+ })
63
+
64
+ const time = dict({
65
+ en: 'time {{time}}',
66
+ zh: '时间 {{time}}',
67
+ ja: '時間 {{time}}',
68
+ ms: 'waktu {{time}}',
69
+ // optional: type your arguments, by default it's `Record<string, string>`
70
+ })<{ time: `${number}:${number}:${number}` }>
71
+
72
+ const { useTranslations } = createTranslations({
73
+ intro,
74
+ time,
75
+ })
76
+
77
+
78
+ export const Page1 = () => {
79
+ const { t, setLanguage } = useTranslations()
80
+ return (
81
+ <>
82
+ <select
83
+ name="language"
84
+ onChange={(e) => setLanguage(e.target.value as 'en')}
85
+ >
86
+ <option value="en">English</option>
87
+ <option value="zh">Chinese</option>
88
+ <option value="ja">Japanese</option>
89
+ <option value="ms">Malay</option>
90
+ </select>
91
+ <p>{t('intro', { name: 'John', age: 30 })}</p>
92
+ <p>{t('time', { time: '12:00:00' })}</p>
93
+ </>
94
+ )
95
+ }
96
+ ```
97
+
98
+ **page2.tsx**
99
+
100
+ ```tsx
101
+ import { createTranslations, dict } from './utils'
102
+
103
+ const weather = dict({
104
+ en: 'The weather in {{city}} has {{humidity}}% humidity.',
105
+ zh: '{{city}}的天气湿度为{{humidity}}%。',
106
+ ja: '{{city}}の湿度は{{humidity}}%です。',
107
+ ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
108
+ })<{ city: string; humidity: number }>
109
+
110
+ const score = dict({
111
+ en: 'Your score is {{score}} out of {{total}}.',
112
+ zh: '你的得分是 {{total}} 分中的 {{score}} 分。',
113
+ ja: 'あなたのスコアは {{total}} 点中 {{score}} 点です。',
114
+ ms: 'Markah anda ialah {{score}} daripada {{total}}.',
115
+ })<{ score: number; total: number }>
116
+
117
+ const lastLogin = dict({
118
+ en: 'Last login: {{date}} at {{time}}',
119
+ zh: '上次登录:{{date}} {{time}}',
120
+ ja: '最終ログイン:{{date}} {{time}}',
121
+ ms: 'Log masuk terakhir: {{date}} pada {{time}}',
122
+ })<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
123
+
124
+ const { useTranslations } = createTranslations({
125
+ greeting,
126
+ score,
127
+ lastLogin,
128
+ })
129
+
130
+ export const Page2 = () => {
131
+ const { t, setLanguage } = useTranslations()
132
+ return (
133
+ <>
134
+ <select
135
+ name="language"
136
+ onChange={(e) => setLanguage(e.target.value as 'en')}
137
+ >
138
+ <option value="en">English</option>
139
+ <option value="zh">Chinese</option>
140
+ <option value="ja">Japanese</option>
141
+ <option value="ms">Malay</option>
142
+ </select>
143
+ <p>{t('weather', { city: 'Kuala Lumpur', humidity: 80 })}</p>
144
+ <p>{t('score', { score: 87, total: 100 })}</p>
145
+ <p>{t('lastLogin', { date: '2024-04-24', time: '09:30' })}</p>
146
+ </>
147
+ )
148
+ }
149
+ ```
150
+
151
+ ## How It Works
152
+
153
+ ### One `kotori` instance per app
154
+
155
+ `kotori` holds the language state. All `createTranslations` calls share that state — changing the language anywhere rerenders everywhere.
156
+
157
+ ### One `createTranslations` per page/component/feature
158
+
159
+ Translations are colocated with the component that uses them. Bundlers naturally code-split them, so each page only loads what it needs.
160
+
161
+ ### Variables are inferred from string literals
162
+
163
+ kotori parses `{{variable}}` at the type level. No separate type definitions needed — the string *is* the schema.
164
+
165
+ ```ts
166
+ // primary string drives the contract
167
+ const greeting = dict({ en: 'Hi {{name}}', zh: '你好 {{name}}' })
168
+ // ^^^^^^ — inferred as required arg
169
+
170
+ // secondary strings are validated against it
171
+ const mismatch = dict({ en: 'Hi {{name}}', zh: '你好 {{other}}' })
172
+ // ^^^^^^^ — compile error
173
+ ```
174
+
175
+ ### Custom argument types
176
+
177
+ By default, variables are typed as `string`. Pass a generic to narrow them:
178
+
179
+ ```ts
180
+ const time = dict({ en: '{{hour}}:{{minute}}' })<{
181
+ hour: number
182
+ minute: number
183
+ }>
184
+ ```
185
+
186
+ ## API
187
+
188
+ ### `kotori(options)`
189
+
190
+ Creates a scoped i18n instance.
191
+
192
+ | option | type | description |
193
+ | --- | --- | --- |
194
+ | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
195
+ | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
196
+
197
+ Returns `{ dict, createTranslations }`.
198
+
199
+ ### `dict(translations)<argsType?>`
200
+
201
+ Defines a translation unit. Takes one string per language. Optionally takes a generic to type the interpolated variables.
202
+
203
+ Returns `() => { translations: Record<string, string> }`.
204
+
205
+ ### `createTranslations(dicts)`
206
+
207
+ Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
208
+
209
+ ### `useTranslations()`
210
+
211
+ React hook. Returns `{ t, getLanguage, setLanguage }`.
212
+
213
+ | return | description |
214
+ | --- | --- |
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. |
216
+ | `getLanguage()` | Returns the current language tag. |
217
+ | `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
218
+
219
+ ## Language Tags
220
+
221
+ 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-Hans`) are accepted and validated at the type level.
222
+
223
+ ## Trivial
224
+
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.
226
+
227
+ *Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
package/dist/index.cjs CHANGED
@@ -1,9 +1,51 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/isEven.ts
3
- const isEven = (v) => !(v & 1);
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
+
4
50
  //#endregion
5
- //#region src/isOdd.ts
6
- const isOdd = (v) => !isEven(v);
7
- //#endregion
8
- exports.isEven = isEven;
9
- exports.isOdd = isOdd;
51
+ exports.kotori = kotori;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,31 @@
1
- //#region src/isOdd.d.ts
2
- declare const isOdd: (v: number) => boolean;
1
+ //#region node_modules/bcp47-language-tags/dist/zh.d.ts
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/isEven.d.ts
5
- declare const isEven: (v: number) => boolean;
4
+ //#region src/index.d.ts
5
+ type Tags = BCP47LanguageTagName;
6
+ type SubTags = BCP47LanguageTagName extends `${infer SubTag}-${string}` ? SubTag : never;
7
+ type AllTags = Tags | SubTags;
8
+ type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
9
+ type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
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[];
14
+ }) => {
15
+ 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>>() => Readonly<{
16
+ translation: typeof translation;
17
+ [_args]?: ArgsType;
18
+ }>;
19
+ createTranslations: <const DictCallbacks extends Record<string, () => Readonly<{
20
+ translation: Record<PrimaryTag | SecondaryTags, string>;
21
+ [_args]?: Record<string, string | number>;
22
+ }>>>(dictCallbacks: DictCallbacks) => {
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;
25
+ getLanguage: () => PrimaryTag | SecondaryTags;
26
+ setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
27
+ };
28
+ };
29
+ };
6
30
  //#endregion
7
- export { isEven, isOdd };
31
+ export { AllTags, SubTags, Tags, kotori };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,31 @@
1
- //#region src/isOdd.d.ts
2
- declare const isOdd: (v: number) => boolean;
1
+ //#region node_modules/bcp47-language-tags/dist/zh.d.ts
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/isEven.d.ts
5
- declare const isEven: (v: number) => boolean;
4
+ //#region src/index.d.ts
5
+ type Tags = BCP47LanguageTagName;
6
+ type SubTags = BCP47LanguageTagName extends `${infer SubTag}-${string}` ? SubTag : never;
7
+ type AllTags = Tags | SubTags;
8
+ type Trim<T extends string> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer L} ` ? Trim<L> : T;
9
+ type ExtractVariables<T extends string> = T extends `${string}{{${infer P}}}${infer Q}` ? Trim<P> | ExtractVariables<Q> : never;
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[];
14
+ }) => {
15
+ 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>>() => Readonly<{
16
+ translation: typeof translation;
17
+ [_args]?: ArgsType;
18
+ }>;
19
+ createTranslations: <const DictCallbacks extends Record<string, () => Readonly<{
20
+ translation: Record<PrimaryTag | SecondaryTags, string>;
21
+ [_args]?: Record<string, string | number>;
22
+ }>>>(dictCallbacks: DictCallbacks) => {
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;
25
+ getLanguage: () => PrimaryTag | SecondaryTags;
26
+ setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
27
+ };
28
+ };
29
+ };
6
30
  //#endregion
7
- export { isEven, isOdd };
31
+ export { AllTags, SubTags, Tags, kotori };
package/dist/index.mjs CHANGED
@@ -1,7 +1,50 @@
1
- //#region src/isEven.ts
2
- const isEven = (v) => !(v & 1);
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
+
3
49
  //#endregion
4
- //#region src/isOdd.ts
5
- const isOdd = (v) => !isEven(v);
6
- //#endregion
7
- export { isEven, isOdd };
50
+ export { kotori };
package/package.json CHANGED
@@ -1,55 +1,65 @@
1
- {
2
- "name": "kotori",
3
- "description": "Strongly-typed and composable internationalization library for React",
4
- "version": "0.0.0",
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
- },
12
- "files": [
13
- "dist"
14
- ],
15
- "lint-staged": {
16
- "*": [
17
- "npm run lint"
18
- ]
19
- },
20
- "type": "module",
21
- "main": "./dist/index.cjs",
22
- "types": "./dist/index.d.cts",
23
- "exports": {
24
- ".": {
25
- "require": {
26
- "types": "./dist/index.d.cts",
27
- "default": "./dist/index.cjs"
28
- },
29
- "import": {
30
- "types": "./dist/index.d.mts",
31
- "default": "./dist/index.mjs"
32
- }
33
- }
34
- },
35
- "devDependencies": {
36
- "@biomejs/biome": "^2.4.12",
37
- "@types/node": "^25.6.0",
38
- "@vitest/coverage-v8": "^4.1.5",
39
- "husky": "^9.1.7",
40
- "lint-staged": "^16.4.0",
41
- "tsdown": "^0.21.10",
42
- "tsx": "^4.21.0",
43
- "typescript": "^6.0.3",
44
- "vitest": "^4.1.5"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/???/???.git"
49
- },
50
- "bugs": {
51
- "url": "https://github.com/???/???/issues"
52
- },
53
- "author": "???",
54
- "license": "???"
55
- }
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
+ }