kotori 0.0.14 → 0.1.2

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
@@ -52,7 +52,7 @@ npm i kotori
52
52
  ```ts
53
53
  import { kotori } from './kotori'
54
54
 
55
- export const { createTranslations, dict } = kotori({
55
+ export const { createTranslations, dict, setLanguage } = kotori({
56
56
  primaryLanguageTag: 'en',
57
57
  secondaryLanguageTags: ['zh', 'ja', 'ms'],
58
58
  })
@@ -83,13 +83,13 @@ const { useTranslations } = createTranslations({
83
83
  time,
84
84
  })
85
85
 
86
-
87
86
  export const Page1 = () => {
88
- const { t, setLanguage } = useTranslations()
87
+ const { t, language, setLanguage } = useTranslations()
89
88
  return (
90
89
  <>
91
90
  <select
92
91
  name="language"
92
+ value={language}
93
93
  onChange={(e) => setLanguage(e.target.value as 'en')}
94
94
  >
95
95
  <option value="en">English</option>
@@ -131,17 +131,18 @@ const lastLogin = dict({
131
131
  })<{ date: `${number}-${number}-${number}`; time: `${number}:${number}` }>
132
132
 
133
133
  const { useTranslations } = createTranslations({
134
- greeting,
134
+ weather,
135
135
  score,
136
136
  lastLogin,
137
137
  })
138
138
 
139
139
  export const Page2 = () => {
140
- const { t, setLanguage } = useTranslations()
140
+ const { t, language, setLanguage } = useTranslations()
141
141
  return (
142
142
  <>
143
143
  <select
144
144
  name="language"
145
+ value={language}
145
146
  onChange={(e) => setLanguage(e.target.value as 'en')}
146
147
  >
147
148
  <option value="en">English</option>
@@ -185,7 +186,7 @@ const mismatch = dict({ en: 'Hi {{name}}', zh: '你好 {{other}}' })
185
186
 
186
187
  ### Custom argument types
187
188
 
188
- By default, variables are typed as `string`. Pass a generic to narrow them:
189
+ By default, variables are typed as `string | number`. Pass a generic to narrow them:
189
190
 
190
191
  ```ts
191
192
  const time = dict({ en: '{{hour}}:{{minute}}' })<{
@@ -205,33 +206,35 @@ Creates a scoped i18n instance.
205
206
  | `primaryLanguageTag` | `AllTags` | The source language. Drives variable inference. |
206
207
  | `secondaryLanguageTags` | `AllTags[]` | Additional supported languages. |
207
208
 
208
- Returns `{ dict, createTranslations }`.
209
+ Returns `{ dict, createTranslations, setLanguage }`.
209
210
 
210
211
  ### `dict(translations)<argsType?>`
211
212
 
212
- Defines a translation unit. Takes one string per language. Optionally takes a generic to type the interpolated variables.
213
-
214
- Returns `() => { translations: Record<string, string> }`.
213
+ Defines a translation unit. Takes one string per language. Optionally takes a generic to narrow the interpolated variable types.
215
214
 
216
215
  ### `createTranslations(dicts)`
217
216
 
218
217
  Registers a set of dicts and returns `{ useTranslations }`. Call once per page or feature module.
219
218
 
219
+ ### `setLanguage(tag)`
220
+
221
+ Updates the current language and rerenders all active `useTranslations` consumers across all pages. Available directly on the `kotori` instance — useful for calling outside of React (route guards, axios interceptors, etc.).
222
+
220
223
  ### `useTranslations()`
221
224
 
222
- React hook. Returns `{ t, getLanguage, setLanguage }`.
225
+ React hook. Returns `{ t, language, setLanguage }`.
223
226
 
224
- | return | description |
225
- | --- | --- |
226
- | `t(key, args?)` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. |
227
- | `getLanguage()` | Returns the current language tag. |
228
- | `setLanguage(tag)` | Updates the language and rerenders all active `useTranslations` consumers. |
227
+ | return | type | description |
228
+ | --- | --- | --- |
229
+ | `t(key, args?)` | `string` | Returns the translated string for the current language. `args` is required if the string has variables, omitted if it doesn't. |
230
+ | `language` | `WorkingTags` | The current language tag as a reactive value. Updates when `setLanguage` is called. |
231
+ | `setLanguage(tag)` | `void` | Updates the language and rerenders all active `useTranslations` consumers. |
229
232
 
230
233
  ## Language Tags
231
234
 
232
235
  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.
233
236
 
234
- ## Trivial
237
+ ## Trivial
235
238
 
236
239
  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.
237
240
 
package/dist/index.cjs CHANGED
@@ -5,40 +5,36 @@ const kotori = (props) => {
5
5
  const listeners = /* @__PURE__ */ new Set();
6
6
  let languageTag = props.primaryLanguageTag;
7
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 });
8
+ const setLanguage = (tag) => {
9
+ languageTag = tag;
10
+ snapshots.forEach((snapshot, key) => {
11
+ snapshots.set(key, {
12
+ ...snapshot,
13
+ language: tag
14
14
  });
15
- listeners.forEach((listener) => {
16
- listener();
17
- });
18
- }
15
+ });
16
+ listeners.forEach((listener) => {
17
+ listener();
18
+ });
19
19
  };
20
20
  return {
21
+ setLanguage,
21
22
  dict: (translation) => () => ({ translation }),
22
23
  createTranslations: (dictCallbacks) => {
23
24
  const s = Symbol();
24
- let refCount = 0;
25
25
  const snapshot = {
26
- ...languageTagMethod,
26
+ language: languageTag,
27
+ setLanguage,
27
28
  t: (key, ...args) => {
28
- let locale = dictCallbacks[key]?.().translation[languageTag];
29
- if (!locale) return;
29
+ let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
30
30
  for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
31
31
  return locale;
32
32
  }
33
33
  };
34
34
  snapshots.set(s, snapshot);
35
35
  return { useTranslations: () => (0, react.useSyncExternalStore)((listener) => {
36
- if (refCount === 0) snapshots.set(s, snapshot);
37
- refCount++;
38
36
  listeners.add(listener);
39
37
  return () => {
40
- refCount--;
41
- if (refCount === 0) snapshots.delete(s);
42
38
  listeners.delete(listener);
43
39
  };
44
40
  }, () => snapshots.get(s), () => snapshot) };
package/dist/index.d.cts CHANGED
@@ -12,6 +12,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
12
12
  primaryLanguageTag: PrimaryTag;
13
13
  secondaryLanguageTags: SecondaryTags[];
14
14
  }) => {
15
+ setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
15
16
  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 | number>>() => Readonly<{
16
17
  translation: typeof translation;
17
18
  [_args]?: ArgsType;
@@ -21,9 +22,9 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
22
  [_args]?: Record<string, string | number>;
22
23
  }>>>(dictCallbacks: DictCallbacks) => {
23
24
  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;
25
+ language: PrimaryTag | SecondaryTags;
26
26
  setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
27
+ t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => string;
27
28
  };
28
29
  };
29
30
  };
package/dist/index.d.mts CHANGED
@@ -12,6 +12,7 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
12
12
  primaryLanguageTag: PrimaryTag;
13
13
  secondaryLanguageTags: SecondaryTags[];
14
14
  }) => {
15
+ setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
15
16
  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 | number>>() => Readonly<{
16
17
  translation: typeof translation;
17
18
  [_args]?: ArgsType;
@@ -21,9 +22,9 @@ declare const kotori: <const PrimaryTag extends AllTags, const SecondaryTags ext
21
22
  [_args]?: Record<string, string | number>;
22
23
  }>>>(dictCallbacks: DictCallbacks) => {
23
24
  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;
25
+ language: PrimaryTag | SecondaryTags;
26
26
  setLanguage: (tag: PrimaryTag | SecondaryTags) => void;
27
+ t: <Key extends keyof DictCallbacks>(key: Key, ...args: keyof NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]> extends never ? [] : [NonNullable<ReturnType<DictCallbacks[Key]>[typeof _args]>]) => string;
27
28
  };
28
29
  };
29
30
  };
package/dist/index.mjs CHANGED
@@ -4,40 +4,36 @@ const kotori = (props) => {
4
4
  const listeners = /* @__PURE__ */ new Set();
5
5
  let languageTag = props.primaryLanguageTag;
6
6
  const snapshots = /* @__PURE__ */ new Map();
7
- const languageTagMethod = {
8
- getLanguage: () => languageTag,
9
- setLanguage: (tag) => {
10
- languageTag = tag;
11
- snapshots.forEach((snapshot, key) => {
12
- snapshots.set(key, { ...snapshot });
7
+ const setLanguage = (tag) => {
8
+ languageTag = tag;
9
+ snapshots.forEach((snapshot, key) => {
10
+ snapshots.set(key, {
11
+ ...snapshot,
12
+ language: tag
13
13
  });
14
- listeners.forEach((listener) => {
15
- listener();
16
- });
17
- }
14
+ });
15
+ listeners.forEach((listener) => {
16
+ listener();
17
+ });
18
18
  };
19
19
  return {
20
+ setLanguage,
20
21
  dict: (translation) => () => ({ translation }),
21
22
  createTranslations: (dictCallbacks) => {
22
23
  const s = Symbol();
23
- let refCount = 0;
24
24
  const snapshot = {
25
- ...languageTagMethod,
25
+ language: languageTag,
26
+ setLanguage,
26
27
  t: (key, ...args) => {
27
- let locale = dictCallbacks[key]?.().translation[languageTag];
28
- if (!locale) return;
28
+ let locale = dictCallbacks[key]?.().translation[languageTag] || "unable_to_load_translations";
29
29
  for (const objKey in args[0]) locale = locale.replace(new RegExp(`\\{\\{\\s*${objKey}\\s*\\}\\}`, "g"), () => String(args[0]?.[objKey]));
30
30
  return locale;
31
31
  }
32
32
  };
33
33
  snapshots.set(s, snapshot);
34
34
  return { useTranslations: () => useSyncExternalStore((listener) => {
35
- if (refCount === 0) snapshots.set(s, snapshot);
36
- refCount++;
37
35
  listeners.add(listener);
38
36
  return () => {
39
- refCount--;
40
- if (refCount === 0) snapshots.delete(s);
41
37
  listeners.delete(listener);
42
38
  };
43
39
  }, () => snapshots.get(s), () => snapshot) };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kotori",
3
3
  "description": "Strongly-typed and composable internationalization library for React",
4
- "version": "0.0.14",
4
+ "version": "0.1.2",
5
5
  "scripts": {
6
6
  "setup": "rm -rf node_modules && npm i && git init && husky",
7
7
  "prepublishOnly": "npm i && npx tsc && npm run build",