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 +4 -0
- package/dist/api.js +16 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/my-pqueue.d.ts +24 -0
- package/dist/my-pqueue.js +108 -0
- package/dist/package.json +20 -0
- package/dist/service.d.ts +35 -0
- package/dist/service.js +179 -0
- package/dist/types.d.ts +17 -8
- package/package.json +1 -1
package/dist/api.d.ts
ADDED
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>;
|
package/dist/service.js
ADDED
|
@@ -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
|
-
|
|
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;
|