i18n-keyless-core 1.8.0 → 1.9.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/dist/api.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare const api: {
2
+ fetchTranslations: (url: string, options: RequestInit) => Promise<any>;
3
+ fetchTranslation: (url: string, options: RequestInit) => Promise<any>;
4
+ };
package/dist/api.js ADDED
@@ -0,0 +1,16 @@
1
+ export const api = {
2
+ fetchTranslations: 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
+ fetchTranslation: 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
+ };
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export type { I18nConfig, Lang, PrimaryLang, Translations, TranslationStore, I18nKeylessRequestBody, I18nKeylessResponse, TranslationOptions, } from "./types";
1
+ export type { I18nConfig, Lang, PrimaryLang, Translations, TranslationStore, TranslationStoreState, I18nKeylessRequestBody, I18nKeylessResponse, TranslationOptions, } from "./types";
2
+ export { getTranslation, fetchAllTranslations, validateLanguage, queue } from "./service";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export { getTranslation, fetchAllTranslations, validateLanguage, 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,20 @@
1
+ {
2
+ "name": "i18n-keyless-core",
3
+ "private": false,
4
+ "version": "1.9.1",
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
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.5.5",
17
+ "@types/react": "^19.0.8",
18
+ "@types/react-dom": "^19.0.3"
19
+ }
20
+ }
@@ -0,0 +1,35 @@
1
+ import type { I18nConfig, Lang, TranslationStore, TranslationOptions, I18nKeylessResponse } from "i18n-keyless-core";
2
+ import MyPQueue from "./my-pqueue";
3
+ export declare const queue: MyPQueue;
4
+ /**
5
+ * Validates the language against the supported languages
6
+ * @param lang - The language to validate
7
+ * @param config - The configuration object
8
+ * @returns The validated language or the fallback language if not supported
9
+ * @throws Error if config is not initialized
10
+ */
11
+ export declare function validateLanguage(lang: I18nConfig["languages"]["supported"][number], config: I18nConfig): Lang | undefined;
12
+ /**
13
+ * Gets a translation for the specified key from the store
14
+ * @param key - The translation key (text in primary language)
15
+ * @param store - The translation store containing translations and config
16
+ * @param options - Optional parameters for translation retrieval
17
+ * @returns The translated text or the original key if not found
18
+ * @throws Error if config is not initialized
19
+ */
20
+ export declare function getTranslation(key: string, store: TranslationStore, options?: TranslationOptions): string;
21
+ /**
22
+ * Queues a key for translation if not already translated
23
+ * @param key - The text to translate
24
+ * @param store - The translation store
25
+ * @param options - Optional parameters for the translation process
26
+ * @throws Error if config is not initialized
27
+ */
28
+ export declare function translateKey(key: string, store: TranslationStore, options?: TranslationOptions): void;
29
+ /**
30
+ * Fetches all translations for a target language
31
+ * @param targetLanguage - The language code to fetch translations for
32
+ * @param store - The translation store
33
+ * @returns Promise resolving to the translation response or void if failed
34
+ */
35
+ export declare function fetchAllTranslations(targetLanguage: Lang, store: TranslationStore): Promise<I18nKeylessResponse | void>;
@@ -0,0 +1,179 @@
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
+ * Validates the language against the supported languages
7
+ * @param lang - The language to validate
8
+ * @param config - The configuration object
9
+ * @returns The validated language or the fallback language if not supported
10
+ * @throws Error if config is not initialized
11
+ */
12
+ export function validateLanguage(lang, config) {
13
+ if (!config) {
14
+ throw new Error(`i18n-keyless: config is not initialized validating language`);
15
+ }
16
+ if (!config.languages.supported.includes(lang)) {
17
+ return config.languages.fallback;
18
+ }
19
+ return lang;
20
+ }
21
+ /**
22
+ * Gets a translation for the specified key from the store
23
+ * @param key - The translation key (text in primary language)
24
+ * @param store - The translation store containing translations and config
25
+ * @param options - Optional parameters for translation retrieval
26
+ * @returns The translated text or the original key if not found
27
+ * @throws Error if config is not initialized
28
+ */
29
+ export function getTranslation(key, store, options) {
30
+ const currentLanguage = store.currentLanguage;
31
+ const config = store.config;
32
+ const translations = store.translations;
33
+ if (!config) {
34
+ throw new Error("i18n-keyless: config is not initialized");
35
+ }
36
+ if (currentLanguage === config.languages.primary) {
37
+ return key;
38
+ }
39
+ if (options?.forceTemporary?.[currentLanguage]) {
40
+ translateKey(key, store, options);
41
+ }
42
+ const context = options?.context;
43
+ const translation = context ? translations[`${key}__${context}`] : translations[key];
44
+ if (!translation) {
45
+ translateKey(key, store, options);
46
+ }
47
+ return translation || key;
48
+ }
49
+ const translating = {};
50
+ /**
51
+ * Queues a key for translation if not already translated
52
+ * @param key - The text to translate
53
+ * @param store - The translation store
54
+ * @param options - Optional parameters for the translation process
55
+ * @throws Error if config is not initialized
56
+ */
57
+ export function translateKey(key, store, options) {
58
+ const currentLanguage = store.currentLanguage;
59
+ const config = store.config;
60
+ const translations = store.translations;
61
+ const uniqueId = store.uniqueId;
62
+ if (!config) {
63
+ throw new Error("i18n-keyless: config is not initialized");
64
+ }
65
+ const context = options?.context;
66
+ const debug = options?.debug;
67
+ // if (key.length > 280) {
68
+ // console.error("i18n-keyless: Key length exceeds 280 characters limit:", key);
69
+ // return;
70
+ // }
71
+ if (!key) {
72
+ return;
73
+ }
74
+ if (debug) {
75
+ console.log("translateKey", key, context, debug);
76
+ }
77
+ const forceTemporaryLang = options?.forceTemporary?.[currentLanguage];
78
+ const translation = context ? translations[`${key}__${context}`] : translations[key];
79
+ if (translation && !forceTemporaryLang) {
80
+ if (debug) {
81
+ console.log("translation exists", `${key}__${context}`);
82
+ }
83
+ return;
84
+ }
85
+ queue.add(async () => {
86
+ try {
87
+ if (translating[key]) {
88
+ return;
89
+ }
90
+ else {
91
+ translating[key] = true;
92
+ }
93
+ if (config.handleTranslate) {
94
+ await config.handleTranslate?.(key);
95
+ }
96
+ else {
97
+ const body = {
98
+ key,
99
+ context,
100
+ forceTemporary: options?.forceTemporary,
101
+ languages: config.languages.supported,
102
+ primaryLanguage: config.languages.primary,
103
+ };
104
+ const apiUrl = config.API_URL || "https://api.i18n-keyless.com";
105
+ const url = `${apiUrl}/translate`;
106
+ if (debug) {
107
+ console.log("fetching translation", url, body);
108
+ }
109
+ const response = await api
110
+ .fetchTranslation(url, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json",
114
+ Authorization: `Bearer ${config.API_KEY}`,
115
+ unique_id: uniqueId || "",
116
+ Version: packageJson.version,
117
+ },
118
+ body: JSON.stringify(body),
119
+ })
120
+ .then((res) => res);
121
+ if (debug) {
122
+ console.log("response", response);
123
+ }
124
+ if (response.message) {
125
+ console.warn("i18n-keyless: ", response.message);
126
+ }
127
+ }
128
+ translating[key] = false;
129
+ return;
130
+ }
131
+ catch (error) {
132
+ console.error("i18n-keyless: Error translating key:", error);
133
+ translating[key] = false;
134
+ }
135
+ }, { priority: 1, id: key });
136
+ }
137
+ /**
138
+ * Fetches all translations for a target language
139
+ * @param targetLanguage - The language code to fetch translations for
140
+ * @param store - The translation store
141
+ * @returns Promise resolving to the translation response or void if failed
142
+ */
143
+ export async function fetchAllTranslations(targetLanguage, store) {
144
+ const config = store.config;
145
+ const lastRefresh = store.lastRefresh;
146
+ const uniqueId = store.uniqueId;
147
+ if (!config) {
148
+ console.error("i18n-keyless: No config found");
149
+ return;
150
+ }
151
+ // if (config.languages.primary === targetLanguage) {
152
+ // return;
153
+ // }
154
+ try {
155
+ const response = config.getAllTranslations
156
+ ? await config.getAllTranslations()
157
+ : await api
158
+ .fetchTranslations(`${config.API_URL || "https://api.i18n-keyless.com"}/translate/${targetLanguage}?last_refresh=${lastRefresh}`, {
159
+ method: "GET",
160
+ headers: {
161
+ "Content-Type": "application/json",
162
+ Authorization: `Bearer ${config.API_KEY}`,
163
+ Version: packageJson.version,
164
+ unique_id: uniqueId || "",
165
+ },
166
+ })
167
+ .then((res) => res);
168
+ if (!response.ok) {
169
+ throw new Error(response.error);
170
+ }
171
+ if (response.message) {
172
+ console.warn("i18n-keyless: ", response.message);
173
+ }
174
+ return response;
175
+ }
176
+ catch (error) {
177
+ console.error("i18n-keyless: fetch all translations error:", error);
178
+ }
179
+ }
package/dist/types.d.ts CHANGED
@@ -132,15 +132,27 @@ export interface I18nConfig {
132
132
  clear?: never;
133
133
  });
134
134
  }
135
- interface TranslationStoreState {
135
+ export interface TranslationStoreState {
136
+ /**
137
+ * the unique id of the consumer of i18n-keyless API, to help identify the usage API side
138
+ */
136
139
  uniqueId: string | null;
140
+ /**
141
+ * the last refresh of the translations, to only fetch the new ones if any
142
+ */
137
143
  lastRefresh: string | null;
144
+ /**
145
+ * the translations fetched from i18n-keyless' API
146
+ */
138
147
  translations: Translations;
148
+ /**
149
+ * the current language of the user
150
+ */
139
151
  currentLanguage: Lang;
152
+ /**
153
+ * i18n-keyless' config
154
+ */
140
155
  config: I18nConfig | null;
141
- translating: Record<string, boolean>;
142
- _hasHydrated: boolean;
143
- storage: I18nConfig["storage"] | null;
144
156
  }
145
157
  export type TranslationOptions = {
146
158
  /**
@@ -161,10 +173,7 @@ export type TranslationOptions = {
161
173
  forceTemporary?: Partial<Record<Lang, string>>;
162
174
  };
163
175
  interface TranslationStoreActions {
164
- _hydrate: () => Promise<void>;
165
- getTranslation: (text: string, options?: TranslationOptions) => string | undefined;
166
- setTranslations: (translations: Translations) => void;
167
- translateKey: (key: string, options?: TranslationOptions) => void;
176
+ setTranslations: (translations: I18nKeylessResponse | void) => void;
168
177
  setLanguage: (lang: Lang) => void;
169
178
  }
170
179
  export type TranslationStore = TranslationStoreState & TranslationStoreActions;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18n-keyless-core",
3
3
  "private": false,
4
- "version": "1.8.0",
4
+ "version": "1.9.1",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",