i18n-keyless-react 1.1.0 → 1.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.
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { useI18nKeyless } from "./store";
3
+ type ComponentProps = NonNullable<ReturnType<typeof useI18nKeyless.getState>["config"]>["component"] extends React.ComponentType<infer P> ? P : never;
4
+ /**
5
+ * the component should be a React component
6
+ *
7
+ * ```ts
8
+ * export default function MyComponent({ anyprop, can, fit, children}) {
9
+ * return <Text>{children}</Text>;
10
+ * }
11
+ * ```
12
+ *
13
+ * Then you can pass MyComponent's props to i18n-keyless' MyI18nText
14
+ *
15
+ * ```ts
16
+ * <MyI18nText anyprop can fit>My text to translate</MyI18nText>
17
+ * ```
18
+ */
19
+ type MyI18nTextProps = Omit<ComponentProps, "children"> & {
20
+ children: string;
21
+ };
22
+ export declare const MyI18nText: React.FC<MyI18nTextProps>;
23
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
3
+ import { useI18nKeyless } from "./store";
4
+ export const MyI18nText = ({ children, ...textProps }) => {
5
+ const translations = useI18nKeyless((store) => store.translations);
6
+ const currentLanguage = useI18nKeyless((store) => store.currentLanguage);
7
+ const config = useI18nKeyless((store) => store.config);
8
+ const translateKey = useI18nKeyless((store) => store.translateKey);
9
+ useEffect(() => {
10
+ translateKey(children);
11
+ // eslint-disable-next-line react-hooks/exhaustive-deps
12
+ }, [children, currentLanguage]);
13
+ if (!config) {
14
+ return null;
15
+ }
16
+ const translatedText = translations[children] || children;
17
+ const TextComponent = config.component;
18
+ return _jsx(TextComponent, { ...textProps, children: translatedText });
19
+ };
@@ -0,0 +1,2 @@
1
+ export { MyI18nText } from "./MyI18nText";
2
+ export { init, useI18nKeyless, useCurrentLanguage, clearI18nKeylessStorage, fetchAllTranslations } from "./store";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { MyI18nText } from "./MyI18nText";
2
+ export { init, useI18nKeyless, useCurrentLanguage, clearI18nKeylessStorage, fetchAllTranslations } from "./store";
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "i18n-keyless-react",
3
+ "private": false,
4
+ "version": "1.1.2",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build:lib": "tsc --project tsconfig.json && npm pack",
14
+ "prepublishOnly": "npm run build:lib"
15
+ },
16
+ "dependencies": {
17
+ "i18n-keyless-core": "1.0.0",
18
+ "p-queue": "^8.1.0",
19
+ "react": "^19.0.0",
20
+ "zustand": "^5.0.3"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.9.0",
24
+ "@types/node": "^22.5.5",
25
+ "@types/react": "^19.0.8",
26
+ "eslint": "^9.9.0",
27
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
28
+ "eslint-plugin-react-refresh": "^0.4.9",
29
+ "typescript": "^5.5.3",
30
+ "typescript-eslint": "^8.0.1"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ import { I18nConfig, Lang, TranslationStore } from "i18n-keyless-core";
2
+ export declare const useI18nKeyless: import("zustand").UseBoundStore<import("zustand").StoreApi<TranslationStore>>;
3
+ export declare const init: (config: I18nConfig) => Promise<void>;
4
+ export declare function useCurrentLanguage(): Lang | null;
5
+ export declare const fetchAllTranslations: (targetLanguage: Lang) => Promise<void>;
6
+ export declare function clearI18nKeylessStorage(): Promise<void>;
package/dist/store.js ADDED
@@ -0,0 +1,287 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { create } from "zustand";
3
+ // https://github.com/sindresorhus/p-queue/issues/145#issuecomment-882068004
4
+ import PQueue from "p-queue/dist";
5
+ import packageJson from "./package.json";
6
+ const queue = new PQueue({ concurrency: 5 });
7
+ queue.on("empty", () => {
8
+ // when each word is translated, fetch the translations for the current language
9
+ fetchAllTranslations(useI18nKeyless.getState().currentLanguage);
10
+ });
11
+ const storeKeys = {
12
+ uniqueId: "i18n-keyless-user-id",
13
+ lastRefresh: "i18n-keyless-last-refresh",
14
+ translations: "i18n-keyless-translations",
15
+ currentLanguage: "i18n-keyless-current-language",
16
+ };
17
+ async function getItem(key, storage) {
18
+ if (!storage) {
19
+ throw new Error("i18n-keyless: storage is not initialized");
20
+ }
21
+ if (storage.getItem) {
22
+ return storage.getItem(key);
23
+ }
24
+ else if (storage.get) {
25
+ return storage.get(key);
26
+ }
27
+ else if (storage.getString) {
28
+ return storage.getString(key);
29
+ }
30
+ return null;
31
+ }
32
+ async function setItem(key, value, storage) {
33
+ if (!storage) {
34
+ throw new Error("i18n-keyless: storage is not initialized");
35
+ }
36
+ if (storage.setItem) {
37
+ storage.setItem(key, value);
38
+ }
39
+ else if (storage.set) {
40
+ storage.set(key, value);
41
+ }
42
+ }
43
+ async function deleteItem(key, storage) {
44
+ if (!storage) {
45
+ throw new Error("i18n-keyless: storage is not initialized");
46
+ }
47
+ if (storage.delete) {
48
+ storage.delete(key);
49
+ }
50
+ else if (storage.del) {
51
+ storage.del(key);
52
+ }
53
+ else if (storage.removeItem) {
54
+ storage.removeItem(key);
55
+ }
56
+ else if (storage.remove) {
57
+ storage.remove(key);
58
+ }
59
+ }
60
+ export const useI18nKeyless = create((set, get) => ({
61
+ uniqueId: null,
62
+ lastRefresh: null,
63
+ translations: {},
64
+ currentLanguage: "fr",
65
+ config: null,
66
+ translating: {},
67
+ _hasHydrated: false,
68
+ storage: null,
69
+ _hydrate: async () => {
70
+ const storage = get().config?.storage;
71
+ if (!storage) {
72
+ throw new Error("i18n-keyless: storage is not initialized");
73
+ }
74
+ const translations = await getItem(storeKeys.translations, storage);
75
+ if (translations) {
76
+ console.log("i18n-keyless: _hydrate", translations);
77
+ set({ translations: JSON.parse(translations) });
78
+ }
79
+ else {
80
+ console.log("i18n-keyless: _hydrate: no translations");
81
+ }
82
+ const currentLanguage = await getItem(storeKeys.currentLanguage, storage);
83
+ if (currentLanguage) {
84
+ console.log("i18n-keyless: _hydrate", currentLanguage);
85
+ set({ currentLanguage: currentLanguage });
86
+ }
87
+ else {
88
+ console.log("i18n-keyless: _hydrate: no current language");
89
+ set({ currentLanguage: get().config?.languages.initWithDefault });
90
+ }
91
+ const uniqueId = await getItem(storeKeys.uniqueId, storage);
92
+ if (uniqueId) {
93
+ set({ uniqueId: uniqueId });
94
+ }
95
+ const lastRefresh = await getItem(storeKeys.lastRefresh, storage);
96
+ if (lastRefresh) {
97
+ set({ lastRefresh: lastRefresh });
98
+ }
99
+ },
100
+ getTranslation: (text) => {
101
+ const translation = get().translations[text];
102
+ if (!translation) {
103
+ get().translateKey(text);
104
+ }
105
+ return translation || text;
106
+ },
107
+ setTranslations: (newTranslations) => {
108
+ const nextTranslations = { ...get().translations, ...newTranslations };
109
+ set({ translations: nextTranslations });
110
+ const storage = get().config?.storage;
111
+ if (!storage) {
112
+ throw new Error("i18n-keyless: storage is not initialized");
113
+ }
114
+ setItem(storeKeys.translations, JSON.stringify(nextTranslations), storage);
115
+ },
116
+ translateKey: (key) => {
117
+ if (key.length > 280) {
118
+ console.error("i18n-keyless: Key length exceeds 280 characters limit:", key);
119
+ return;
120
+ }
121
+ const translation = get().translations[key];
122
+ if (translation) {
123
+ return;
124
+ }
125
+ const config = get().config;
126
+ if (!config?.addMissingTranslations) {
127
+ return;
128
+ }
129
+ queue.add(async () => {
130
+ // console.log("i18n-keyless: translateKey", key);
131
+ try {
132
+ if (get().translating[key]) {
133
+ return;
134
+ }
135
+ else {
136
+ set({ translating: { ...get().translating, [key]: true } });
137
+ }
138
+ if (config?.handleTranslate) {
139
+ const result = await config?.handleTranslate?.(key);
140
+ }
141
+ else {
142
+ const body = {
143
+ key,
144
+ languages: config.languages.supported,
145
+ primaryLanguage: config.languages.primary,
146
+ };
147
+ const apiUrl = config.API_URL || "https://api.i18n-keyless.com";
148
+ const url = `${apiUrl}/translate`;
149
+ // console.log("i18n-keyless: POST", url);
150
+ const response = await fetch(url, {
151
+ method: "POST",
152
+ headers: {
153
+ "Content-Type": "application/json",
154
+ Authorization: `Bearer ${config.API_KEY}`,
155
+ unique_id: get().uniqueId || "",
156
+ Version: packageJson.version,
157
+ },
158
+ body: JSON.stringify(body),
159
+ }).then((res) => res.json());
160
+ if (response.message) {
161
+ console.warn("i18n-keyless: ", response.message);
162
+ }
163
+ }
164
+ set({ translating: { ...get().translating, [key]: false } });
165
+ return;
166
+ }
167
+ catch (error) {
168
+ console.error("i18n-keyless: Error translating key:", error);
169
+ set({ translating: { ...get().translating, [key]: false } });
170
+ }
171
+ }, { priority: 1, id: key });
172
+ },
173
+ setLanguage: async (lang) => {
174
+ console.log("i18n-keyless: setLanguage", lang);
175
+ const config = get().config;
176
+ const sanitizedLang = !config || !config.languages.supported.includes(lang) ? config?.languages.fallback : lang;
177
+ set({ currentLanguage: sanitizedLang });
178
+ if (config?.storage) {
179
+ setItem(storeKeys.currentLanguage, sanitizedLang, config.storage);
180
+ }
181
+ // Only fetch translations if the new language is not the primary language
182
+ if (lang !== config?.languages.primary) {
183
+ await fetchAllTranslations(lang);
184
+ }
185
+ },
186
+ }));
187
+ export const init = async (config) => {
188
+ // console.log("i18n-keyless: init", config);
189
+ if (!config.languages) {
190
+ throw new Error("i18n-keyless: languages is required");
191
+ }
192
+ if (!config.languages.primary) {
193
+ throw new Error("i18n-keyless: primary is required");
194
+ }
195
+ if (!config.languages.initWithDefault) {
196
+ config.languages.initWithDefault = config.languages.primary;
197
+ }
198
+ if (!config.languages.fallback) {
199
+ config.languages.fallback = config.languages.primary;
200
+ }
201
+ if (!config.languages.supported.includes(config.languages.initWithDefault)) {
202
+ config.languages.supported.push(config.languages.initWithDefault);
203
+ }
204
+ if (!config.component) {
205
+ throw new Error("i18n-keyless: component is required");
206
+ }
207
+ if (!config.storage) {
208
+ throw new Error("i18n-keyless: storage is required. You can use react-native-mmkv, @react-native-async-storage/async-storage, or window.localStorage, or any storage that has a getItem, setItem, removeItem, or get, set, and remove method");
209
+ }
210
+ if (!config.getAllTranslations || !config.handleTranslate) {
211
+ if (!config.API_KEY) {
212
+ if (!config.API_URL) {
213
+ throw new Error("i18n-keyless: you didn't provide an API_KEY nor an API_URL nor a handleTranslate + getAllTranslations function. You need to provide one of them to make i18n-keyless work");
214
+ }
215
+ }
216
+ }
217
+ if (config.addMissingTranslations !== false) {
218
+ // default to true
219
+ config.addMissingTranslations = true;
220
+ }
221
+ useI18nKeyless.setState({ config });
222
+ await useI18nKeyless.getState()._hydrate();
223
+ const currentLanguage = useI18nKeyless.getState().currentLanguage;
224
+ config.onInit?.(currentLanguage);
225
+ };
226
+ export function useCurrentLanguage() {
227
+ const currentLanguage = useI18nKeyless((state) => state.currentLanguage);
228
+ return currentLanguage;
229
+ }
230
+ export const fetchAllTranslations = async (targetLanguage) => {
231
+ // console.log("i18n-keyless: fetchAllTranslations", targetLanguage);
232
+ const store = useI18nKeyless.getState();
233
+ const config = store.config;
234
+ if (!config) {
235
+ console.error("i18n-keyless: No config found");
236
+ return;
237
+ }
238
+ if (config.languages.primary === targetLanguage) {
239
+ // console.log("i18n-keyless: using primary language");
240
+ return;
241
+ }
242
+ try {
243
+ const response = config.getAllTranslations
244
+ ? await config.getAllTranslations()
245
+ : await fetch(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/${targetLanguage}?last_refresh=${store.lastRefresh}`, {
246
+ method: "GET",
247
+ headers: {
248
+ "Content-Type": "application/json",
249
+ Authorization: `Bearer ${config.API_KEY}`,
250
+ Version: packageJson.version,
251
+ unique_id: store.uniqueId || "",
252
+ },
253
+ }).then((res) => res.json());
254
+ if (!response.ok) {
255
+ throw new Error(response.error);
256
+ }
257
+ if (response.message) {
258
+ console.warn("i18n-keyless: ", response.message);
259
+ }
260
+ if (response.data.uniqueId) {
261
+ useI18nKeyless.setState({ uniqueId: response.data.uniqueId });
262
+ setItem(storeKeys.uniqueId, response.data.uniqueId, config.storage);
263
+ }
264
+ if (response.data.lastRefresh) {
265
+ useI18nKeyless.setState({ lastRefresh: response.data.lastRefresh });
266
+ setItem(storeKeys.lastRefresh, response.data.lastRefresh, config.storage);
267
+ }
268
+ const data = response.data;
269
+ useI18nKeyless.getState().setTranslations(data.translations);
270
+ }
271
+ catch (error) {
272
+ console.error("i18n-keyless: Batch translation error:", error);
273
+ }
274
+ };
275
+ export async function clearI18nKeylessStorage() {
276
+ useI18nKeyless.setState({
277
+ translations: {},
278
+ currentLanguage: "fr",
279
+ config: null,
280
+ });
281
+ const config = useI18nKeyless.getState().config;
282
+ if (config?.storage) {
283
+ for (const key of Object.keys(storeKeys)) {
284
+ deleteItem(key, config.storage);
285
+ }
286
+ }
287
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "1.1.0",
4
+ "version": "1.1.2",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -10,7 +10,7 @@
10
10
  "dist"
11
11
  ],
12
12
  "scripts": {
13
- "build:lib": "tsc --project ../../tsconfig.json && npm pack",
13
+ "build:lib": "tsc --project tsconfig.json && npm pack",
14
14
  "prepublishOnly": "npm run build:lib"
15
15
  },
16
16
  "dependencies": {