@lingui/cli 5.7.0 → 5.9.0

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.
@@ -32,7 +32,8 @@ function mergeCatalog(prevCatalog, nextCatalog, forSourceLocale, options) {
32
32
  ? nextCatalog[key].message || key
33
33
  : prevCatalog[key].translation;
34
34
  const _a = nextCatalog[key], { obsolete } = _a, rest = __rest(_a, ["obsolete"]);
35
- return [key, Object.assign(Object.assign({}, rest), { translation })];
35
+ const { extra } = prevCatalog[key];
36
+ return [key, Object.assign(Object.assign(Object.assign({}, extra), rest), { translation })];
36
37
  }));
37
38
  // Mark all remaining translations as obsolete
38
39
  // Only if *options.files* is not provided
@@ -1,4 +1,4 @@
1
- import { LinguiConfigNormalized, OrderBy } from "@lingui/conf";
1
+ import { LinguiConfigNormalized, OrderBy, OrderByFn } from "@lingui/conf";
2
2
  import { FormatterWrapper } from "./formats";
3
3
  import { CompiledCatalogNamespace } from "./compile";
4
4
  import { GetTranslationsOptions } from "./catalog/getTranslationsForCatalog";
@@ -69,4 +69,4 @@ export declare class Catalog {
69
69
  export declare function cleanObsolete<T extends ExtractedCatalogType>(messages: T): T;
70
70
  export declare function order<T extends ExtractedCatalogType>(by: OrderBy, catalog: T): T;
71
71
  export declare function writeCompiled(path: string, locale: string, compiledCatalog: string, namespace?: CompiledCatalogNamespace): Promise<string>;
72
- export declare function orderByMessage<T extends ExtractedCatalogType>(messages: T): T;
72
+ export declare const orderByMessage: OrderByFn;
@@ -3,11 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Catalog = void 0;
6
+ exports.orderByMessage = exports.Catalog = void 0;
7
7
  exports.cleanObsolete = cleanObsolete;
8
8
  exports.order = order;
9
9
  exports.writeCompiled = writeCompiled;
10
- exports.orderByMessage = orderByMessage;
11
10
  const fs_1 = __importDefault(require("fs"));
12
11
  const path_1 = __importDefault(require("path"));
13
12
  const glob_1 = require("glob");
@@ -140,8 +139,7 @@ class Catalog {
140
139
  return await this.format.read(filename, undefined);
141
140
  }
142
141
  get sourcePaths() {
143
- const includeGlobs = this.include
144
- .map((includePath) => {
142
+ const includeGlobs = this.include.map((includePath) => {
145
143
  const isDir = (0, utils_1.isDirectory)(includePath);
146
144
  /**
147
145
  * glob library results from absolute patterns such as /foo/* are mounted onto the root setting using path.join.
@@ -150,8 +148,7 @@ class Catalog {
150
148
  return isDir
151
149
  ? (0, normalize_path_1.default)(path_1.default.resolve(process.cwd(), includePath === "/" ? "" : includePath, "**/*.*"))
152
150
  : includePath;
153
- })
154
- .map(utils_1.makePathRegexSafe);
151
+ });
155
152
  return (0, glob_1.globSync)(includeGlobs, { ignore: this.exclude, mark: true });
156
153
  }
157
154
  get localeDir() {
@@ -173,28 +170,33 @@ function cleanObsolete(messages) {
173
170
  return Object.fromEntries(Object.entries(messages).filter(([, message]) => !message.obsolete));
174
171
  }
175
172
  function order(by, catalog) {
176
- return {
177
- messageId: orderByMessageId,
178
- message: orderByMessage,
179
- origin: orderByOrigin,
180
- }[by](catalog);
181
- }
182
- /**
183
- * Object keys are in the same order as they were created
184
- * https://stackoverflow.com/a/31102605/1535540
185
- */
186
- function orderByMessageId(messages) {
187
- return Object.keys(messages)
188
- .sort()
173
+ const orderByFn = typeof by === "function"
174
+ ? by
175
+ : {
176
+ messageId: orderByMessageId,
177
+ message: exports.orderByMessage,
178
+ origin: orderByOrigin,
179
+ }[by];
180
+ return Object.keys(catalog)
181
+ .sort((a, b) => {
182
+ return orderByFn({ messageId: a, entry: catalog[a] }, { messageId: b, entry: catalog[b] });
183
+ })
189
184
  .reduce((acc, key) => {
190
185
  ;
191
- acc[key] = messages[key];
186
+ acc[key] = catalog[key];
192
187
  return acc;
193
188
  }, {});
194
189
  }
195
- function orderByOrigin(messages) {
196
- function getFirstOrigin(messageKey) {
197
- const sortedOrigins = messages[messageKey].origin.sort((a, b) => {
190
+ /**
191
+ * Object keys are in the same order as they were created
192
+ * https://stackoverflow.com/a/31102605/1535540
193
+ */
194
+ const orderByMessageId = (a, b) => {
195
+ return a.messageId.localeCompare(b.messageId);
196
+ };
197
+ const orderByOrigin = (a, b) => {
198
+ function getFirstOrigin(entry) {
199
+ const sortedOrigins = entry.origin.sort((a, b) => {
198
200
  if (a[0] < b[0])
199
201
  return -1;
200
202
  if (a[0] > b[0])
@@ -203,26 +205,18 @@ function orderByOrigin(messages) {
203
205
  });
204
206
  return sortedOrigins[0];
205
207
  }
206
- return Object.keys(messages)
207
- .sort((a, b) => {
208
- const [aFile, aLineNumber] = getFirstOrigin(a);
209
- const [bFile, bLineNumber] = getFirstOrigin(b);
210
- if (aFile < bFile)
211
- return -1;
212
- if (aFile > bFile)
213
- return 1;
214
- if (aLineNumber < bLineNumber)
215
- return -1;
216
- if (aLineNumber > bLineNumber)
217
- return 1;
218
- return 0;
219
- })
220
- .reduce((acc, key) => {
221
- ;
222
- acc[key] = messages[key];
223
- return acc;
224
- }, {});
225
- }
208
+ const [aFile, aLineNumber] = getFirstOrigin(a.entry);
209
+ const [bFile, bLineNumber] = getFirstOrigin(b.entry);
210
+ if (aFile < bFile)
211
+ return -1;
212
+ if (aFile > bFile)
213
+ return 1;
214
+ if (aLineNumber < bLineNumber)
215
+ return -1;
216
+ if (aLineNumber > bLineNumber)
217
+ return 1;
218
+ return 0;
219
+ };
226
220
  async function writeCompiled(path, locale, compiledCatalog, namespace) {
227
221
  let ext;
228
222
  switch (namespace) {
@@ -240,21 +234,14 @@ async function writeCompiled(path, locale, compiledCatalog, namespace) {
240
234
  await (0, utils_1.writeFile)(filename, compiledCatalog);
241
235
  return filename;
242
236
  }
243
- function orderByMessage(messages) {
237
+ const orderByMessage = (a, b) => {
244
238
  // hardcoded en-US locale to have consistent sorting
245
239
  // @see https://github.com/lingui/js-lingui/pull/1808
246
240
  const collator = new Intl.Collator("en-US");
247
- return Object.keys(messages)
248
- .sort((a, b) => {
249
- const aMsg = messages[a].message || "";
250
- const bMsg = messages[b].message || "";
251
- const aCtxt = messages[a].context || "";
252
- const bCtxt = messages[b].context || "";
253
- return collator.compare(aMsg, bMsg) || collator.compare(aCtxt, bCtxt);
254
- })
255
- .reduce((acc, key) => {
256
- ;
257
- acc[key] = messages[key];
258
- return acc;
259
- }, {});
260
- }
241
+ const aMsg = a.entry.message || "";
242
+ const bMsg = b.entry.message || "";
243
+ const aCtxt = a.entry.context || "";
244
+ const bCtxt = b.entry.context || "";
245
+ return collator.compare(aMsg, bMsg) || collator.compare(aCtxt, bCtxt);
246
+ };
247
+ exports.orderByMessage = orderByMessage;
@@ -34,11 +34,13 @@ async function command(config, options) {
34
34
  workerPool = (0, extractWorkerPool_1.createExtractWorkerPool)(options.workersOptions);
35
35
  }
36
36
  spinner.start();
37
+ let extractionResult;
37
38
  try {
38
- await Promise.all(catalogs.map(async (catalog) => {
39
+ extractionResult = await Promise.all(catalogs.map(async (catalog) => {
39
40
  const result = await catalog.make(Object.assign(Object.assign({}, options), { orderBy: config.orderBy, workerPool }));
40
41
  catalogStats[(0, normalize_path_1.default)(path_1.default.relative(config.rootDir, catalog.path))] = result || {};
41
42
  commandSuccess && (commandSuccess = Boolean(result));
43
+ return { catalog, messagesByLocale: result };
42
44
  }));
43
45
  }
44
46
  finally {
@@ -70,7 +72,7 @@ async function command(config, options) {
70
72
  try {
71
73
  const module = require(`./services/${moduleName}`);
72
74
  await module
73
- .default(config, options)
75
+ .default(config, options, extractionResult)
74
76
  .then(console.log)
75
77
  .catch(console.error);
76
78
  }
@@ -0,0 +1,4 @@
1
+ import { MessageType } from "@lingui/conf";
2
+ import { TranslationIoSegment } from "./translationio-api";
3
+ export declare function createSegmentFromLinguiItem(key: string, item: MessageType): TranslationIoSegment;
4
+ export declare function createLinguiItemFromSegment(segment: TranslationIoSegment): readonly [string, MessageType];
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSegmentFromLinguiItem = createSegmentFromLinguiItem;
4
+ exports.createLinguiItemFromSegment = createLinguiItemFromSegment;
5
+ const generateMessageId_1 = require("@lingui/message-utils/generateMessageId");
6
+ const EXPLICIT_ID_FLAG = "js-lingui-explicit-id";
7
+ const EXPLICIT_ID_AND_CONTEXT_FLAG = "js-lingui-explicit-id-and-context";
8
+ function isGeneratedId(id, message) {
9
+ return id === (0, generateMessageId_1.generateMessageId)(message.message, message.context);
10
+ }
11
+ const joinOrigin = (origin) => origin.join(":");
12
+ const splitOrigin = (origin) => {
13
+ const [file, line] = origin.split(":");
14
+ return [file, line ? Number(line) : null];
15
+ };
16
+ function createSegmentFromLinguiItem(key, item) {
17
+ const itemHasExplicitId = !isGeneratedId(key, item);
18
+ const itemHasContext = !!item.context;
19
+ const segment = {
20
+ type: "source", // No way to edit text for source language (inside code), so not using "key" here
21
+ source: "",
22
+ context: "",
23
+ references: [],
24
+ comment: "",
25
+ };
26
+ // For segment.source & segment.context, we must remain compatible with projects created/synced before Lingui V4
27
+ if (itemHasExplicitId) {
28
+ segment.source = item.message || item.translation;
29
+ segment.context = key;
30
+ }
31
+ else {
32
+ segment.source = item.message || item.translation;
33
+ if (itemHasContext) {
34
+ segment.context = item.context;
35
+ }
36
+ }
37
+ if (item.origin) {
38
+ segment.references = item.origin.map(joinOrigin);
39
+ }
40
+ // Since Lingui v4, when using explicit IDs, Lingui automatically adds 'js-lingui-explicit-id' to the extractedComments array
41
+ const comments = [];
42
+ if (itemHasExplicitId) {
43
+ if (itemHasContext) {
44
+ // segment.context is already used for the explicit ID, so we need to pass the context (for translators) in segment.comment
45
+ comments.push(item.context, EXPLICIT_ID_AND_CONTEXT_FLAG);
46
+ }
47
+ else {
48
+ comments.push(EXPLICIT_ID_FLAG);
49
+ }
50
+ }
51
+ segment.comment = [...comments, ...(item.comments || [])].join(" | ");
52
+ return segment;
53
+ }
54
+ function createLinguiItemFromSegment(segment) {
55
+ var _a, _b, _c;
56
+ const segmentHasExplicitId = (_a = segment.comment) === null || _a === void 0 ? void 0 : _a.includes(EXPLICIT_ID_FLAG);
57
+ const segmentHasExplicitIdAndContext = (_b = segment.comment) === null || _b === void 0 ? void 0 : _b.includes(EXPLICIT_ID_AND_CONTEXT_FLAG);
58
+ const item = {
59
+ translation: segment.target,
60
+ origin: ((_c = segment.references) === null || _c === void 0 ? void 0 : _c.length)
61
+ ? segment.references.map(splitOrigin)
62
+ : [],
63
+ message: segment.source,
64
+ comments: [],
65
+ };
66
+ let id = null;
67
+ if (segmentHasExplicitId || segmentHasExplicitIdAndContext) {
68
+ id = segment.context;
69
+ }
70
+ else {
71
+ id = (0, generateMessageId_1.generateMessageId)(segment.source, segment.context);
72
+ item.context = segment.context;
73
+ }
74
+ if (segment.comment) {
75
+ item.comments = segment.comment.split(" | ").filter(
76
+ // drop flags from comments
77
+ (comment) => comment !== EXPLICIT_ID_AND_CONTEXT_FLAG && comment !== EXPLICIT_ID_FLAG);
78
+ // We recompose a target PO Item that is consistent with the source PO Item
79
+ if (segmentHasExplicitIdAndContext) {
80
+ item.context = item.comments.shift();
81
+ }
82
+ }
83
+ return [id, item];
84
+ }
@@ -0,0 +1,48 @@
1
+ export type TranslationIoSyncRequest = {
2
+ client: "lingui";
3
+ version: string;
4
+ source_language: string;
5
+ target_languages: string[];
6
+ segments: TranslationIoSegment[];
7
+ purge?: boolean;
8
+ };
9
+ export type TranslationIoInitRequest = {
10
+ client: "lingui";
11
+ version: string;
12
+ source_language: string;
13
+ target_languages: string[];
14
+ segments: {
15
+ [locale: string]: TranslationIoSegment[];
16
+ };
17
+ };
18
+ export type TranslationIoSegment = {
19
+ type: string;
20
+ source: string;
21
+ target?: string;
22
+ context?: string;
23
+ references?: string[];
24
+ comment?: string;
25
+ };
26
+ export type TranslationIoProject = {
27
+ name: string;
28
+ url: string;
29
+ };
30
+ export type TranslationIoResponse = {
31
+ errors?: string[];
32
+ project?: TranslationIoProject;
33
+ segments?: {
34
+ [locale: string]: TranslationIoSegment[];
35
+ };
36
+ };
37
+ export type FetchResult<T> = {
38
+ data: T;
39
+ error: undefined;
40
+ } | {
41
+ error: {
42
+ response: Response;
43
+ message: string;
44
+ };
45
+ data: undefined;
46
+ };
47
+ export declare function tioSync(request: TranslationIoSyncRequest, apiKey: string): Promise<FetchResult<TranslationIoResponse>>;
48
+ export declare function tioInit(request: TranslationIoInitRequest, apiKey: string): Promise<FetchResult<TranslationIoResponse>>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tioSync = tioSync;
4
+ exports.tioInit = tioInit;
5
+ // todo: need to enable strictNullChecks to support this kind of type narrowing
6
+ // export type TranslationIoResponse =
7
+ // | TranslationIoErrorResponse
8
+ // | TranslationProjectResponse
9
+ //
10
+ // type TranslationIoErrorResponse = {
11
+ // errors: string[]
12
+ // }
13
+ // type TranslationProjectResponse = {
14
+ // errors: null
15
+ // project: TranslationIoProject
16
+ // segments: { [locale: string]: TranslationIoSegment[] }
17
+ // }
18
+ async function post(url, request) {
19
+ const response = await fetch(url, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ },
24
+ body: JSON.stringify(request),
25
+ });
26
+ if (!response.ok && response.status !== 400) {
27
+ return {
28
+ error: {
29
+ response,
30
+ message: `Request failed with ${response.status} ${response.statusText} status. Body: ${await response.text()}`,
31
+ },
32
+ data: undefined,
33
+ };
34
+ }
35
+ return { data: await response.json(), error: undefined };
36
+ }
37
+ async function tioSync(request, apiKey) {
38
+ return post(`https://translation.io/api/v1/segments/sync.json?api_key=${apiKey}`, request);
39
+ }
40
+ async function tioInit(request, apiKey) {
41
+ return post(`https://translation.io/api/v1/segments/init.json?api_key=${apiKey}`, request);
42
+ }
@@ -1,3 +1,32 @@
1
1
  import { LinguiConfigNormalized } from "@lingui/conf";
2
2
  import { CliExtractOptions } from "../lingui-extract";
3
- export default function syncProcess(config: LinguiConfigNormalized, options: CliExtractOptions): Promise<string>;
3
+ import { TranslationIoProject, TranslationIoSegment } from "./translationIO/translationio-api";
4
+ import { Catalog } from "../api/catalog";
5
+ import { AllCatalogsType } from "../api/types";
6
+ type ExtractionResult = {
7
+ catalog: Catalog;
8
+ messagesByLocale: AllCatalogsType;
9
+ }[];
10
+ export default function syncProcess(config: LinguiConfigNormalized, options: CliExtractOptions, extractionResult: ExtractionResult): Promise<string>;
11
+ export declare function init(config: LinguiConfigNormalized, extractionResult: ExtractionResult): Promise<{
12
+ readonly success: false;
13
+ readonly errors: string[];
14
+ readonly project?: undefined;
15
+ } | {
16
+ readonly success: true;
17
+ readonly project: TranslationIoProject;
18
+ readonly errors?: string[];
19
+ }>;
20
+ export declare function sync(config: LinguiConfigNormalized, options: CliExtractOptions, extractionResult: ExtractionResult): Promise<{
21
+ readonly success: false;
22
+ readonly errors: string[];
23
+ readonly project?: undefined;
24
+ } | {
25
+ readonly success: true;
26
+ readonly project: TranslationIoProject;
27
+ readonly errors?: string[];
28
+ }>;
29
+ export declare function writeSegmentsToCatalogs(config: LinguiConfigNormalized, sourceLocale: string, extractionResult: ExtractionResult, segmentsPerLocale: {
30
+ [locale: string]: TranslationIoSegment[];
31
+ }): Promise<void>;
32
+ export {};
@@ -4,130 +4,103 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = syncProcess;
7
+ exports.init = init;
8
+ exports.sync = sync;
9
+ exports.writeSegmentsToCatalogs = writeSegmentsToCatalogs;
7
10
  const fs_1 = __importDefault(require("fs"));
8
11
  const path_1 = require("path");
9
- const pofile_1 = __importDefault(require("pofile"));
10
- const https_1 = __importDefault(require("https"));
11
- const glob_1 = require("glob");
12
- const date_fns_1 = require("date-fns");
13
- const EXPLICIT_ID_FLAG = "js-lingui-explicit-id";
14
- const EXPLICIT_ID_AND_CONTEXT_FLAG = "js-lingui-explicit-id-and-context";
15
- const getCreateHeaders = (language) => ({
16
- "POT-Creation-Date": (0, date_fns_1.format)(new Date(), "yyyy-MM-dd HH:mmxxxx"),
17
- "MIME-Version": "1.0",
18
- "Content-Type": "text/plain; charset=utf-8",
19
- "Content-Transfer-Encoding": "8bit",
20
- "X-Generator": "@lingui/cli",
21
- Language: language,
22
- });
12
+ const translationio_api_1 = require("./translationIO/translationio-api");
13
+ const catalog_1 = require("../api/catalog");
14
+ const segment_converters_1 = require("./translationIO/segment-converters");
23
15
  const getTargetLocales = (config) => {
24
16
  const sourceLocale = config.sourceLocale || "en";
25
17
  const pseudoLocale = config.pseudoLocale || "pseudo";
26
18
  return config.locales.filter((value) => value != sourceLocale && value != pseudoLocale);
27
19
  };
28
- const validCatalogFormat = (config) => {
29
- if (typeof config.format === "string") {
30
- return config.format === "po";
31
- }
32
- return config.format.catalogExtension === ".po";
33
- };
34
20
  // Main sync method, call "Init" or "Sync" depending on the project context
35
- async function syncProcess(config, options) {
36
- if (!validCatalogFormat(config)) {
37
- console.error(`\n----------\nTranslation.io service is only compatible with the "po" format. Please update your Lingui configuration accordingly.\n----------`);
38
- process.exit(1);
21
+ async function syncProcess(config, options, extractionResult) {
22
+ const reportSuccess = (project) => {
23
+ return `\n----------\nProject successfully synchronized. Please use this URL to translate: ${project.url}\n----------`;
24
+ };
25
+ const reportError = (errors) => {
26
+ throw `\n----------\nSynchronization with Translation.io failed: ${errors.join(", ")}\n----------`;
27
+ };
28
+ const { success, project, errors } = await init(config, extractionResult);
29
+ if (success) {
30
+ return reportSuccess(project);
39
31
  }
40
- return await new Promise((resolve, reject) => {
41
- const successCallback = (project) => {
42
- resolve(`\n----------\nProject successfully synchronized. Please use this URL to translate: ${project.url}\n----------`);
43
- };
44
- const failCallback = (errors) => {
45
- reject(`\n----------\nSynchronization with Translation.io failed: ${errors.join(", ")}\n----------`);
46
- };
47
- init(config, options, successCallback, (errors) => {
48
- if (errors.length &&
49
- errors[0] === "This project has already been initialized.") {
50
- sync(config, options, successCallback, failCallback);
51
- }
52
- else {
53
- failCallback(errors);
54
- }
55
- });
56
- });
32
+ if (errors[0] === "This project has already been initialized.") {
33
+ const { success, project, errors } = await sync(config, options, extractionResult);
34
+ if (success) {
35
+ return reportSuccess(project);
36
+ }
37
+ return reportError(errors);
38
+ }
39
+ return reportError(errors);
57
40
  }
58
41
  // Initialize project with source and existing translations (only first time!)
59
42
  // Cf. https://translation.io/docs/create-library#initialization
60
- function init(config, options, successCallback, failCallback) {
43
+ async function init(config, extractionResult) {
61
44
  const sourceLocale = config.sourceLocale || "en";
62
45
  const targetLocales = getTargetLocales(config);
63
- const paths = poPathsPerLocale(config);
64
46
  const segments = {};
65
47
  targetLocales.forEach((targetLocale) => {
66
48
  segments[targetLocale] = [];
67
49
  });
68
50
  // Create segments from source locale PO items
69
- paths[sourceLocale].forEach((path) => {
70
- const raw = fs_1.default.readFileSync(path).toString();
71
- const po = pofile_1.default.parse(raw);
72
- po.items
73
- .filter((item) => !item["obsolete"])
74
- .forEach((item) => {
51
+ for (const { messagesByLocale } of extractionResult) {
52
+ const messages = messagesByLocale[sourceLocale];
53
+ Object.entries(messages).forEach(([key, entry]) => {
54
+ if (entry.obsolete)
55
+ return;
75
56
  targetLocales.forEach((targetLocale) => {
76
- const newSegment = createSegmentFromPoItem(item);
77
- segments[targetLocale].push(newSegment);
57
+ segments[targetLocale].push((0, segment_converters_1.createSegmentFromLinguiItem)(key, entry));
78
58
  });
79
59
  });
80
- });
60
+ }
81
61
  // Add translations to segments from target locale PO items
82
- targetLocales.forEach((targetLocale) => {
83
- paths[targetLocale].forEach((path) => {
84
- const raw = fs_1.default.readFileSync(path).toString();
85
- const po = pofile_1.default.parse(raw);
86
- po.items
87
- .filter((item) => !item["obsolete"])
88
- .forEach((item, index) => {
89
- segments[targetLocale][index].target = item.msgstr[0];
62
+ for (const { messagesByLocale } of extractionResult) {
63
+ for (const targetLocale of targetLocales) {
64
+ const messages = messagesByLocale[targetLocale];
65
+ Object.entries(messages)
66
+ .filter(([, entry]) => !entry.obsolete)
67
+ .forEach(([, entry], index) => {
68
+ segments[targetLocale][index].target = entry.translation;
90
69
  });
91
- });
92
- });
93
- const request = {
70
+ }
71
+ }
72
+ const { data, error } = await (0, translationio_api_1.tioInit)({
94
73
  client: "lingui",
95
74
  version: require("@lingui/core/package.json").version,
96
75
  source_language: sourceLocale,
97
76
  target_languages: targetLocales,
98
77
  segments: segments,
99
- };
100
- postTio("init", request, config.service.apiKey, (response) => {
101
- if (response.errors) {
102
- failCallback(response.errors);
103
- }
104
- else {
105
- saveSegmentsToTargetPos(config, paths, response.segments);
106
- successCallback(response.project);
107
- }
108
- }, (error) => {
109
- console.error(`\n----------\nSynchronization with Translation.io failed: ${error}\n----------`);
110
- });
78
+ }, config.service.apiKey);
79
+ if (error) {
80
+ return { success: false, errors: [error.message] };
81
+ }
82
+ if (data.errors) {
83
+ return { success: false, errors: data.errors };
84
+ }
85
+ await writeSegmentsToCatalogs(config, sourceLocale, extractionResult, data.segments);
86
+ return { success: true, project: data.project };
111
87
  }
112
88
  // Send all source text from PO to Translation.io and create new PO based on received translations
113
89
  // Cf. https://translation.io/docs/create-library#synchronization
114
- function sync(config, options, successCallback, failCallback) {
90
+ async function sync(config, options, extractionResult) {
115
91
  const sourceLocale = config.sourceLocale || "en";
116
92
  const targetLocales = getTargetLocales(config);
117
- const paths = poPathsPerLocale(config);
118
93
  const segments = [];
119
94
  // Create segments with correct source
120
- paths[sourceLocale].forEach((path) => {
121
- const raw = fs_1.default.readFileSync(path).toString();
122
- const po = pofile_1.default.parse(raw);
123
- po.items
124
- .filter((item) => !item["obsolete"])
125
- .forEach((item) => {
126
- const newSegment = createSegmentFromPoItem(item);
127
- segments.push(newSegment);
95
+ for (const { messagesByLocale } of extractionResult) {
96
+ const messages = messagesByLocale[sourceLocale];
97
+ Object.entries(messages).forEach(([key, entry]) => {
98
+ if (entry.obsolete)
99
+ return;
100
+ segments.push((0, segment_converters_1.createSegmentFromLinguiItem)(key, entry));
128
101
  });
129
- });
130
- const request = {
102
+ }
103
+ const { data, error } = await (0, translationio_api_1.tioSync)({
131
104
  client: "lingui",
132
105
  version: require("@lingui/core/package.json").version,
133
106
  source_language: sourceLocale,
@@ -135,172 +108,47 @@ function sync(config, options, successCallback, failCallback) {
135
108
  segments: segments,
136
109
  // Sync and then remove unused segments (not present in the local application) from Translation.io
137
110
  purge: Boolean(options.clean),
138
- };
139
- postTio("sync", request, config.service.apiKey, (response) => {
140
- if (response.errors) {
141
- failCallback(response.errors);
142
- }
143
- else {
144
- saveSegmentsToTargetPos(config, paths, response.segments);
145
- successCallback(response.project);
146
- }
147
- }, (error) => {
148
- console.error(`\n----------\nSynchronization with Translation.io failed: ${error}\n----------`);
149
- });
150
- }
151
- function createSegmentFromPoItem(item) {
152
- const itemHasExplicitId = item.extractedComments.includes(EXPLICIT_ID_FLAG);
153
- const itemHasContext = item.msgctxt != null;
154
- const segment = {
155
- type: "source", // No way to edit text for source language (inside code), so not using "key" here
156
- source: "",
157
- context: "",
158
- references: [],
159
- comment: "",
160
- };
161
- // For segment.source & segment.context, we must remain compatible with projects created/synced before Lingui V4
162
- if (itemHasExplicitId) {
163
- segment.source = item.msgstr[0];
164
- segment.context = item.msgid;
165
- }
166
- else {
167
- segment.source = item.msgid;
168
- if (itemHasContext) {
169
- segment.context = item.msgctxt;
170
- }
111
+ }, config.service.apiKey);
112
+ if (error) {
113
+ return { success: false, errors: [error.message] };
171
114
  }
172
- if (item.references.length) {
173
- segment.references = item.references;
115
+ if (data.errors) {
116
+ return { success: false, errors: data.errors };
174
117
  }
175
- // Since Lingui v4, when using explicit IDs, Lingui automatically adds 'js-lingui-explicit-id' to the extractedComments array
176
- if (item.extractedComments.length) {
177
- segment.comment = item.extractedComments.join(" | ");
178
- if (itemHasExplicitId && itemHasContext) {
179
- // segment.context is already used for the explicit ID, so we need to pass the context (for translators) in segment.comment
180
- segment.comment = `${item.msgctxt} | ${segment.comment}`;
181
- // Replace the flag to let us know how to recompose a target PO Item that is consistent with the source PO Item
182
- segment.comment = segment.comment.replace(EXPLICIT_ID_FLAG, EXPLICIT_ID_AND_CONTEXT_FLAG);
183
- }
184
- }
185
- return segment;
118
+ await writeSegmentsToCatalogs(config, sourceLocale, extractionResult, data.segments);
119
+ return { success: true, project: data.project };
186
120
  }
187
- function createPoItemFromSegment(segment) {
188
- var _a, _b;
189
- const segmentHasExplicitId = (_a = segment.comment) === null || _a === void 0 ? void 0 : _a.includes(EXPLICIT_ID_FLAG);
190
- const segmentHasExplicitIdAndContext = (_b = segment.comment) === null || _b === void 0 ? void 0 : _b.includes(EXPLICIT_ID_AND_CONTEXT_FLAG);
191
- const item = new pofile_1.default.Item();
192
- if (segmentHasExplicitId || segmentHasExplicitIdAndContext) {
193
- item.msgid = segment.context;
194
- }
195
- else {
196
- item.msgid = segment.source;
197
- item.msgctxt = segment.context;
198
- }
199
- item.msgstr = [segment.target];
200
- item.references =
201
- segment.references && segment.references.length ? segment.references : [];
202
- if (segment.comment) {
203
- segment.comment = segment.comment.replace(EXPLICIT_ID_AND_CONTEXT_FLAG, EXPLICIT_ID_FLAG);
204
- item.extractedComments = segment.comment ? segment.comment.split(" | ") : [];
205
- // We recompose a target PO Item that is consistent with the source PO Item
206
- if (segmentHasExplicitIdAndContext) {
207
- item.msgctxt = item.extractedComments.shift();
208
- }
209
- }
210
- return item;
211
- }
212
- function saveSegmentsToTargetPos(config, paths, segmentsPerLocale) {
213
- Object.keys(segmentsPerLocale).forEach((targetLocale) => {
214
- // Remove existing target POs and JS for this target locale
215
- paths[targetLocale].forEach((path) => {
216
- const jsPath = path.replace(/\.po?$/, "") + ".js";
217
- const dirPath = (0, path_1.dirname)(path);
218
- // Remove PO, JS and empty dir
219
- if (fs_1.default.existsSync(path)) {
220
- fs_1.default.unlinkSync(path);
221
- }
222
- if (fs_1.default.existsSync(jsPath)) {
223
- fs_1.default.unlinkSync(jsPath);
224
- }
225
- if (fs_1.default.existsSync(dirPath) && fs_1.default.readdirSync(dirPath).length === 0) {
226
- fs_1.default.rmdirSync(dirPath);
227
- }
228
- });
229
- // Find target path (ignoring {name})
230
- const localePath = "".concat(config.catalogs[0].path
231
- .replace(/{locale}/g, targetLocale)
232
- .replace(/{name}/g, ""), ".po");
233
- const segments = segmentsPerLocale[targetLocale];
234
- const po = new pofile_1.default();
235
- po.headers = getCreateHeaders(targetLocale);
236
- const items = [];
237
- segments.forEach((segment) => {
238
- const item = createPoItemFromSegment(segment);
239
- items.push(item);
240
- });
241
- // Sort items by messageId
242
- po.items = items.sort((a, b) => {
243
- if (a.msgid < b.msgid) {
244
- return -1;
245
- }
246
- if (a.msgid > b.msgid) {
247
- return 1;
248
- }
249
- return 0;
250
- });
251
- // Check that localePath directory exists and save PO file
252
- fs_1.default.promises.mkdir((0, path_1.dirname)(localePath), { recursive: true }).then(() => {
253
- po.save(localePath, (err) => {
254
- if (err) {
255
- console.error("Error while saving target PO files:");
256
- console.error(err);
257
- process.exit(1);
121
+ async function writeSegmentsToCatalogs(config, sourceLocale, extractionResult, segmentsPerLocale) {
122
+ // Create segments from source locale PO items
123
+ for (const { catalog, messagesByLocale } of extractionResult) {
124
+ const sourceMessages = messagesByLocale[sourceLocale];
125
+ for (const targetLocale of Object.keys(segmentsPerLocale)) {
126
+ // Remove existing target POs and JS for this target locale
127
+ {
128
+ const path = catalog.getFilename(targetLocale);
129
+ const jsPath = path.replace(new RegExp(`${catalog.format.getCatalogExtension()}$`), "") + ".js";
130
+ const dirPath = (0, path_1.dirname)(path);
131
+ // todo: check tests and all these logic, maybe it could be simplified to just drop the folder
132
+ // Remove PO, JS and empty dir
133
+ if (fs_1.default.existsSync(path)) {
134
+ await fs_1.default.promises.unlink(path);
135
+ }
136
+ if (fs_1.default.existsSync(jsPath)) {
137
+ await fs_1.default.promises.unlink(jsPath);
138
+ }
139
+ if (fs_1.default.existsSync(dirPath) && fs_1.default.readdirSync(dirPath).length === 0) {
140
+ await fs_1.default.promises.rmdir(dirPath);
258
141
  }
259
- });
260
- });
261
- });
262
- }
263
- function poPathsPerLocale(config) {
264
- const paths = {};
265
- config.locales.forEach((locale) => {
266
- paths[locale] = [];
267
- config.catalogs.forEach((catalog) => {
268
- const path = "".concat(catalog.path.replace(/{locale}/g, locale).replace(/{name}/g, "*"), ".po");
269
- // If {name} is present (replaced by *), list all the existing POs
270
- if (path.includes("*")) {
271
- paths[locale] = paths[locale].concat((0, glob_1.globSync)(path));
272
- }
273
- else {
274
- paths[locale].push(path);
275
142
  }
276
- });
277
- });
278
- return paths;
279
- }
280
- function postTio(action, request, apiKey, successCallback, failCallback) {
281
- const jsonRequest = JSON.stringify(request);
282
- const options = {
283
- hostname: "translation.io",
284
- path: `/api/v1/segments/${action}.json?api_key=${apiKey}`,
285
- method: "POST",
286
- headers: {
287
- "Content-Type": "application/json",
288
- },
289
- };
290
- const req = https_1.default.request(options, (res) => {
291
- res.setEncoding("utf8");
292
- let body = "";
293
- res.on("data", (chunk) => {
294
- body = body.concat(chunk);
295
- });
296
- res.on("end", () => {
297
- const response = JSON.parse(body);
298
- successCallback(response);
299
- });
300
- });
301
- req.on("error", (e) => {
302
- failCallback(e);
303
- });
304
- req.write(jsonRequest);
305
- req.end();
143
+ const translations = Object.fromEntries(segmentsPerLocale[targetLocale].map((segment) => (0, segment_converters_1.createLinguiItemFromSegment)(segment)));
144
+ const messages = Object.fromEntries(Object.entries(sourceMessages).map(([key, entry]) => {
145
+ var _a;
146
+ return [
147
+ key,
148
+ Object.assign(Object.assign({}, entry), { translation: (_a = translations[key]) === null || _a === void 0 ? void 0 : _a.translation }),
149
+ ];
150
+ }));
151
+ await catalog.write(targetLocale, (0, catalog_1.order)(config.orderBy, messages));
152
+ }
153
+ }
306
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingui/cli",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "description": "CLI for working wit message catalogs",
5
5
  "keywords": [
6
6
  "cli",
@@ -62,12 +62,12 @@
62
62
  "@babel/parser": "^7.22.0",
63
63
  "@babel/runtime": "^7.21.0",
64
64
  "@babel/types": "^7.21.2",
65
- "@lingui/babel-plugin-extract-messages": "5.7.0",
66
- "@lingui/babel-plugin-lingui-macro": "5.7.0",
67
- "@lingui/conf": "5.7.0",
68
- "@lingui/core": "5.7.0",
69
- "@lingui/format-po": "5.7.0",
70
- "@lingui/message-utils": "5.7.0",
65
+ "@lingui/babel-plugin-extract-messages": "5.9.0",
66
+ "@lingui/babel-plugin-lingui-macro": "5.9.0",
67
+ "@lingui/conf": "5.9.0",
68
+ "@lingui/core": "5.9.0",
69
+ "@lingui/format-po": "5.9.0",
70
+ "@lingui/message-utils": "5.9.0",
71
71
  "chokidar": "3.5.1",
72
72
  "cli-table": "^0.3.11",
73
73
  "commander": "^10.0.0",
@@ -93,7 +93,8 @@
93
93
  "@types/normalize-path": "^3.0.0",
94
94
  "mock-fs": "^5.2.0",
95
95
  "mockdate": "^3.0.5",
96
+ "msw": "^2.12.7",
96
97
  "ts-node": "^10.9.2"
97
98
  },
98
- "gitHead": "e8c42d548af8fae7365094e58249148fa6a6019f"
99
+ "gitHead": "491d4c17651c3f76116fe7f63f6bb8a554bef8da"
99
100
  }