i18n-keyless-react 1.10.5 → 1.10.7

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.
Files changed (37) hide show
  1. package/dist/core/api.d.ts +5 -0
  2. package/dist/core/api.js +23 -0
  3. package/dist/core/index.d.ts +2 -0
  4. package/dist/core/index.js +1 -0
  5. package/dist/core/my-pqueue.d.ts +24 -0
  6. package/dist/core/my-pqueue.js +108 -0
  7. package/dist/core/package.json +22 -0
  8. package/dist/core/service.d.ts +27 -0
  9. package/dist/core/service.js +163 -0
  10. package/dist/core/types.d.ts +94 -0
  11. package/dist/{__tests__ → react/__tests__}/I18nKeylessText.test.js +9 -2
  12. package/dist/{__tests__ → react/__tests__}/__mocks__/store.d.ts +1 -1
  13. package/dist/{__tests__ → react/__tests__}/__mocks__/store.js +10 -2
  14. package/dist/react/__tests__/store.test.d.ts +1 -0
  15. package/dist/react/index.d.ts +6 -0
  16. package/dist/react/index.js +4 -0
  17. package/dist/{package.json → react/package.json} +3 -2
  18. package/dist/{store.d.ts → react/store.d.ts} +3 -2
  19. package/dist/{store.js → react/store.js} +27 -15
  20. package/dist/react/types.d.ts +154 -0
  21. package/dist/react/types.js +1 -0
  22. package/dist/{utils.d.ts → react/utils.d.ts} +9 -1
  23. package/dist/{utils.js → react/utils.js} +16 -0
  24. package/package.json +3 -2
  25. package/dist/index.d.ts +0 -5
  26. package/dist/index.js +0 -4
  27. /package/dist/{__tests__/I18nKeylessText.test.d.ts → core/types.js} +0 -0
  28. /package/dist/{I18nKeylessText.d.ts → react/I18nKeylessText.d.ts} +0 -0
  29. /package/dist/{I18nKeylessText.js → react/I18nKeylessText.js} +0 -0
  30. /package/dist/{__tests__/store.test.d.ts → react/__tests__/I18nKeylessText.test.d.ts} +0 -0
  31. /package/dist/{__tests__ → react/__tests__}/__mocks__/zustand.d.ts +0 -0
  32. /package/dist/{__tests__ → react/__tests__}/__mocks__/zustand.js +0 -0
  33. /package/dist/{__tests__ → react/__tests__}/setup.d.ts +0 -0
  34. /package/dist/{__tests__ → react/__tests__}/setup.js +0 -0
  35. /package/dist/{__tests__ → react/__tests__}/store.test.js +0 -0
  36. /package/dist/{vitest.config.d.ts → react/vitest.config.d.ts} +0 -0
  37. /package/dist/{vitest.config.js → react/vitest.config.js} +0 -0
@@ -0,0 +1,5 @@
1
+ export declare const api: {
2
+ fetchTranslation: (url: string, options: RequestInit) => Promise<any>;
3
+ fetchTranslationsForOneLanguage: (url: string, options: RequestInit) => Promise<any>;
4
+ fetchAllTranslationsForAllLanguages: (url: string, options: RequestInit) => Promise<any>;
5
+ };
@@ -0,0 +1,23 @@
1
+ export const api = {
2
+ fetchTranslation: async (url, options) => {
3
+ return fetch(url, options)
4
+ .then((res) => res.json())
5
+ .catch((err) => {
6
+ return { ok: false, error: err.message };
7
+ });
8
+ },
9
+ fetchTranslationsForOneLanguage: async (url, options) => {
10
+ return fetch(url, options)
11
+ .then((res) => res.json())
12
+ .catch((err) => {
13
+ return { ok: false, error: err.message };
14
+ });
15
+ },
16
+ fetchAllTranslationsForAllLanguages: async (url, options) => {
17
+ return fetch(url, options)
18
+ .then((res) => res.json())
19
+ .catch((err) => {
20
+ return { ok: false, error: err.message };
21
+ });
22
+ },
23
+ };
@@ -0,0 +1,2 @@
1
+ export type { Lang, PrimaryLang, Translations, HandleTranslateFunction, GetAllTranslationsFunction, GetAllTranslationsForAllLanguagesFunction, LanguagesConfig, LastRefresh, UniqueId, I18nKeylessRequestBody, I18nKeylessResponse, I18nKeylessAllTranslationsResponse, FetchTranslationParams, TranslationOptions, } from "./types";
2
+ export { getTranslationCore, getAllTranslationsFromLanguage, queue } from "./service";
@@ -0,0 +1 @@
1
+ export { getTranslationCore, getAllTranslationsFromLanguage, queue } from "./service";
@@ -0,0 +1,24 @@
1
+ type Task<T> = () => Promise<T>;
2
+ declare class EventEmitter {
3
+ private events;
4
+ on(event: string, callback: () => void): void;
5
+ off(event: string, callback: () => void): void;
6
+ emit(event: string): void;
7
+ }
8
+ export default class MyPQueue extends EventEmitter {
9
+ private queue;
10
+ private pending;
11
+ private readonly concurrency;
12
+ private processing;
13
+ constructor(options?: {
14
+ concurrency?: number;
15
+ });
16
+ add<T>(task: Task<T>, options?: {
17
+ priority?: number;
18
+ id?: string;
19
+ }): Promise<T>;
20
+ private processNext;
21
+ get size(): number;
22
+ get isPaused(): boolean;
23
+ }
24
+ export {};
@@ -0,0 +1,108 @@
1
+ // https://github.com/sindresorhus/p-queue/issues/145#issuecomment-882068004
2
+ // p-queu import is broken, so here is the smalle implementation of it
3
+ class EventEmitter {
4
+ constructor() {
5
+ Object.defineProperty(this, "events", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: {}
10
+ });
11
+ }
12
+ on(event, callback) {
13
+ if (!this.events[event]) {
14
+ this.events[event] = [];
15
+ }
16
+ this.events[event].push(callback);
17
+ }
18
+ off(event, callback) {
19
+ if (!this.events[event])
20
+ return;
21
+ this.events[event] = this.events[event].filter((cb) => cb !== callback);
22
+ }
23
+ emit(event) {
24
+ if (!this.events[event])
25
+ return;
26
+ this.events[event].forEach((callback) => callback());
27
+ }
28
+ }
29
+ export default class MyPQueue extends EventEmitter {
30
+ constructor(options = {}) {
31
+ super();
32
+ Object.defineProperty(this, "queue", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: []
37
+ });
38
+ Object.defineProperty(this, "pending", {
39
+ enumerable: true,
40
+ configurable: true,
41
+ writable: true,
42
+ value: 0
43
+ });
44
+ Object.defineProperty(this, "concurrency", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: void 0
49
+ });
50
+ Object.defineProperty(this, "processing", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: false
55
+ });
56
+ this.concurrency = options.concurrency ?? Infinity;
57
+ }
58
+ add(task, options = {}) {
59
+ const { priority = 0, id = String(Date.now()) } = options;
60
+ // If task with same ID exists, return its promise
61
+ const existingTask = this.queue.find((item) => item.id === id);
62
+ if (existingTask) {
63
+ return existingTask.task();
64
+ }
65
+ return new Promise((resolve, reject) => {
66
+ const wrappedTask = async () => {
67
+ try {
68
+ const result = await task();
69
+ resolve(result);
70
+ return result;
71
+ }
72
+ catch (error) {
73
+ reject(error);
74
+ throw error;
75
+ }
76
+ finally {
77
+ this.pending--;
78
+ this.processNext();
79
+ }
80
+ };
81
+ this.queue.push({ task: wrappedTask, priority, id });
82
+ this.queue.sort((a, b) => b.priority - a.priority);
83
+ this.processNext();
84
+ });
85
+ }
86
+ async processNext() {
87
+ if (this.processing)
88
+ return;
89
+ this.processing = true;
90
+ while (this.queue.length > 0 && this.pending < this.concurrency) {
91
+ const task = this.queue.shift();
92
+ if (task) {
93
+ this.pending++;
94
+ task.task().catch(() => { });
95
+ }
96
+ }
97
+ this.processing = false;
98
+ if (this.queue.length === 0 && this.pending === 0) {
99
+ this.emit("empty");
100
+ }
101
+ }
102
+ get size() {
103
+ return this.queue.length;
104
+ }
105
+ get isPaused() {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "i18n-keyless-core",
3
+ "private": false,
4
+ "version": "1.10.7",
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
+ "prepublishOnly": "rm -rf ./dist && tsc --project tsconfig.json && npm pack",
14
+ "test": "echo 'no test for core'",
15
+ "postpublish": "rm -rf ./dist && rm *.tgz"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.5.5",
19
+ "@types/react": "^19.0.8",
20
+ "@types/react-dom": "^19.0.3"
21
+ }
22
+ }
@@ -0,0 +1,27 @@
1
+ import type { Lang, TranslationOptions, I18nKeylessResponse, FetchTranslationParams } from "./types";
2
+ import MyPQueue from "./my-pqueue";
3
+ export declare const queue: MyPQueue;
4
+ /**
5
+ * Gets a translation for the specified key from the store
6
+ * @param key - The translation key (text in primary language)
7
+ * @param store - The translation store containing translations and config
8
+ * @param options - Optional parameters for translation retrieval
9
+ * @returns The translated text or the original key if not found
10
+ * @throws Error if config is not initialized
11
+ */
12
+ export declare function getTranslationCore(key: string, store: FetchTranslationParams, options?: TranslationOptions): string;
13
+ /**
14
+ * Queues a key for translation if not already translated
15
+ * @param key - The text to translate
16
+ * @param store - The translation store
17
+ * @param options - Optional parameters for the translation process
18
+ * @throws Error if config is not initialized
19
+ */
20
+ export declare function translateKey(key: string, store: FetchTranslationParams, options?: TranslationOptions): void;
21
+ /**
22
+ * Fetches all translations for a target language
23
+ * @param targetLanguage - The language code to fetch translations for
24
+ * @param store - The translation store
25
+ * @returns Promise resolving to the translation response or void if failed
26
+ */
27
+ export declare function getAllTranslationsFromLanguage(targetLanguage: Lang, store: FetchTranslationParams): Promise<I18nKeylessResponse | void>;
@@ -0,0 +1,163 @@
1
+ import MyPQueue from "./my-pqueue";
2
+ import packageJson from "./package.json";
3
+ import { api } from "./api";
4
+ export const queue = new MyPQueue({ concurrency: 5 });
5
+ /**
6
+ * Gets a translation for the specified key from the store
7
+ * @param key - The translation key (text in primary language)
8
+ * @param store - The translation store containing translations and config
9
+ * @param options - Optional parameters for translation retrieval
10
+ * @returns The translated text or the original key if not found
11
+ * @throws Error if config is not initialized
12
+ */
13
+ export function getTranslationCore(key, store, options) {
14
+ const currentLanguage = store.currentLanguage;
15
+ const config = store.config;
16
+ const translations = store.translations;
17
+ if (!config.API_KEY) {
18
+ throw new Error("i18n-keyless: config is not initialized");
19
+ }
20
+ if (currentLanguage === config.languages.primary) {
21
+ return key;
22
+ }
23
+ if (options?.forceTemporary?.[currentLanguage]) {
24
+ translateKey(key, store, options);
25
+ }
26
+ const context = options?.context;
27
+ const translation = context ? translations[`${key}__${context}`] : translations[key];
28
+ if (!translation) {
29
+ translateKey(key, store, options);
30
+ }
31
+ return translation || key;
32
+ }
33
+ const translating = {};
34
+ /**
35
+ * Queues a key for translation if not already translated
36
+ * @param key - The text to translate
37
+ * @param store - The translation store
38
+ * @param options - Optional parameters for the translation process
39
+ * @throws Error if config is not initialized
40
+ */
41
+ export function translateKey(key, store, options) {
42
+ const currentLanguage = store.currentLanguage;
43
+ const config = store.config;
44
+ const translations = store.translations;
45
+ const uniqueId = store.uniqueId;
46
+ if (!config.API_KEY) {
47
+ throw new Error("i18n-keyless: config is not initialized");
48
+ }
49
+ const context = options?.context;
50
+ const debug = options?.debug;
51
+ // if (key.length > 280) {
52
+ // console.error("i18n-keyless: Key length exceeds 280 characters limit:", key);
53
+ // return;
54
+ // }
55
+ if (!key) {
56
+ return;
57
+ }
58
+ if (debug) {
59
+ console.log("translateKey", key, context, debug);
60
+ }
61
+ const forceTemporaryLang = options?.forceTemporary?.[currentLanguage];
62
+ const translation = context ? translations[`${key}__${context}`] : translations[key];
63
+ if (translation && !forceTemporaryLang) {
64
+ if (debug) {
65
+ console.log("translation exists", `${key}__${context}`);
66
+ }
67
+ return;
68
+ }
69
+ queue.add(async () => {
70
+ try {
71
+ if (translating[key]) {
72
+ return;
73
+ }
74
+ else {
75
+ translating[key] = true;
76
+ }
77
+ if (config.handleTranslate) {
78
+ await config.handleTranslate?.(key);
79
+ }
80
+ else {
81
+ const body = {
82
+ key,
83
+ context,
84
+ forceTemporary: options?.forceTemporary,
85
+ languages: config.languages.supported,
86
+ primaryLanguage: config.languages.primary,
87
+ };
88
+ const apiUrl = config.API_URL || "https://api.i18n-keyless.com";
89
+ const url = `${apiUrl}/translate`;
90
+ if (debug) {
91
+ console.log("fetching translation", url, body);
92
+ }
93
+ const response = await api
94
+ .fetchTranslation(url, {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ Authorization: `Bearer ${config.API_KEY}`,
99
+ unique_id: uniqueId || "",
100
+ Version: packageJson.version,
101
+ },
102
+ body: JSON.stringify(body),
103
+ })
104
+ .then((res) => res);
105
+ if (debug) {
106
+ console.log("response", response);
107
+ }
108
+ if (response.message) {
109
+ console.warn("i18n-keyless: ", response.message);
110
+ }
111
+ }
112
+ translating[key] = false;
113
+ return;
114
+ }
115
+ catch (error) {
116
+ console.error("i18n-keyless: Error translating key:", error);
117
+ translating[key] = false;
118
+ }
119
+ }, { priority: 1, id: key });
120
+ }
121
+ /**
122
+ * Fetches all translations for a target language
123
+ * @param targetLanguage - The language code to fetch translations for
124
+ * @param store - The translation store
125
+ * @returns Promise resolving to the translation response or void if failed
126
+ */
127
+ export async function getAllTranslationsFromLanguage(targetLanguage, store) {
128
+ const config = store.config;
129
+ const lastRefresh = store.lastRefresh;
130
+ const uniqueId = store.uniqueId;
131
+ if (!config.API_KEY) {
132
+ console.error("i18n-keyless: No config found");
133
+ return;
134
+ }
135
+ // if (config.languages.primary === targetLanguage) {
136
+ // return;
137
+ // }
138
+ try {
139
+ const response = config.getAllTranslations
140
+ ? await config.getAllTranslations()
141
+ : await api
142
+ .fetchTranslationsForOneLanguage(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/${targetLanguage}?last_refresh=${lastRefresh}`, {
143
+ method: "GET",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ Authorization: `Bearer ${config.API_KEY}`,
147
+ Version: packageJson.version,
148
+ unique_id: uniqueId || "",
149
+ },
150
+ })
151
+ .then((res) => res);
152
+ if (!response.ok) {
153
+ throw new Error(response.error);
154
+ }
155
+ if (response.message) {
156
+ console.warn("i18n-keyless: ", response.message);
157
+ }
158
+ return response;
159
+ }
160
+ catch (error) {
161
+ console.error("i18n-keyless: fetch all translations error:", error);
162
+ }
163
+ }
@@ -0,0 +1,94 @@
1
+ export type PrimaryLang = "fr" | "en";
2
+ export type Lang = "fr" | "en" | "nl" | "it" | "de" | "es" | "pl" | "pt" | "ro" | "sv" | "tr" | "ja" | "cn" | "ru" | "ko" | "ar";
3
+ export type Translations = Record<string, string>;
4
+ export type HandleTranslateFunction = (key: string) => Promise<{
5
+ ok: boolean;
6
+ message: string;
7
+ }>;
8
+ export type GetAllTranslationsFunction = () => Promise<I18nKeylessResponse>;
9
+ export type GetAllTranslationsForAllLanguagesFunction = () => Promise<I18nKeylessAllTranslationsResponse>;
10
+ export type LastRefresh = string | null;
11
+ export type UniqueId = string | null;
12
+ export type LanguagesConfig = {
13
+ /**
14
+ * the language used by the developer
15
+ */
16
+ primary: PrimaryLang;
17
+ /**
18
+ * the languages supported for the user.
19
+ * For now we support:
20
+ * fr, nl, it, de, es, pl, pt, ro, sv, tr, ja, cn, ru, ko, ar
21
+ *
22
+ * If you need more, please reach out to @ambroselli_io on X/Twitter or by mail at arnaud.ambroselli.io@gmail.com
23
+ */
24
+ supported: Lang[];
25
+ /**
26
+ * if the user's langauge is not supported, the fallback language will be used
27
+ */
28
+ fallback?: Lang;
29
+ /**
30
+ * the language to use when the app is initialized
31
+ */
32
+ initWithDefault?: Lang;
33
+ };
34
+ export type TranslationOptions = {
35
+ /**
36
+ * The context of the translation.
37
+ * Useful for ambiguous translations, like "8 heures" in French could be "8 AM" or "8 hours".
38
+ * You'll find it useful when it occurs to you, don't worry :)
39
+ */
40
+ context?: string;
41
+ /**
42
+ * Could be helpful if something weird happens with this particular key.
43
+ */
44
+ debug?: boolean;
45
+ /**
46
+ * If the proposed translation from AI is not satisfactory,
47
+ * you can use this field to setup your own translation.
48
+ * You can leave it there forever, or remove it once your translation is saved.
49
+ */
50
+ forceTemporary?: Partial<Record<Lang, string>>;
51
+ };
52
+ export interface I18nKeylessRequestBody {
53
+ key: string;
54
+ context?: string;
55
+ forceTemporary?: TranslationOptions["forceTemporary"];
56
+ languages: LanguagesConfig["supported"];
57
+ primaryLanguage: LanguagesConfig["primary"];
58
+ }
59
+ export interface I18nKeylessResponse {
60
+ ok: boolean;
61
+ data: {
62
+ translations: Translations;
63
+ uniqueId: UniqueId;
64
+ lastRefresh: LastRefresh;
65
+ };
66
+ error: string;
67
+ message: string;
68
+ }
69
+ export interface I18nKeylessAllTranslationsResponse {
70
+ ok: boolean;
71
+ data: {
72
+ translations: Record<Lang, Translations>;
73
+ uniqueId: UniqueId;
74
+ lastRefresh: LastRefresh;
75
+ };
76
+ error: string;
77
+ message: string;
78
+ }
79
+ export type FetchTranslationParams = {
80
+ uniqueId: UniqueId;
81
+ lastRefresh: LastRefresh;
82
+ currentLanguage: Lang;
83
+ config: {
84
+ API_KEY: string;
85
+ API_URL?: string;
86
+ languages: LanguagesConfig;
87
+ addMissingTranslations?: boolean;
88
+ debug?: boolean;
89
+ handleTranslate?: HandleTranslateFunction;
90
+ getAllTranslations?: GetAllTranslationsFunction;
91
+ getAllTranslationsForAllLanguages?: GetAllTranslationsForAllLanguagesFunction;
92
+ };
93
+ translations: Translations;
94
+ };
@@ -6,7 +6,13 @@ import { getTranslationCore } from "i18n-keyless-core";
6
6
  // Create a mock store before vi.mock call using vi.hoisted
7
7
  const mockStore = vi.hoisted(() => {
8
8
  const store = {
9
- config: null,
9
+ config: {
10
+ API_KEY: "any-fucking-key",
11
+ languages: {
12
+ primary: "en",
13
+ supported: ["en"],
14
+ },
15
+ },
10
16
  currentLanguage: "en",
11
17
  translations: {},
12
18
  uniqueId: null,
@@ -57,9 +63,10 @@ describe("I18nKeylessText", () => {
57
63
  translations: {},
58
64
  currentLanguage: "en",
59
65
  config: {
66
+ API_KEY: "any-fucking-key",
60
67
  languages: {
61
68
  primary: "en",
62
- supported: ["en", "fr"],
69
+ supported: ["en"],
63
70
  },
64
71
  },
65
72
  });
@@ -1,4 +1,4 @@
1
- import { type TranslationStore } from "i18n-keyless-core";
1
+ import { type TranslationStore } from "../../types";
2
2
  export declare const store: TranslationStore;
3
3
  export declare const mockStore: any;
4
4
  export declare const mockStorage: {
@@ -1,7 +1,13 @@
1
- import { validateLanguage } from "i18n-keyless-core";
2
1
  import { vi } from "vitest";
2
+ import { validateLanguage } from "../../utils";
3
3
  export const store = {
4
- config: null,
4
+ config: {
5
+ API_KEY: "any-fucking-keyk",
6
+ languages: {
7
+ primary: "fr",
8
+ supported: ["fr"],
9
+ },
10
+ },
5
11
  currentLanguage: "fr",
6
12
  translations: {},
7
13
  uniqueId: null,
@@ -13,6 +19,7 @@ export const store = {
13
19
  }),
14
20
  };
15
21
  // Create a function that supports the selector pattern
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
23
  const useI18nKeylessMock = ((selectorOrStore) => {
17
24
  // If it's a function (selector), call it with the store
18
25
  if (typeof selectorOrStore === "function") {
@@ -20,6 +27,7 @@ const useI18nKeylessMock = ((selectorOrStore) => {
20
27
  }
21
28
  // Otherwise return the store
22
29
  return store;
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
31
  });
24
32
  // Add the getState and setState methods to the mock function
25
33
  useI18nKeylessMock.getState = vi.fn(() => store);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export { I18nKeylessText } from "./I18nKeylessText";
2
+ export { init, setCurrentLanguage, useCurrentLanguage, getTranslation } from "./store";
3
+ export { clearI18nKeylessStorage, validateLanguage } from "./utils";
4
+ export type { I18nKeylessTextProps } from "./I18nKeylessText";
5
+ export { type I18nConfig, type TranslationStoreState, type TranslationOptions, type TranslationStore } from "./types";
6
+ export { type Lang, type PrimaryLang, type Translations, type I18nKeylessRequestBody, type I18nKeylessResponse, getAllTranslationsFromLanguage, queue, } from "i18n-keyless-core";
@@ -0,0 +1,4 @@
1
+ export { I18nKeylessText } from "./I18nKeylessText";
2
+ export { init, setCurrentLanguage, useCurrentLanguage, getTranslation } from "./store";
3
+ export { clearI18nKeylessStorage, validateLanguage } from "./utils";
4
+ export { getAllTranslationsFromLanguage, queue, } from "i18n-keyless-core";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "1.10.5",
4
+ "version": "1.10.7",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  "prepublishOnly": "rm -rf ./dist && tsc --project tsconfig.json && npm run test && npm pack",
14
14
  "test": "vitest run",
15
15
  "test:watch": "vitest",
16
- "test:coverage": "vitest run --coverage"
16
+ "test:coverage": "vitest run --coverage",
17
+ "postpublish": "rm -rf ./dist && rm *.tgz"
17
18
  },
18
19
  "dependencies": {
19
20
  "i18n-keyless-core": "latest",
@@ -1,4 +1,5 @@
1
- import { type I18nConfig, type Lang, type TranslationStore, type TranslationOptions } from "i18n-keyless-core";
1
+ import { type Lang, type TranslationOptions } from "i18n-keyless-core";
2
+ import { type I18nConfig, type TranslationStore } from "./types";
2
3
  export declare const useI18nKeyless: import("zustand").UseBoundStore<import("zustand").StoreApi<TranslationStore>>;
3
4
  /**
4
5
  * Initializes the i18n configuration with defaults and validation
@@ -6,7 +7,7 @@ export declare const useI18nKeyless: import("zustand").UseBoundStore<import("zus
6
7
  * @returns The validated and completed configuration
7
8
  * @throws Error if required configuration properties are missing
8
9
  */
9
- export declare function init(newConfig: Omit<I18nConfig, "getAllTranslationsForAllLanguages">): Promise<void>;
10
+ export declare function init(newConfig: I18nConfig): Promise<void>;
10
11
  export declare function useCurrentLanguage(): Lang | null;
11
12
  export declare function getTranslation(key: string, options?: TranslationOptions): string;
12
13
  export declare function setCurrentLanguage(lang: I18nConfig["languages"]["supported"][number]): void;
@@ -1,23 +1,32 @@
1
- import { queue, getAllTranslationsFromLanguage, validateLanguage, getTranslationCore, } from "i18n-keyless-core";
1
+ import { queue, getAllTranslationsFromLanguage, getTranslationCore, } from "i18n-keyless-core";
2
2
  import { create } from "zustand";
3
- import { storeKeys, setItem, getItem, clearI18nKeylessStorage } from "./utils";
3
+ import { storeKeys, setItem, getItem, clearI18nKeylessStorage, validateLanguage } from "./utils";
4
4
  queue.on("empty", () => {
5
5
  // when each word is translated, fetch the translations for the current language
6
6
  const store = useI18nKeyless.getState();
7
- getAllTranslationsFromLanguage(store.currentLanguage, store).then(store.setTranslations);
7
+ if (store.config) {
8
+ getAllTranslationsFromLanguage(store.currentLanguage, store).then(store.setTranslations);
9
+ }
8
10
  });
9
11
  export const useI18nKeyless = create((set, get) => ({
10
12
  uniqueId: null,
11
13
  lastRefresh: null,
12
14
  translations: {},
13
15
  currentLanguage: "fr",
14
- config: null,
16
+ config: {
17
+ API_KEY: "",
18
+ languages: {
19
+ primary: "fr",
20
+ supported: ["fr"],
21
+ },
22
+ storage: undefined,
23
+ },
15
24
  setTranslations: (response) => {
16
25
  if (!response?.ok) {
17
26
  return;
18
27
  }
19
28
  const config = get().config;
20
- if (!config) {
29
+ if (!config.API_KEY) {
21
30
  throw new Error(`i18n-keyless: config is not initialized setting translations`);
22
31
  }
23
32
  const newTranslations = response.data.translations;
@@ -38,12 +47,12 @@ export const useI18nKeyless = create((set, get) => ({
38
47
  }
39
48
  },
40
49
  setLanguage: async (lang) => {
41
- const config = get().config;
42
- if (!config) {
50
+ const store = get();
51
+ if (!store.config) {
43
52
  throw new Error(`i18n-keyless: config is not initialized setting translations`);
44
53
  }
45
- const debug = config.debug;
46
- const validatedLang = validateLanguage(lang, config);
54
+ const debug = store.config.debug;
55
+ const validatedLang = validateLanguage(lang, store.config);
47
56
  if (validatedLang !== lang) {
48
57
  if (debug)
49
58
  console.log("i18n-keyless: language", lang, "is not supported, fallback to", validatedLang);
@@ -53,18 +62,18 @@ export const useI18nKeyless = create((set, get) => ({
53
62
  console.log("i18n-keyless: setLanguage", lang);
54
63
  }
55
64
  set({ currentLanguage: validatedLang });
56
- if (config.storage) {
57
- setItem(storeKeys.currentLanguage, validatedLang, config.storage);
65
+ if (store.config.storage) {
66
+ setItem(storeKeys.currentLanguage, validatedLang, store.config.storage);
58
67
  }
59
68
  // Only fetch translations if the new language is not the primary language
60
- if (lang !== config.languages.primary) {
61
- await getAllTranslationsFromLanguage(lang, get()).then(get().setTranslations);
69
+ if (lang !== store.config.languages.primary) {
70
+ await getAllTranslationsFromLanguage(lang, store).then(store.setTranslations);
62
71
  }
63
72
  },
64
73
  }));
65
74
  async function hydrate() {
66
75
  const config = useI18nKeyless.getState().config;
67
- if (!config) {
76
+ if (!config.API_KEY) {
68
77
  throw new Error(`i18n-keyless: config is not initialized hydrating`);
69
78
  }
70
79
  const storage = config.storage;
@@ -139,6 +148,9 @@ export async function init(newConfig) {
139
148
  // default to true
140
149
  newConfig.addMissingTranslations = true;
141
150
  }
151
+ if (!newConfig.API_KEY) {
152
+ throw new Error(`i18n-keyless: API_KEY is required`);
153
+ }
142
154
  useI18nKeyless.setState({ config: newConfig });
143
155
  await hydrate();
144
156
  const currentLanguage = useI18nKeyless.getState().currentLanguage;
@@ -159,7 +171,7 @@ export async function clearI18nKeylessStorageAndStore() {
159
171
  useI18nKeyless.setState({
160
172
  translations: {},
161
173
  currentLanguage: "fr",
162
- config: null,
174
+ config: undefined,
163
175
  });
164
176
  const config = useI18nKeyless.getState().config;
165
177
  if (config?.storage) {
@@ -0,0 +1,154 @@
1
+ import type { I18nKeylessResponse, Lang, Translations, HandleTranslateFunction, GetAllTranslationsFunction, UniqueId, LastRefresh, LanguagesConfig } from "i18n-keyless-core";
2
+ type GetStorageFunction = (key: string) => string | null | undefined | Promise<string | null | undefined>;
3
+ type SetStorageFunction = (key: string, value: string) => void | Promise<void>;
4
+ type RemoveStorageFunction = (key: string) => void | Promise<void>;
5
+ type ClearStorageFunction = () => void | Promise<void>;
6
+ export interface I18nConfig {
7
+ /**
8
+ * The API key for the i18n-keyless API
9
+ *
10
+ * contact @ambroselli_io on X/Twitter or by mail at arnaud.ambroselli.io@gmail.com for getting one
11
+ */
12
+ API_KEY: string;
13
+ /**
14
+ * Your own API URL for the i18n-keyless API
15
+ *
16
+ * You'll need to implement two routes on your server
17
+ * - GET /translate/:lang
18
+ * - POST /translate -- with a body of { key: string }
19
+ */
20
+ API_URL?: string;
21
+ /**
22
+ * The languages config
23
+ *
24
+ * primary: the language used by the developer
25
+ * supported: the languages supported for the user
26
+ * fallback: if the user's langauge is not supported, the fallback language will be used
27
+ * initWithDefault: the language to use when the app is initialized for the first time
28
+ */
29
+ languages: LanguagesConfig;
30
+ /**
31
+ * if true, everytime a primary key is not found
32
+ * there will be a call to POST /translate -- with a body of { key: string }
33
+ * which should handle adding the key to the translations and, if needed,
34
+ * translate the key to all the languages supported by the user
35
+ *
36
+ * Two scenarios
37
+ * 1. Enable it in dev mode only: you'll may add some useless key, but you are 100% sure all the translations are up to date
38
+ * 2. Enable it in prod mode only: you take a risk taht the translations is not available when required for the first user demanding
39
+ * Best take: enable it all the time
40
+ */
41
+ addMissingTranslations?: boolean;
42
+ /**
43
+ * called right after the store is initialized, maybe to hide screensplash. or init specific default langauge for dayjs, or whatever
44
+ */
45
+ onInit?: (lang: Lang) => void;
46
+ /**
47
+ * if true, all the logs will be displayed in the console
48
+ */
49
+ debug?: boolean;
50
+ /**
51
+ * called everytime the language is set, maybe to set also the locale to dayjs or whatever
52
+ */
53
+ onSetLanguage?: (lang: Lang) => void;
54
+ /**
55
+ * if this function exists, it will be called instead of the API call
56
+ * if this function doesn't exist, the default behavior is to call the API
57
+ * therefore you would need either to
58
+ * - use this `handleTranslate` function to handle the translation with your own API
59
+ * - not use this `handleTranslate` function, and use the built in API call with API_KEY filled
60
+ * - not use this `handleTranslate` function nor API_KEY key, and provide your own API_URL
61
+ */
62
+ handleTranslate?: HandleTranslateFunction;
63
+ /**
64
+ * if this function exists, it will be called instead of the API call
65
+ * if this function doesn't exist, the default behavior is to call the API, with the API_KEY
66
+ * therefore you need either to
67
+ * - use this `getAllTranslations` function to handle the translation with your own API
68
+ * - not use this `getAllTranslations` function, and use the built in API call with the API_KEY filled
69
+ * - not use this `getAllTranslations` function nor API_KEY key, and provide your own API_URL
70
+ */
71
+ getAllTranslations?: GetAllTranslationsFunction;
72
+ /**
73
+ * the storage to use for the translations
74
+ *
75
+ * you can use react-native-mmkv, @react-native-async-storage/async-storage, or window.localStorage, or idb-keyval for IndexedDB, or any storage that has a getItem, setItem, removeItem, or get, set, and remove method
76
+ */
77
+ storage?: {
78
+ getItem?: GetStorageFunction;
79
+ get?: GetStorageFunction;
80
+ getString?: GetStorageFunction;
81
+ setItem?: SetStorageFunction;
82
+ set?: SetStorageFunction;
83
+ removeItem?: RemoveStorageFunction;
84
+ remove?: RemoveStorageFunction;
85
+ delete?: RemoveStorageFunction;
86
+ del?: RemoveStorageFunction;
87
+ clear?: ClearStorageFunction;
88
+ clearAll?: ClearStorageFunction;
89
+ } & {
90
+ getString?: GetStorageFunction;
91
+ get?: GetStorageFunction;
92
+ getItem?: GetStorageFunction;
93
+ } & {
94
+ setItem?: SetStorageFunction;
95
+ set?: SetStorageFunction;
96
+ } & {
97
+ removeItem?: RemoveStorageFunction;
98
+ remove?: RemoveStorageFunction;
99
+ delete?: RemoveStorageFunction;
100
+ del?: RemoveStorageFunction;
101
+ } & ({
102
+ clear: ClearStorageFunction;
103
+ clearAll?: never;
104
+ } | {
105
+ clearAll: ClearStorageFunction;
106
+ clear?: never;
107
+ });
108
+ }
109
+ export interface TranslationStoreState {
110
+ /**
111
+ * the unique id of the consumer of i18n-keyless API, to help identify the usage API side
112
+ */
113
+ uniqueId: UniqueId;
114
+ /**
115
+ * the last refresh of the translations, to only fetch the new ones if any
116
+ */
117
+ lastRefresh: LastRefresh;
118
+ /**
119
+ * the translations fetched from i18n-keyless' API
120
+ */
121
+ translations: Translations;
122
+ /**
123
+ * the current language of the user
124
+ */
125
+ currentLanguage: Lang;
126
+ /**
127
+ * i18n-keyless' config
128
+ */
129
+ config: I18nConfig;
130
+ }
131
+ export type TranslationOptions = {
132
+ /**
133
+ * The context of the translation.
134
+ * Useful for ambiguous translations, like "8 heures" in French could be "8 AM" or "8 hours".
135
+ * You'll find it useful when it occurs to you, don't worry :)
136
+ */
137
+ context?: string;
138
+ /**
139
+ * Could be helpful if something weird happens with this particular key.
140
+ */
141
+ debug?: boolean;
142
+ /**
143
+ * If the proposed translation from AI is not satisfactory,
144
+ * you can use this field to setup your own translation.
145
+ * You can leave it there forever, or remove it once your translation is saved.
146
+ */
147
+ forceTemporary?: Partial<Record<Lang, string>>;
148
+ };
149
+ interface TranslationStoreActions {
150
+ setTranslations: (translations: I18nKeylessResponse | void) => void;
151
+ setLanguage: (lang: Lang) => void;
152
+ }
153
+ export type TranslationStore = TranslationStoreState & TranslationStoreActions;
154
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { I18nConfig } from "i18n-keyless-core";
1
+ import type { I18nConfig } from "./types";
2
2
  /**
3
3
  * The keys used to store i18n-keyless data in storage
4
4
  */
@@ -36,3 +36,11 @@ export declare function deleteItem(key: string, storage: I18nConfig["storage"]):
36
36
  * @param storage - The storage implementation to clear
37
37
  */
38
38
  export declare function clearI18nKeylessStorage(storage: I18nConfig["storage"]): Promise<void>;
39
+ /**
40
+ * Validates the language against the supported languages
41
+ * @param lang - The language to validate
42
+ * @param config - The configuration object
43
+ * @returns The validated language or the fallback language if not supported
44
+ * @throws Error if config is not initialized
45
+ */
46
+ export declare function validateLanguage(lang: I18nConfig["languages"]["supported"][number], config: I18nConfig): import("i18n-keyless-core").Lang | undefined;
@@ -99,3 +99,19 @@ export async function clearI18nKeylessStorage(storage) {
99
99
  deleteItem(key, storage);
100
100
  }
101
101
  }
102
+ /**
103
+ * Validates the language against the supported languages
104
+ * @param lang - The language to validate
105
+ * @param config - The configuration object
106
+ * @returns The validated language or the fallback language if not supported
107
+ * @throws Error if config is not initialized
108
+ */
109
+ export function validateLanguage(lang, config) {
110
+ if (!config.API_KEY) {
111
+ throw new Error(`i18n-keyless: config is not initialized validating language`);
112
+ }
113
+ if (!config.languages.supported.includes(lang)) {
114
+ return config.languages.fallback;
115
+ }
116
+ return lang;
117
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-react",
3
3
  "private": false,
4
- "version": "1.10.5",
4
+ "version": "1.10.7",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -13,7 +13,8 @@
13
13
  "prepublishOnly": "rm -rf ./dist && tsc --project tsconfig.json && npm run test && npm pack",
14
14
  "test": "vitest run",
15
15
  "test:watch": "vitest",
16
- "test:coverage": "vitest run --coverage"
16
+ "test:coverage": "vitest run --coverage",
17
+ "postpublish": "rm -rf ./dist && rm *.tgz"
17
18
  },
18
19
  "dependencies": {
19
20
  "i18n-keyless-core": "latest",
package/dist/index.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export { I18nKeylessText } from "./I18nKeylessText";
2
- export { init, setCurrentLanguage, useCurrentLanguage, getTranslation } from "./store";
3
- export { clearI18nKeylessStorage } from "./utils";
4
- export type { I18nKeylessTextProps } from "./I18nKeylessText";
5
- export { type I18nConfig, type Lang, type PrimaryLang, type Translations, type TranslationStore, type TranslationStoreState, type I18nKeylessRequestBody, type I18nKeylessResponse, type TranslationOptions, getAllTranslationsFromLanguage, validateLanguage, queue, } from "i18n-keyless-core";
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export { I18nKeylessText } from "./I18nKeylessText";
2
- export { init, setCurrentLanguage, useCurrentLanguage, getTranslation } from "./store";
3
- export { clearI18nKeylessStorage } from "./utils";
4
- export { getAllTranslationsFromLanguage, validateLanguage, queue, } from "i18n-keyless-core";
File without changes
File without changes