kotori 0.0.0 → 0.0.1
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 +21 -0
- package/README.md +212 -5
- package/dist/index.cjs +1 -9
- package/dist/index.d.cts +29 -5
- package/dist/index.d.mts +29 -5
- package/dist/index.mjs +1 -7
- package/package.json +16 -6
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,214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Kotori
|
|
2
2
|
|
|
3
|
-
Strongly-typed
|
|
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
|
-
|
|
5
|
+
```ts
|
|
6
|
+
const intro = dict({ en: 'Hello {{name}}', zh: '你好 {{name}}' })
|
|
7
|
+
|
|
8
|
+
t('intro', { name: 'John' }) // ✅ typed
|
|
9
|
+
t('intro') // ❌ compile error: missing { name }
|
|
10
|
+
t('intro', { nama: 'John' }) // ❌ compile error: unknown key 'nama'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- No codegen
|
|
14
|
+
- No JSON
|
|
15
|
+
- No dependencies
|
|
16
|
+
- 0.31kb gzipped
|
|
17
|
+
- Modular and tree-shakeable
|
|
18
|
+
- Language change in one page rerenders all pages
|
|
19
|
+
- Variables typed and inferred from string literals
|
|
20
|
+
- Translation keys are typed — no more string typos
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i kotori
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
**utils.ts**
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { kotori } from './kotori'
|
|
34
|
+
|
|
35
|
+
export const { createTranslations, dict } = kotori({
|
|
36
|
+
primaryLanguageTag: 'en',
|
|
37
|
+
secondaryLanguageTags: ['zh', 'ja', 'ms'],
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**page1.tsx**
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { createTranslations, dict } from './utils'
|
|
45
|
+
|
|
46
|
+
const intro = dict({
|
|
47
|
+
en: 'my name is {{name}}, I am {{age}} years old.',
|
|
48
|
+
zh: '我叫{{name}},我今年{{age}}岁了。',
|
|
49
|
+
ja: '私の名前は{{name}}で、{{age}}歳です。',
|
|
50
|
+
ms: 'nama saya {{name}}, saya berumur {{age}} tahun.',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const time = dict({
|
|
54
|
+
en: 'time {{time}}',
|
|
55
|
+
zh: '时间 {{time}}',
|
|
56
|
+
ja: '時間 {{time}}',
|
|
57
|
+
ms: 'waktu {{time}}',
|
|
58
|
+
// type your arguments, by default it's `Record<string, string>`
|
|
59
|
+
})<{ time: `${number}:${number}:${number}` }>
|
|
60
|
+
|
|
61
|
+
const { useTranslations } = createTranslations({
|
|
62
|
+
intro,
|
|
63
|
+
time,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const Page1 = () => {
|
|
68
|
+
const { t, setLanguage } = useTranslations()
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<select
|
|
72
|
+
name="language"
|
|
73
|
+
onChange={(e) => setLanguage(e.target.value as 'en')}
|
|
74
|
+
>
|
|
75
|
+
<option value="en">English</option>
|
|
76
|
+
<option value="zh">Chinese</option>
|
|
77
|
+
<option value="ja">Japanese</option>
|
|
78
|
+
<option value="ms">Malay</option>
|
|
79
|
+
</select>
|
|
80
|
+
<p>{t('intro', { name: 'John', age: 30 })}</p>
|
|
81
|
+
<p>{t('time', { time: '12:00:00' })}</p>
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**page2.tsx**
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { createTranslations, dict } from './utils'
|
|
91
|
+
|
|
92
|
+
const weather = dict({
|
|
93
|
+
en: 'The weather in {{city}} has {{humidity}}% humidity.',
|
|
94
|
+
zh: '{{city}}的天气湿度为{{humidity}}%。',
|
|
95
|
+
ja: '{{city}}の湿度は{{humidity}}%です。',
|
|
96
|
+
ms: 'Cuaca di {{city}} mempunyai kelembapan {{humidity}}%.',
|
|
97
|
+
})<{ city: string; humidity: number }>
|
|
98
|
+
|
|
99
|
+
const score = dict({
|
|
100
|
+
en: 'Your score is {{score}} out of {{total}}.',
|
|
101
|
+
zh: '你的得分是 {{total}} 分中的 {{score}} 分。',
|
|
102
|
+
ja: 'あなたのスコアは {{total}} 点中 {{score}} 点です。',
|
|
103
|
+
ms: 'Markah anda ialah {{score}} daripada {{total}}.',
|
|
104
|
+
})<{ score: number; total: number }>
|
|
105
|
+
|
|
106
|
+
const lastLogin = dict({
|
|
107
|
+
en: 'Last login: {{date}} at {{time}}',
|
|
108
|
+
zh: '上次登录:{{date}} {{time}}',
|
|
109
|
+
ja: '最終ログイン:{{date}} {{time}}',
|
|
110
|
+
ms: 'Log masuk terakhir: {{date}} pada {{time}}',
|
|
111
|
+
})<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
|
|
112
|
+
|
|
113
|
+
const { useTranslations } = createTranslations({
|
|
114
|
+
greeting,
|
|
115
|
+
score,
|
|
116
|
+
lastLogin,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export const Page2 = () => {
|
|
120
|
+
const { t, setLanguage } = useTranslations()
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<select
|
|
124
|
+
name="language"
|
|
125
|
+
onChange={(e) => setLanguage(e.target.value as 'en')}
|
|
126
|
+
>
|
|
127
|
+
<option value="en">English</option>
|
|
128
|
+
<option value="zh">Chinese</option>
|
|
129
|
+
<option value="ja">Japanese</option>
|
|
130
|
+
<option value="ms">Malay</option>
|
|
131
|
+
</select>
|
|
132
|
+
<p>{t('weather', { city: 'Kuala Lumpur', humidity: 80 })}</p>
|
|
133
|
+
<p>{t('score', { score: 87, total: 100 })}</p>
|
|
134
|
+
<p>{t('lastLogin', { date: '2024-04-24', time: '09:30' })}</p>
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## How It Works
|
|
141
|
+
|
|
142
|
+
### One `kotori` instance per app
|
|
143
|
+
|
|
144
|
+
`kotori` holds the language state. All `createTranslations` calls share that state — changing the language anywhere rerenders everywhere.
|
|
145
|
+
|
|
146
|
+
### One `createTranslations` per page/feature
|
|
147
|
+
|
|
148
|
+
Translations are colocated with the component that uses them. Bundlers naturally code-split them, so each page only loads what it needs.
|
|
149
|
+
|
|
150
|
+
### Variables are inferred from string literals
|
|
151
|
+
|
|
152
|
+
kotori parses `{{variable}}` at the type level. No separate type definitions needed — the string *is* the schema.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// primary string drives the contract
|
|
156
|
+
const greeting = dict({ en: 'Hi {{name}}', zh: '你好 {{name}}' })
|
|
157
|
+
// ^^^^^^ — inferred as required arg
|
|
158
|
+
|
|
159
|
+
// secondary strings are validated against it
|
|
160
|
+
const mismatch = dict({ en: 'Hi {{name}}', zh: '你好 {{other}}' })
|
|
161
|
+
// ^^^^^^^ — compile error
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Custom argument types
|
|
165
|
+
|
|
166
|
+
By default, variables are typed as `string`. Pass a generic to narrow them:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const time = dict({ en: '{{hour}}:{{minute}}' })<{
|
|
170
|
+
hour: number
|
|
171
|
+
minute: number
|
|
172
|
+
}>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## API
|
|
176
|
+
|
|
177
|
+
### `kotori(options)`
|
|
178
|
+
|
|
179
|
+
Creates a scoped i18n instance.
|
|
180
|
+
|
|
181
|
+
| option | type | description |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
|
|
184
|
+
| `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
|
|
185
|
+
|
|
186
|
+
Returns `{ dict, createTranslations }`.
|
|
187
|
+
|
|
188
|
+
### `dict(translations)(argsType?)`
|
|
189
|
+
|
|
190
|
+
Defines a translation unit. Takes one string per language. Optionally takes a generic to type the interpolated variables.
|
|
191
|
+
|
|
192
|
+
### `createTranslations(dicts)`
|
|
193
|
+
|
|
194
|
+
Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
|
|
195
|
+
|
|
196
|
+
### `useTranslations()`
|
|
197
|
+
|
|
198
|
+
React hook. Returns `{ t, getLanguage, setLanguage }`.
|
|
199
|
+
|
|
200
|
+
| return | description |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `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
|
+
| `getLanguage()` | Returns the current language tag. |
|
|
204
|
+
| `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
|
|
205
|
+
|
|
206
|
+
## Language Tags
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
## Trivial
|
|
211
|
+
|
|
212
|
+
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
|
+
|
|
214
|
+
*Kotori* (小鳥) means "small bird" in Japanese. No deeper relevance to the library — it just sounds nice.
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,
|
|
2
|
-
//#region src/isEven.ts
|
|
3
|
-
const isEven = (v) => !(v & 1);
|
|
4
|
-
//#endregion
|
|
5
|
-
//#region src/isOdd.ts
|
|
6
|
-
const isOdd = (v) => !isEven(v);
|
|
7
|
-
//#endregion
|
|
8
|
-
exports.isEven = isEven;
|
|
9
|
-
exports.isOdd = isOdd;
|
|
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;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
//#region
|
|
2
|
-
|
|
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/
|
|
5
|
-
|
|
4
|
+
//#region src/kotori.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
|
+
getLanguage: () => PrimaryTag | SecondaryTags;
|
|
25
|
+
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
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
6
30
|
//#endregion
|
|
7
|
-
export {
|
|
31
|
+
export { AllTags, ExtractVariables, SubTags, Tags, kotori };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
//#region
|
|
2
|
-
|
|
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/
|
|
5
|
-
|
|
4
|
+
//#region src/kotori.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
|
+
getLanguage: () => PrimaryTag | SecondaryTags;
|
|
25
|
+
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
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
6
30
|
//#endregion
|
|
7
|
-
export {
|
|
31
|
+
export { AllTags, ExtractVariables, SubTags, Tags, kotori };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const isEven = (v) => !(v & 1);
|
|
3
|
-
//#endregion
|
|
4
|
-
//#region src/isOdd.ts
|
|
5
|
-
const isOdd = (v) => !isEven(v);
|
|
6
|
-
//#endregion
|
|
7
|
-
export { isEven, isOdd };
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kotori",
|
|
3
3
|
"description": "Strongly-typed and composable internationalization library for React",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.1",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"setup": "rm -rf node_modules && npm i && git init && husky",
|
|
7
7
|
"prepublishOnly": "npm run build",
|
|
8
8
|
"build": "tsdown",
|
|
9
9
|
"test": "vitest",
|
|
10
|
-
"lint": "npx @biomejs/biome check --write"
|
|
10
|
+
"lint": "npx @biomejs/biome check --write",
|
|
11
|
+
"dev": "vite --host"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"dist"
|
|
@@ -35,9 +36,15 @@
|
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@biomejs/biome": "^2.4.12",
|
|
37
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",
|
|
38
42
|
"@vitest/coverage-v8": "^4.1.5",
|
|
43
|
+
"bcp47-language-tags": "^1.1.0",
|
|
39
44
|
"husky": "^9.1.7",
|
|
40
45
|
"lint-staged": "^16.4.0",
|
|
46
|
+
"react": "^19.2.5",
|
|
47
|
+
"react-dom": "^19.2.5",
|
|
41
48
|
"tsdown": "^0.21.10",
|
|
42
49
|
"tsx": "^4.21.0",
|
|
43
50
|
"typescript": "^6.0.3",
|
|
@@ -45,11 +52,14 @@
|
|
|
45
52
|
},
|
|
46
53
|
"repository": {
|
|
47
54
|
"type": "git",
|
|
48
|
-
"url": "git+https://github.com
|
|
55
|
+
"url": "git+https://github.com/tylim88/kotori.git"
|
|
49
56
|
},
|
|
50
57
|
"bugs": {
|
|
51
|
-
"url": "https://github.com
|
|
58
|
+
"url": "https://github.com/tylim88/kotori/issues"
|
|
52
59
|
},
|
|
53
|
-
"author": "
|
|
54
|
-
"license": "
|
|
60
|
+
"author": "tylim88",
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"react": ">=19.2.5"
|
|
64
|
+
}
|
|
55
65
|
}
|