@ccgp/i18n-ai 0.0.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/README.md +83 -0
- package/dist/chunk-QBRFUS6N.mjs +292 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +356 -0
- package/dist/cli.mjs +53 -0
- package/dist/index.d.mts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +338 -0
- package/dist/index.mjs +26 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
TranslationService: () => TranslationService,
|
|
34
|
+
analyzeChanges: () => analyzeChanges,
|
|
35
|
+
cleanupLock: () => cleanupLock,
|
|
36
|
+
detectChange: () => detectChange,
|
|
37
|
+
flattenObject: () => flattenObject,
|
|
38
|
+
generateHash: () => generateHash,
|
|
39
|
+
loadLock: () => loadLock,
|
|
40
|
+
saveLock: () => saveLock,
|
|
41
|
+
translate: () => translate,
|
|
42
|
+
unflattenObject: () => unflattenObject,
|
|
43
|
+
updateLockEntry: () => updateLockEntry
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/core/lock-manager.ts
|
|
48
|
+
var import_crypto = require("crypto");
|
|
49
|
+
var import_promises = require("fs/promises");
|
|
50
|
+
function generateHash(text) {
|
|
51
|
+
return (0, import_crypto.createHash)("sha256").update(text).digest("hex").substring(0, 16);
|
|
52
|
+
}
|
|
53
|
+
async function loadLock(lockPath) {
|
|
54
|
+
try {
|
|
55
|
+
const content = await (0, import_promises.readFile)(lockPath, "utf-8");
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
version: "1.0.0",
|
|
60
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
locales: {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function saveLock(lockPath, lock) {
|
|
66
|
+
lock.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
67
|
+
const content = JSON.stringify(lock, null, 2);
|
|
68
|
+
await (0, import_promises.writeFile)(lockPath, content + "\n", "utf-8");
|
|
69
|
+
}
|
|
70
|
+
function detectChange(key, locale, sourceText, currentTranslation, lock) {
|
|
71
|
+
const localeData = lock.locales[locale];
|
|
72
|
+
const lockEntry = localeData?.[key];
|
|
73
|
+
if (!lockEntry) {
|
|
74
|
+
return {
|
|
75
|
+
key,
|
|
76
|
+
status: "new",
|
|
77
|
+
sourceText,
|
|
78
|
+
currentTranslation
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const currentHash = generateHash(sourceText);
|
|
82
|
+
if (currentHash !== lockEntry.sourceHash) {
|
|
83
|
+
return {
|
|
84
|
+
key,
|
|
85
|
+
status: "modified",
|
|
86
|
+
sourceText,
|
|
87
|
+
currentTranslation,
|
|
88
|
+
previousSourceText: lockEntry.sourceText
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
key,
|
|
93
|
+
status: "unchanged",
|
|
94
|
+
sourceText,
|
|
95
|
+
currentTranslation: lockEntry.translation
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function updateLockEntry(lock, locale, key, sourceText, translation) {
|
|
99
|
+
if (!lock.locales[locale]) {
|
|
100
|
+
lock.locales[locale] = {};
|
|
101
|
+
}
|
|
102
|
+
lock.locales[locale][key] = {
|
|
103
|
+
sourceHash: generateHash(sourceText),
|
|
104
|
+
sourceText,
|
|
105
|
+
translation,
|
|
106
|
+
translatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function analyzeChanges(baseFlat, targetFlat, locale, lock) {
|
|
110
|
+
const result = {
|
|
111
|
+
new: [],
|
|
112
|
+
modified: [],
|
|
113
|
+
unchanged: []
|
|
114
|
+
};
|
|
115
|
+
for (const [key, sourceText] of baseFlat.entries()) {
|
|
116
|
+
const currentTranslation = targetFlat.get(key);
|
|
117
|
+
const change = detectChange(key, locale, sourceText, currentTranslation, lock);
|
|
118
|
+
result[change.status].push(change);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function cleanupLock(lock, locale, validKeys) {
|
|
123
|
+
if (!lock.locales[locale]) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
const currentKeys = Object.keys(lock.locales[locale]);
|
|
127
|
+
let removedCount = 0;
|
|
128
|
+
for (const key of currentKeys) {
|
|
129
|
+
if (!validKeys.has(key)) {
|
|
130
|
+
delete lock.locales[locale][key];
|
|
131
|
+
removedCount++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return removedCount;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/core/translator.ts
|
|
138
|
+
var import_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
|
|
139
|
+
var import_ai = require("ai");
|
|
140
|
+
var import_zod = __toESM(require("zod"));
|
|
141
|
+
var translate = async ({ text, lang, targetLang, apiKey, model }) => {
|
|
142
|
+
const token = apiKey || process.env.OPENROUTER_API_KEY || process.env.AI_API_KEY;
|
|
143
|
+
if (!token) {
|
|
144
|
+
throw new Error("Missing API Key. Please provide apiKey or set OPENROUTER_API_KEY / AI_API_KEY environment variable.");
|
|
145
|
+
}
|
|
146
|
+
const openrouter = (0, import_ai_sdk_provider.createOpenRouter)({
|
|
147
|
+
apiKey: token
|
|
148
|
+
});
|
|
149
|
+
const { output } = await (0, import_ai.generateText)({
|
|
150
|
+
model: openrouter.languageModel(model || "google/gemini-2.5-flash"),
|
|
151
|
+
prompt: `Translate the following text from ${lang} to ${targetLang}. Return ONLY the translated text, no explanations or additional text.
|
|
152
|
+
|
|
153
|
+
Text to translate: ${text}`,
|
|
154
|
+
output: import_ai.Output.object({
|
|
155
|
+
schema: import_zod.default.object({
|
|
156
|
+
translatedText: import_zod.default.string()
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
return output.translatedText;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/core/sync.ts
|
|
164
|
+
var import_promises2 = require("fs/promises");
|
|
165
|
+
var import_path = require("path");
|
|
166
|
+
var import_chalk = __toESM(require("chalk"));
|
|
167
|
+
function flattenObject(obj, prefix = "") {
|
|
168
|
+
const result = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
170
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
171
|
+
if (typeof value === "string") {
|
|
172
|
+
result.set(fullPath, value);
|
|
173
|
+
} else if (typeof value === "object" && value !== null) {
|
|
174
|
+
const nested = flattenObject(value, fullPath);
|
|
175
|
+
nested.forEach((val, path) => result.set(path, val));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function unflattenObject(flatMap) {
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const [path, value] of flatMap.entries()) {
|
|
183
|
+
const keys = path.split(".");
|
|
184
|
+
let current = result;
|
|
185
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
186
|
+
const key = keys[i];
|
|
187
|
+
if (!current[key]) {
|
|
188
|
+
current[key] = {};
|
|
189
|
+
}
|
|
190
|
+
current = current[key];
|
|
191
|
+
}
|
|
192
|
+
current[keys[keys.length - 1]] = value;
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
var TranslationService = class {
|
|
197
|
+
constructor(config) {
|
|
198
|
+
this.config = config;
|
|
199
|
+
}
|
|
200
|
+
async sync() {
|
|
201
|
+
console.log(import_chalk.default.bold("\u{1F30D} AI Translation Synchronization System"));
|
|
202
|
+
console.log(import_chalk.default.dim("\u2550".repeat(50)));
|
|
203
|
+
console.log("");
|
|
204
|
+
try {
|
|
205
|
+
console.log(`\u2713 Locales found: ${import_chalk.default.green(this.config.locales.join(", "))}`);
|
|
206
|
+
console.log(`\u2713 Base locale: ${import_chalk.default.cyan(this.config.defaultLocale)}`);
|
|
207
|
+
const basePath = (0, import_path.join)(this.config.messagesDir, `${this.config.defaultLocale}.json`);
|
|
208
|
+
let baseContent;
|
|
209
|
+
try {
|
|
210
|
+
baseContent = await (0, import_promises2.readFile)(basePath, "utf-8");
|
|
211
|
+
} catch (e) {
|
|
212
|
+
throw new Error(`Could not read base locale file at ${basePath}`);
|
|
213
|
+
}
|
|
214
|
+
const baseObj = JSON.parse(baseContent);
|
|
215
|
+
const baseFlat = flattenObject(baseObj);
|
|
216
|
+
console.log(`\u{1F4D6} Loading ${this.config.defaultLocale}.json (${baseFlat.size} keys total)`);
|
|
217
|
+
console.log("\u{1F510} Loading lock file...");
|
|
218
|
+
const lock = await loadLock(this.config.lockFilePath);
|
|
219
|
+
const targetLocales = this.config.locales.filter((l) => l !== this.config.defaultLocale);
|
|
220
|
+
for (const targetLocale of targetLocales) {
|
|
221
|
+
await this.syncLocale(targetLocale, baseFlat, lock);
|
|
222
|
+
}
|
|
223
|
+
await saveLock(this.config.lockFilePath, lock);
|
|
224
|
+
console.log(`
|
|
225
|
+
\u{1F510} Lock file updated at ${this.config.lockFilePath}`);
|
|
226
|
+
console.log(import_chalk.default.dim("\n\u2550".repeat(50)));
|
|
227
|
+
console.log(import_chalk.default.green("\u2705 Synchronization complete!\n"));
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error(import_chalk.default.red("\n\u274C Error during synchronization:"), error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async syncLocale(targetLocale, baseFlat, lock) {
|
|
234
|
+
console.log(`
|
|
235
|
+
\u{1F50E} Synchronizing: ${import_chalk.default.yellow(targetLocale)}`);
|
|
236
|
+
console.log(import_chalk.default.dim("\u2500".repeat(50)));
|
|
237
|
+
const targetPath = (0, import_path.join)(this.config.messagesDir, `${targetLocale}.json`);
|
|
238
|
+
let targetObj = {};
|
|
239
|
+
try {
|
|
240
|
+
const targetContent = await (0, import_promises2.readFile)(targetPath, "utf-8");
|
|
241
|
+
targetObj = JSON.parse(targetContent);
|
|
242
|
+
} catch {
|
|
243
|
+
console.log(import_chalk.default.yellow(`\u26A0\uFE0F File ${targetLocale}.json does not exist, creating new one
|
|
244
|
+
`));
|
|
245
|
+
}
|
|
246
|
+
const targetFlat = flattenObject(targetObj);
|
|
247
|
+
const changes = analyzeChanges(baseFlat, targetFlat, targetLocale, lock);
|
|
248
|
+
const validKeys = new Set(baseFlat.keys());
|
|
249
|
+
const obsoleteKeys = [];
|
|
250
|
+
for (const key of targetFlat.keys()) {
|
|
251
|
+
if (!validKeys.has(key)) {
|
|
252
|
+
obsoleteKeys.push(key);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
console.log("\n\u{1F4CA} Change analysis:");
|
|
256
|
+
console.log(` \u{1F4DD} ${changes.new.length} new key(s)`);
|
|
257
|
+
console.log(` \u{1F504} ${changes.modified.length} modified key(s)`);
|
|
258
|
+
console.log(` \u2705 ${changes.unchanged.length} unchanged key(s)`);
|
|
259
|
+
console.log(` \u{1F5D1}\uFE0F ${obsoleteKeys.length} obsolete key(s)`);
|
|
260
|
+
const toTranslate = [...changes.new, ...changes.modified];
|
|
261
|
+
if (toTranslate.length === 0 && obsoleteKeys.length === 0) {
|
|
262
|
+
console.log(import_chalk.default.green(`
|
|
263
|
+
\u2705 ${targetLocale}.json is already fully synchronized`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (changes.new.length > 0) {
|
|
267
|
+
console.log("\n\u{1F4DD} New keys:");
|
|
268
|
+
changes.new.forEach(({ key }) => console.log(import_chalk.default.green(` + ${key}`)));
|
|
269
|
+
}
|
|
270
|
+
if (changes.modified.length > 0) {
|
|
271
|
+
console.log("\n\u{1F504} Modified keys:");
|
|
272
|
+
changes.modified.forEach(({ key, previousSourceText, sourceText }) => {
|
|
273
|
+
console.log(import_chalk.default.yellow(` ~ ${key}`));
|
|
274
|
+
console.log(import_chalk.default.dim(` Old: "${previousSourceText}"`));
|
|
275
|
+
console.log(import_chalk.default.dim(` New: "${sourceText}"`));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (toTranslate.length > 0) {
|
|
279
|
+
console.log(import_chalk.default.blue(`
|
|
280
|
+
\u{1F916} Translating ${toTranslate.length} keys with AI...`));
|
|
281
|
+
console.log(import_chalk.default.dim("\u2500".repeat(50)));
|
|
282
|
+
}
|
|
283
|
+
let translatedCount = 0;
|
|
284
|
+
if (toTranslate.length > 0) {
|
|
285
|
+
for (const change of toTranslate) {
|
|
286
|
+
try {
|
|
287
|
+
const translatedValue = await translate({
|
|
288
|
+
text: change.sourceText,
|
|
289
|
+
lang: this.config.defaultLocale,
|
|
290
|
+
targetLang: targetLocale,
|
|
291
|
+
apiKey: this.config.apiKey,
|
|
292
|
+
model: this.config.model
|
|
293
|
+
});
|
|
294
|
+
targetFlat.set(change.key, translatedValue);
|
|
295
|
+
updateLockEntry(lock, targetLocale, change.key, change.sourceText, translatedValue);
|
|
296
|
+
translatedCount++;
|
|
297
|
+
const icon = change.status === "new" ? "\u2795" : "\u{1F504}";
|
|
298
|
+
console.log(` ${icon} ${change.key}`);
|
|
299
|
+
console.log(import_chalk.default.dim(` "${change.sourceText}"`));
|
|
300
|
+
console.log(import_chalk.default.cyan(` \u2192 "${translatedValue}"`));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(import_chalk.default.red(` \u274C Error translating ${change.key}:`), error);
|
|
303
|
+
if (change.currentTranslation) {
|
|
304
|
+
targetFlat.set(change.key, change.currentTranslation);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const change of changes.unchanged) {
|
|
310
|
+
if (change.currentTranslation) {
|
|
311
|
+
targetFlat.set(change.key, change.currentTranslation);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const key of obsoleteKeys) {
|
|
315
|
+
targetFlat.delete(key);
|
|
316
|
+
}
|
|
317
|
+
cleanupLock(lock, targetLocale, validKeys);
|
|
318
|
+
const updatedObj = unflattenObject(targetFlat);
|
|
319
|
+
const updatedContent = JSON.stringify(updatedObj, null, 2);
|
|
320
|
+
await (0, import_promises2.writeFile)(targetPath, updatedContent + "\n", "utf-8");
|
|
321
|
+
console.log(import_chalk.default.green(`
|
|
322
|
+
\u{1F4BE} ${targetPath} updated`));
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
326
|
+
0 && (module.exports = {
|
|
327
|
+
TranslationService,
|
|
328
|
+
analyzeChanges,
|
|
329
|
+
cleanupLock,
|
|
330
|
+
detectChange,
|
|
331
|
+
flattenObject,
|
|
332
|
+
generateHash,
|
|
333
|
+
loadLock,
|
|
334
|
+
saveLock,
|
|
335
|
+
translate,
|
|
336
|
+
unflattenObject,
|
|
337
|
+
updateLockEntry
|
|
338
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TranslationService,
|
|
3
|
+
analyzeChanges,
|
|
4
|
+
cleanupLock,
|
|
5
|
+
detectChange,
|
|
6
|
+
flattenObject,
|
|
7
|
+
generateHash,
|
|
8
|
+
loadLock,
|
|
9
|
+
saveLock,
|
|
10
|
+
translate,
|
|
11
|
+
unflattenObject,
|
|
12
|
+
updateLockEntry
|
|
13
|
+
} from "./chunk-QBRFUS6N.mjs";
|
|
14
|
+
export {
|
|
15
|
+
TranslationService,
|
|
16
|
+
analyzeChanges,
|
|
17
|
+
cleanupLock,
|
|
18
|
+
detectChange,
|
|
19
|
+
flattenObject,
|
|
20
|
+
generateHash,
|
|
21
|
+
loadLock,
|
|
22
|
+
saveLock,
|
|
23
|
+
translate,
|
|
24
|
+
unflattenObject,
|
|
25
|
+
updateLockEntry
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ccgp/i18n-ai",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "AI-powered i18n translation and synchronization library",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"i18n-ai": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts --clean",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"start": "node dist/cli.js",
|
|
22
|
+
"lint": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "bun run build"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/carlosgaravito/i18n-ai.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"i18n",
|
|
31
|
+
"ai",
|
|
32
|
+
"translation",
|
|
33
|
+
"automation"
|
|
34
|
+
],
|
|
35
|
+
"author": "Carlos Garavito",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@openrouter/ai-sdk-provider": "^1.5.4",
|
|
39
|
+
"ai": "^6.0.20",
|
|
40
|
+
"chalk": "^5.4.1",
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"dotenv": "^16.4.5",
|
|
43
|
+
"zod": "^4.3.5"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.11.24",
|
|
47
|
+
"tsup": "^8.3.5",
|
|
48
|
+
"tsx": "^4.7.1",
|
|
49
|
+
"typescript": "^5.3.3"
|
|
50
|
+
}
|
|
51
|
+
}
|