lingui-po-translate 1.0.0 → 1.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.
Files changed (45) hide show
  1. package/dist/core/core-definitions.js +21 -0
  2. package/dist/core/core-util.js +65 -0
  3. package/dist/core/invoke-translation-service.js +82 -0
  4. package/dist/core/translate-cli.js +113 -0
  5. package/dist/core/translate-core.js +230 -0
  6. package/dist/core/tset-ops.js +110 -0
  7. package/dist/file-formats/common/format-cache.js +72 -0
  8. package/dist/file-formats/common/managed-json.js +37 -0
  9. package/dist/file-formats/common/managed-utf8.js +69 -0
  10. package/dist/file-formats/common/parse-utils.js +14 -0
  11. package/dist/file-formats/csv/csv.js +64 -0
  12. package/dist/file-formats/file-format-definitions.js +43 -0
  13. package/dist/file-formats/flat-json/flat-json.js +29 -0
  14. package/dist/file-formats/flutter-arb/flutter-arb.js +62 -0
  15. package/dist/file-formats/ios-strings/ios-read.js +72 -0
  16. package/dist/file-formats/ios-strings/ios-strings.js +22 -0
  17. package/dist/file-formats/ios-strings/ios-write.js +37 -0
  18. package/dist/file-formats/nested-json/nested-json.js +66 -0
  19. package/dist/file-formats/po/comment-parser.js +68 -0
  20. package/dist/file-formats/po/po-files.js +78 -0
  21. package/dist/file-formats/po/po-ops.js +111 -0
  22. package/dist/file-formats/xml/xml-generic.js +81 -0
  23. package/dist/file-formats/xml/xml-read.js +66 -0
  24. package/dist/file-formats/xml/xml-traverse.js +160 -0
  25. package/dist/file-formats/xml/xml-write.js +62 -0
  26. package/dist/file-formats/yaml/yaml-generic.js +113 -0
  27. package/dist/file-formats/yaml/yaml-manipulation.js +132 -0
  28. package/dist/file-formats/yaml/yaml-parse.js +38 -0
  29. package/dist/index.js +66 -0
  30. package/dist/matchers/i18next.js +11 -0
  31. package/dist/matchers/icu.js +23 -0
  32. package/dist/matchers/matcher-definitions.js +46 -0
  33. package/dist/matchers/sprintf.js +11 -0
  34. package/dist/services/azure-translator.js +52 -0
  35. package/dist/services/google-translate.js +55 -0
  36. package/dist/services/key-as-translation.js +18 -0
  37. package/dist/services/manual.js +29 -0
  38. package/dist/services/openai-translate.js +91 -0
  39. package/dist/services/service-definitions.js +57 -0
  40. package/dist/services/sync-without-translate.js +18 -0
  41. package/dist/services/typechat.js +214 -0
  42. package/dist/util/extract-version.js +20 -0
  43. package/dist/util/flatten.js +40 -0
  44. package/dist/util/util.js +87 -0
  45. package/package.json +2 -2
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TypeChatTranslate = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const axios_1 = (0, tslib_1.__importDefault)(require("axios"));
6
+ const clipboardy_1 = (0, tslib_1.__importDefault)(require("clipboardy"));
7
+ const inquirer_1 = (0, tslib_1.__importDefault)(require("inquirer"));
8
+ const lodash_1 = require("lodash");
9
+ const typechat_1 = require("typechat");
10
+ const util_1 = require("../util/util");
11
+ const MINUTE_MS = 60 * 1000;
12
+ function generateSchema(batch, name, comment) {
13
+ const properties = batch
14
+ .map((tString) => {
15
+ return ` '${tString.key}': string;`;
16
+ })
17
+ .join("\n");
18
+ const schema = `export interface ${name} {\n${properties}\n}\n`;
19
+ return comment ? `// ${comment} \n\n${schema}` : schema;
20
+ }
21
+ function generatePrompt(batch, args) {
22
+ const entries = batch.reduce((entries, tString) => {
23
+ entries[tString.key] = tString.value;
24
+ return entries;
25
+ }, {});
26
+ // Build context info for entries that have context
27
+ const contextInfo = batch
28
+ .filter((tString) => tString.context)
29
+ .map((tString) => `- "${tString.key}": ${tString.context}`)
30
+ .join("\n");
31
+ const basePrompt = `Translate the following JSON object from ${args.srcLng} into ${args.targetLng}:\n`;
32
+ const customPrompt = args.prompt
33
+ ? `\nAdditional instructions: ${args.prompt}\n\n`
34
+ : "\n";
35
+ const contextPrompt = contextInfo
36
+ ? `\nContext for specific keys:\n${contextInfo}\n\n`
37
+ : "";
38
+ return basePrompt + customPrompt + contextPrompt + JSON.stringify(entries, null, 2);
39
+ }
40
+ function parseResponse(batch, data) {
41
+ return batch.map((tString) => {
42
+ var _a;
43
+ const result = {
44
+ key: tString.key,
45
+ translated: (_a = data[tString.key]) !== null && _a !== void 0 ? _a : "",
46
+ };
47
+ return result;
48
+ });
49
+ }
50
+ async function translateBatch(model, batch, args, env) {
51
+ var _a;
52
+ console.log("Translate a batch of " + batch.length + " strings with TypeChat...");
53
+ const schemaName = (_a = env.TYPECHAT_SCHEMA_NAME) !== null && _a !== void 0 ? _a : "AppLocalizations";
54
+ const schemaComment = env.TYPECHAT_SCHEMA_COMMENT;
55
+ const translator = (0, typechat_1.createJsonTranslator)(model, generateSchema(batch, schemaName, schemaComment), schemaName);
56
+ const response = await translator.translate(generatePrompt(batch, args));
57
+ if (!response.success) {
58
+ (0, util_1.logFatal)(response.message);
59
+ }
60
+ return parseResponse(batch, response.data);
61
+ }
62
+ function createLanguageModel(env) {
63
+ var _a, _b, _c;
64
+ const apiKey = (_a = env.OPENAI_API_KEY) !== null && _a !== void 0 ? _a : missingEnvironmentVariable("OPENAI_API_KEY");
65
+ const model = (_b = env.OPENAI_MODEL) !== null && _b !== void 0 ? _b : "gpt-4o-mini-2024-07-18";
66
+ const url = (_c = env.OPENAI_ENDPOINT) !== null && _c !== void 0 ? _c : "https://api.openai.com/v1/chat/completions";
67
+ return createAxiosLanguageModel(url, {
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ },
71
+ }, { model });
72
+ }
73
+ function createManualModel() {
74
+ const model = {
75
+ complete,
76
+ };
77
+ return model;
78
+ async function complete(prompt) {
79
+ await clipboardy_1.default.write(prompt);
80
+ console.log(`Prompt copied to clipboard`);
81
+ await inquirer_1.default.prompt([
82
+ {
83
+ name: "Enter",
84
+ message: "Press enter after you copied the response.",
85
+ type: "input",
86
+ },
87
+ ]);
88
+ const result = await clipboardy_1.default.read();
89
+ return (0, typechat_1.success)(result);
90
+ }
91
+ }
92
+ function sanitize_keys(target, propertyKey, descriptor) {
93
+ const originalMethod = descriptor.value;
94
+ descriptor.value = async function (args) {
95
+ const sanitizedKeys = args.strings.map(({ key }) => {
96
+ return {
97
+ new: key.replace(/[^a-zA-Z0-9_]+/g, "_"),
98
+ old: key,
99
+ };
100
+ });
101
+ // Replace keys with sanitized versions
102
+ for (let i = 0; i < sanitizedKeys.length; i++) {
103
+ args.strings[i].key = sanitizedKeys[i].new;
104
+ }
105
+ // Call the original method
106
+ const results = await originalMethod.apply(this, [args]);
107
+ // Restore original keys in the results
108
+ for (let i = 0; i < sanitizedKeys.length; i++) {
109
+ results[i].key = sanitizedKeys[i].old;
110
+ }
111
+ return results;
112
+ };
113
+ return descriptor;
114
+ }
115
+ class TypeChatTranslate {
116
+ constructor(manual) {
117
+ this.manual = manual !== null && manual !== void 0 ? manual : false;
118
+ }
119
+ async translateStrings(args) {
120
+ var _a, _b;
121
+ const rpm = parseInt((_a = process.env.TYPECHAT_RPM) !== null && _a !== void 0 ? _a : "");
122
+ const batchSize = parseInt((_b = process.env.OPEN_AI_BATCH_SIZE) !== null && _b !== void 0 ? _b : "");
123
+ const batches = (0, lodash_1.chunk)(args.strings, isNaN(batchSize) ? 10 : batchSize);
124
+ const results = [];
125
+ const model = this.manual
126
+ ? createManualModel()
127
+ : createLanguageModel(process.env);
128
+ for (var i = 0; i < batches.length; i++) {
129
+ const batch = batches[i];
130
+ const start = new Date();
131
+ const result = await translateBatch(model, batch, args, process.env);
132
+ results.push(result);
133
+ // Sleep to not exceeded the specified requests per minute (RPM)
134
+ if (!this.manual && !isNaN(rpm) && rpm > 0 && i < batches.length - 1) {
135
+ const requestDuration = new Date().getTime() - start.getTime();
136
+ const sleepDuration = (MINUTE_MS / rpm) - requestDuration;
137
+ console.log(`Going to sleep for ${sleepDuration} ms`);
138
+ await sleep(sleepDuration);
139
+ }
140
+ }
141
+ return (0, lodash_1.flatten)(results);
142
+ }
143
+ }
144
+ (0, tslib_1.__decorate)([
145
+ sanitize_keys
146
+ ], TypeChatTranslate.prototype, "translateStrings", null);
147
+ exports.TypeChatTranslate = TypeChatTranslate;
148
+ // The following code is from TypeChat
149
+ // https://github.com/microsoft/TypeChat/blob/e8395ef2e4688ec7b94a7612046aeaec0af93046/src/model.ts#L65
150
+ // MIT License - https://github.com/microsoft/TypeChat/blob/main/LICENSE
151
+ // Copyright (c) Microsoft Corporation.
152
+ /**
153
+ * Common implementation of language model encapsulation of an OpenAI REST API endpoint.
154
+ */
155
+ function createAxiosLanguageModel(url, config, defaultParams) {
156
+ const client = axios_1.default.create(config);
157
+ const model = {
158
+ complete,
159
+ };
160
+ return model;
161
+ async function complete(prompt) {
162
+ var _a, _b, _c, _d;
163
+ let retryCount = 0;
164
+ const retryMaxAttempts = (_a = model.retryMaxAttempts) !== null && _a !== void 0 ? _a : 3;
165
+ const retryPauseMs = (_b = model.retryPauseMs) !== null && _b !== void 0 ? _b : 1000;
166
+ while (true) {
167
+ const params = {
168
+ max_tokens: 2048,
169
+ temperature: 0,
170
+ ...defaultParams,
171
+ messages: [{ role: "user", content: prompt }],
172
+ n: 1,
173
+ };
174
+ const result = await client.post(url, params, {
175
+ validateStatus: (status) => true,
176
+ });
177
+ if (result.status === 200) {
178
+ return (0, typechat_1.success)((_d = (_c = result.data.choices[0].message) === null || _c === void 0 ? void 0 : _c.content) !== null && _d !== void 0 ? _d : "");
179
+ }
180
+ if (result.status === 401) {
181
+ return (0, typechat_1.error)(`REST API error ${result.status}: ${result.statusText}`);
182
+ }
183
+ if (!isTransientHttpError(result.status) ||
184
+ retryCount >= retryMaxAttempts) {
185
+ return (0, typechat_1.error)(`REST API error ${result.status}: ${result.statusText}`);
186
+ }
187
+ await sleep(retryPauseMs);
188
+ retryCount++;
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Returns true of the given HTTP status code represents a transient error.
194
+ */
195
+ function isTransientHttpError(code) {
196
+ switch (code) {
197
+ case 429: // TooManyRequests
198
+ case 500: // InternalServerError
199
+ case 502: // BadGateway
200
+ case 503: // ServiceUnavailable
201
+ case 504: // GatewayTimeout
202
+ return true;
203
+ }
204
+ return false;
205
+ }
206
+ /**
207
+ * Sleeps for the given number of milliseconds.
208
+ */
209
+ function sleep(ms) {
210
+ return new Promise((resolve) => setTimeout(resolve, ms > 0 ? ms : 0));
211
+ }
212
+ function missingEnvironmentVariable(name) {
213
+ (0, util_1.logFatal)(`Missing environment variable: ${name}`);
214
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractVersion = void 0;
4
+ const path_1 = require("path");
5
+ const fs_1 = require("fs");
6
+ function extractVersion(args) {
7
+ const rootDir = (0, path_1.resolve)((0, path_1.join)(args.cliBinDir, ".."));
8
+ try {
9
+ const jsonStr = (0, fs_1.readFileSync)((0, path_1.join)(rootDir, "package.json"), {
10
+ encoding: "utf8",
11
+ flag: "r",
12
+ });
13
+ const packageJson = JSON.parse(jsonStr);
14
+ return packageJson.version;
15
+ }
16
+ catch (e) {
17
+ return e.toString() + "\nFailed to retrieve the version";
18
+ }
19
+ }
20
+ exports.extractVersion = extractVersion;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unflatten = exports.NESTED_JSON_SEPARATOR = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const lodash_1 = (0, tslib_1.__importDefault)(require("lodash"));
6
+ exports.NESTED_JSON_SEPARATOR = ".";
7
+ function replaceAll(string, search, replace) {
8
+ return string.split(search).join(replace);
9
+ }
10
+ const escapeRules = {
11
+ ".": "\\\\|",
12
+ "[": "\\\\(",
13
+ "]": "\\\\)",
14
+ };
15
+ function unescapeKey(str) {
16
+ let targetStr = str;
17
+ for (const replace of Object.keys(escapeRules)) {
18
+ const search = escapeRules[replace];
19
+ targetStr = replaceAll(targetStr, search, replace);
20
+ }
21
+ return targetStr;
22
+ }
23
+ function unescapeObject(obj) {
24
+ const targetObj = {};
25
+ for (const key of Object.keys(obj)) {
26
+ let value = obj[key];
27
+ if (typeof value === "object" && value !== null) {
28
+ value = unescapeObject(value);
29
+ }
30
+ targetObj[unescapeKey(key)] = value;
31
+ }
32
+ return targetObj;
33
+ }
34
+ function unflatten(params) {
35
+ const rawUnflattened = lodash_1.default.reduce(params, function (result, value, key) {
36
+ return lodash_1.default.set(result, key, value);
37
+ }, {});
38
+ return unescapeObject(rawUnflattened);
39
+ }
40
+ exports.unflatten = unflatten;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nodeVersionSatisfies = exports.runCommandOrDie = exports.writeUtf8File = exports.readUtf8File = exports.deleteFile = exports.logFatal = exports.getDebugPath = exports.checkNotDir = exports.checkDir = exports.joinDirWithFileName = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const child_process_1 = require("child_process");
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const semver_1 = (0, tslib_1.__importDefault)(require("semver"));
9
+ function joinDirWithFileName(dir, fileName) {
10
+ checkDir(dir);
11
+ return (0, path_1.join)((0, path_1.resolve)(dir), fileName);
12
+ }
13
+ exports.joinDirWithFileName = joinDirWithFileName;
14
+ function isDirectory(path) {
15
+ try {
16
+ const stat = (0, fs_1.lstatSync)(path);
17
+ return stat.isDirectory();
18
+ }
19
+ catch (e) {
20
+ return false;
21
+ }
22
+ }
23
+ function extractHint(hint) {
24
+ if (!hint) {
25
+ return "";
26
+ }
27
+ return hint.errorHint + " ";
28
+ }
29
+ function checkDir(dir, hint) {
30
+ checkExists(dir, hint);
31
+ if (!isDirectory(dir)) {
32
+ logFatal(`${extractHint(hint)}${getDebugPath(dir)} is not a directory.`);
33
+ }
34
+ }
35
+ exports.checkDir = checkDir;
36
+ function checkNotDir(path, hint) {
37
+ checkExists(path, hint);
38
+ if (isDirectory(path)) {
39
+ logFatal(`${extractHint(hint)}${getDebugPath(path)} is a directory.`);
40
+ }
41
+ }
42
+ exports.checkNotDir = checkNotDir;
43
+ function checkExists(path, hint) {
44
+ if (!(0, fs_1.existsSync)(path)) {
45
+ logFatal(`${extractHint(hint)}${getDebugPath(path)} does not exist.`);
46
+ }
47
+ }
48
+ function getDebugPath(path) {
49
+ return `\'${(0, path_1.resolve)(path)}\'`; // Show an absolute path to users in case of errors.
50
+ }
51
+ exports.getDebugPath = getDebugPath;
52
+ function logFatal(msg) {
53
+ console.error(`error: ${msg}`);
54
+ return process.exit(1);
55
+ }
56
+ exports.logFatal = logFatal;
57
+ function deleteFile(path) {
58
+ checkExists(path);
59
+ (0, fs_1.unlinkSync)(path);
60
+ console.info(`Deleted ${getDebugPath(path)}`);
61
+ }
62
+ exports.deleteFile = deleteFile;
63
+ function readUtf8File(path) {
64
+ checkNotDir(path);
65
+ return (0, fs_1.readFileSync)(path, { encoding: "utf8", flag: "r" });
66
+ }
67
+ exports.readUtf8File = readUtf8File;
68
+ function writeUtf8File(path, content) {
69
+ (0, fs_1.writeFileSync)(path, content, { encoding: "utf8" });
70
+ }
71
+ exports.writeUtf8File = writeUtf8File;
72
+ function runCommandOrDie(command) {
73
+ try {
74
+ return (0, child_process_1.execSync)(command).toString();
75
+ }
76
+ catch (e) {
77
+ //console.error(e.stderr.toString());
78
+ logFatal(`Failed to run \'${command}\' in current directory \'${process.cwd()}\'.`);
79
+ }
80
+ }
81
+ exports.runCommandOrDie = runCommandOrDie;
82
+ function nodeVersionSatisfies(feature, range) {
83
+ if (!semver_1.default.satisfies(process.version, range)) {
84
+ logFatal(`${feature} requires node ${range}`);
85
+ }
86
+ }
87
+ exports.nodeVersionSatisfies = nodeVersionSatisfies;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lingui-po-translate",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-powered translation tool for Lingui PO files with context-aware translations",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,7 +35,7 @@
35
35
  "test": "jest --testTimeout=10000",
36
36
  "test:windows": "npm run test -- --config=test/windows.jest.config.js",
37
37
  "test:generate_refs": "GENERATE_REFS=True npm run test",
38
- "prepublishOnly": "git diff --exit-code"
38
+ "prepublishOnly": "rm -rf ./dist && tsc && git diff --exit-code"
39
39
  },
40
40
  "dependencies": {
41
41
  "@google-cloud/translate": "^8.3.0",