@lingui/cli 6.5.0 → 6.7.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.
package/README.md CHANGED
@@ -4,22 +4,43 @@
4
4
 
5
5
  # @lingui/cli
6
6
 
7
- > lingui command line library for manipulating message catalogues
7
+ > Lingui CLI: extracts i18n messages from source code into catalogs and compiles the translated catalogs for the runtime
8
8
 
9
- `@lingui/cli` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.
9
+ `@lingui/cli` is part of [Lingui][documentation]. Lingui is a lightweight, open-source internationalization (i18n) library for JavaScript and TypeScript. It brings compile-time macros and a CLI for message extraction to React, React Native, Vue, SolidJS, Astro, Svelte, and Node.js.
10
10
 
11
- ## Installation & Usage
11
+ The `lingui` command reads `lingui.config.js` (or `.ts`) from the project root: the source locale, the target locales and where the catalogs live. The [installation guide][installation] shows the minimal file and the [configuration reference][conf] lists every option.
12
12
 
13
- See the [reference][reference] documentation.
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install --save-dev @lingui/cli
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ - `lingui extract` finds the messages in the source, merges them into one catalog per locale, keeps existing translations and marks removed messages as obsolete.
22
+ - `lingui compile` compiles the translated catalogs into JavaScript modules for the runtime and reports messages with invalid ICU syntax.
23
+ - `lingui extract-template` writes a single `.pot` template with the source messages, for translation platforms that create the locale files themselves.
24
+ - `lingui extract-experimental` follows the imports from each entry point instead of a glob, so a multi-page app gets one catalog per page.
25
+
26
+ Extraction parses JavaScript and TypeScript out of the box; Vue single-file components need [`@lingui/extractor-vue`][extractor-vue]. Catalogs are PO files by default, with [other formats][catalog-formats] available as separate packages. To let the bundler compile catalogs instead of running `lingui compile`, use [`@lingui/vite-plugin`][vite-plugin], [`@lingui/loader`][loader] or [`@lingui/metro-transformer`][metro-transformer].
27
+
28
+ See the [CLI reference][reference] for all commands and options.
14
29
 
15
30
  ## License
16
31
 
17
- This package is licensed under [MIT][license] license.
32
+ This package is licensed under the [MIT][license] license.
18
33
 
19
34
  [license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
20
- [linguijs]: https://github.com/lingui/js-lingui
21
35
  [documentation]: https://lingui.dev
36
+ [installation]: https://lingui.dev/installation
37
+ [conf]: https://lingui.dev/ref/conf
22
38
  [reference]: https://lingui.dev/ref/cli
39
+ [catalog-formats]: https://lingui.dev/ref/catalog-formats
40
+ [extractor-vue]: https://lingui.dev/ref/extractor-vue
41
+ [vite-plugin]: https://lingui.dev/ref/vite-plugin
42
+ [loader]: https://lingui.dev/ref/loader
43
+ [metro-transformer]: https://lingui.dev/ref/metro-transformer
23
44
  [package]: https://www.npmjs.com/package/@lingui/cli
24
45
  [badge-downloads]: https://img.shields.io/npm/dw/@lingui/cli.svg
25
46
  [badge-version]: https://img.shields.io/npm/v/@lingui/cli.svg
@@ -43,13 +43,18 @@ export async function getCatalogs(config) {
43
43
  });
44
44
  candidates.forEach((catalogDir) => {
45
45
  const name = path.basename(catalogDir);
46
+ const globName = escapeCatalogNameForGlob(name);
46
47
  catalogs.push(new Catalog({
47
48
  name,
48
49
  path: normalizeRelativePath(replacePlaceholders(catalog.path, { name })),
49
50
  include: include.map((path) => replacePlaceholders(path, { name })),
50
51
  exclude: exclude.map((path) => replacePlaceholders(path, { name })),
51
52
  format,
52
- }, config));
53
+ }, config, {
54
+ path: replacePlaceholders(catalog.path, { name: globName }),
55
+ include: include.map((path) => replacePlaceholders(path, { name: globName })),
56
+ exclude: exclude.map((path) => replacePlaceholders(path, { name: globName })),
57
+ }));
53
58
  });
54
59
  });
55
60
  if (config.experimental?.extractor &&
@@ -58,6 +63,9 @@ export async function getCatalogs(config) {
58
63
  }
59
64
  return catalogs;
60
65
  }
66
+ function escapeCatalogNameForGlob(value) {
67
+ return value.replace(/[[\]()]/g, "[$&]");
68
+ }
61
69
  /**
62
70
  * Ensure that value is always array. If not, turn it into an array of one element.
63
71
  */
@@ -29,7 +29,13 @@ export type CatalogProps = {
29
29
  templatePath?: string;
30
30
  format: FormatterWrapper;
31
31
  };
32
+ type CatalogGlobPatterns = {
33
+ path: string;
34
+ include: Array<string>;
35
+ exclude: Array<string>;
36
+ };
32
37
  export declare class Catalog {
38
+ #private;
33
39
  config: LinguiConfigNormalized;
34
40
  name?: string;
35
41
  path: string;
@@ -37,7 +43,7 @@ export declare class Catalog {
37
43
  exclude: Array<string>;
38
44
  format: FormatterWrapper;
39
45
  templateFile: string;
40
- constructor({ name, path, include, templatePath, format, exclude }: CatalogProps, config: LinguiConfigNormalized);
46
+ constructor({ name, path, include, templatePath, format, exclude }: CatalogProps, config: LinguiConfigNormalized, globPatterns?: CatalogGlobPatterns);
41
47
  getFilename(locale: string): string;
42
48
  make(options: MakeOptions): Promise<AllCatalogsType | false>;
43
49
  makeTemplate(options: MakeTemplateOptions): Promise<CatalogType | false>;
@@ -71,3 +77,4 @@ export declare function cleanObsolete<T extends CatalogType>(messages: T): T;
71
77
  export declare function order<T extends CatalogType>(by: OrderBy, catalog: T): T;
72
78
  export declare function writeCompiled(path: string, locale: string, compiledCatalog: string, namespace?: CompiledCatalogNamespace): Promise<string>;
73
79
  export declare const orderByMessage: OrderByFn;
80
+ export {};
@@ -17,12 +17,19 @@ export class Catalog {
17
17
  exclude;
18
18
  format;
19
19
  templateFile;
20
- constructor({ name, path, include, templatePath, format, exclude = [] }, config) {
20
+ #includePatterns;
21
+ #excludePatterns;
22
+ constructor({ name, path, include, templatePath, format, exclude = [] }, config, globPatterns = { path, include, exclude }) {
21
23
  this.config = config;
22
24
  this.name = name;
23
25
  this.path = normalizeRelativePath(path);
24
26
  this.include = include.map(normalizeRelativePath);
25
27
  this.exclude = [this.localeDir, ...exclude.map(normalizeRelativePath)];
28
+ this.#includePatterns = createCatalogPatterns(this.include, globPatterns.include.map(normalizeRelativePath));
29
+ this.#excludePatterns = createCatalogPatterns(this.exclude, [
30
+ getLocaleDir(normalizeRelativePath(globPatterns.path)),
31
+ ...globPatterns.exclude.map(normalizeRelativePath),
32
+ ]);
26
33
  this.format = format;
27
34
  this.templateFile =
28
35
  templatePath ||
@@ -194,30 +201,54 @@ export class Catalog {
194
201
  async readTemplate() {
195
202
  return await this.format.read(this.templateFile, undefined);
196
203
  }
204
+ /** @internal */
205
+ get sourcePatterns() {
206
+ return {
207
+ include: resolveCatalogPatterns(this.include, this.#includePatterns),
208
+ exclude: resolveCatalogPatterns(this.exclude, this.#excludePatterns),
209
+ };
210
+ }
197
211
  get sourcePaths() {
198
- const includeGlobs = this.include.map((includePath) => {
212
+ const sourcePatterns = this.sourcePatterns;
213
+ const includeGlobs = this.include.map((includePath, index) => {
214
+ const includeGlob = sourcePatterns.include[index];
199
215
  const isDir = isDirectory(includePath);
200
216
  /**
201
217
  * glob library results from absolute patterns such as /foo/* are mounted onto the root setting using path.join.
202
218
  * On windows, this will by default result in /foo/* matching C:\foo\bar.txt.
203
219
  */
204
220
  return isDir
205
- ? normalize(path.resolve(process.cwd(), includePath === "/" ? "" : includePath, "**/*.*"))
206
- : includePath;
221
+ ? normalize(path.resolve(process.cwd(), includePath === "/" ? "" : includeGlob, "**/*.*"))
222
+ : includeGlob;
207
223
  });
208
- return globSync(includeGlobs, { exclude: this.exclude });
224
+ return globSync(includeGlobs, { exclude: sourcePatterns.exclude });
209
225
  }
210
226
  get localeDir() {
211
- const localePatternIndex = this.path.indexOf(LOCALE);
212
- if (localePatternIndex === -1) {
213
- throw Error(`Invalid catalog path: ${LOCALE} variable is missing`);
214
- }
215
- return this.path.substring(0, localePatternIndex);
227
+ return getLocaleDir(this.path);
216
228
  }
217
229
  get locales() {
218
230
  return this.config.locales;
219
231
  }
220
232
  }
233
+ function createCatalogPatterns(values, globs) {
234
+ return values.map((value, index) => ({
235
+ value,
236
+ glob: globs[index] ?? value,
237
+ }));
238
+ }
239
+ function resolveCatalogPatterns(values, patterns) {
240
+ return values.map((value, index) => {
241
+ const pattern = patterns[index];
242
+ return pattern?.value === value ? pattern.glob : value;
243
+ });
244
+ }
245
+ function getLocaleDir(catalogPath) {
246
+ const localePatternIndex = catalogPath.indexOf(LOCALE);
247
+ if (localePatternIndex === -1) {
248
+ throw Error(`Invalid catalog path: ${LOCALE} variable is missing`);
249
+ }
250
+ return catalogPath.substring(0, localePatternIndex);
251
+ }
221
252
  function getTemplatePath(ext, path) {
222
253
  return path.replace(LOCALE_SUFFIX_RE, "messages" + ext);
223
254
  }
@@ -242,12 +273,15 @@ export function order(by, catalog) {
242
273
  return acc;
243
274
  }, {});
244
275
  }
276
+ // hardcoded en-US locale to have consistent sorting
277
+ // @see https://github.com/lingui/js-lingui/pull/1808
278
+ const collator = new Intl.Collator("en-US");
245
279
  /**
246
280
  * Object keys are in the same order as they were created
247
281
  * https://stackoverflow.com/a/31102605/1535540
248
282
  */
249
283
  const orderByMessageId = (a, b) => {
250
- return a.messageId.localeCompare(b.messageId);
284
+ return collator.compare(a.messageId, b.messageId);
251
285
  };
252
286
  const orderByOrigin = (a, b) => {
253
287
  if (!a.entry.origin || !b.entry.origin) {
@@ -292,9 +326,6 @@ export async function writeCompiled(path, locale, compiledCatalog, namespace) {
292
326
  await writeFile(filename, compiledCatalog);
293
327
  return filename;
294
328
  }
295
- // hardcoded en-US locale to have consistent sorting
296
- // @see https://github.com/lingui/js-lingui/pull/1808
297
- const collator = new Intl.Collator("en-US");
298
329
  export const orderByMessage = (a, b) => {
299
330
  const aMsg = a.entry.message || "";
300
331
  const bMsg = b.entry.message || "";
@@ -14,8 +14,9 @@ export async function compileLocale(catalogs, locale, options, config, doMerge,
14
14
  fallbackLocales: config.fallbackLocales,
15
15
  sourceLocale: config.sourceLocale,
16
16
  });
17
+ const pseudoLocaleConfig = config.pseudoLocale.find((item) => item.locale === locale);
17
18
  if (!options.allowEmpty &&
18
- locale !== config.pseudoLocale.locale &&
19
+ !pseudoLocaleConfig &&
19
20
  missingMessages.length > 0) {
20
21
  logger.error(styleText("red", `Error: Failed to compile catalog for locale ${styleText("bold", locale)}!`));
21
22
  if (options.verbose) {
@@ -53,12 +54,13 @@ async function compileAndWrite(locale, config, options, writePath, messages, log
53
54
  const namespace = options.typescript
54
55
  ? "ts"
55
56
  : options.namespace || config.compileNamespace;
57
+ const pseudoLocaleConfig = config.pseudoLocale.find((item) => item.locale === locale);
56
58
  const { source: compiledCatalog, errors } = createCompiledCatalog(locale, messages, {
57
59
  strict: false,
58
60
  namespace,
59
61
  outputPrefix: options.outputPrefix,
60
- pseudoLocale: config.pseudoLocale.locale,
61
- pseudoLocaleOptions: config.pseudoLocale.options,
62
+ pseudoLocale: pseudoLocaleConfig?.locale,
63
+ pseudoLocaleOptions: pseudoLocaleConfig?.options,
62
64
  compilerBabelOptions: config.compilerBabelOptions,
63
65
  });
64
66
  if (errors.length) {
@@ -7,8 +7,9 @@ export async function getPathsForExtractWatcher(config) {
7
7
  const paths = [];
8
8
  const ignored = [];
9
9
  catalogs.forEach((catalog) => {
10
- paths.push(...catalog.include);
11
- ignored.push(...catalog.exclude);
10
+ const sourcePatterns = catalog.sourcePatterns;
11
+ paths.push(...sourcePatterns.include);
12
+ ignored.push(...sourcePatterns.exclude);
12
13
  });
13
14
  return { paths, ignored };
14
15
  }
package/dist/api/stats.js CHANGED
@@ -25,8 +25,9 @@ export function printStats(config, catalogs) {
25
25
  return a.localeCompare(b);
26
26
  })
27
27
  .forEach((locale) => {
28
- if (locale === config.pseudoLocale.locale)
29
- return; // skip pseudo locale
28
+ // skip pseudo locale
29
+ if (config.pseudoLocale.some((item) => item.locale === locale))
30
+ return;
30
31
  const catalog = catalogs[locale];
31
32
  // catalog is null if no catalog exists on disk and the locale
32
33
  // was not extracted due to a `--locale` filter
@@ -7,8 +7,8 @@ import { readFileSync } from "node:fs";
7
7
  import path from "node:path";
8
8
  const getTargetLocales = (config) => {
9
9
  const sourceLocale = config.sourceLocale || "en";
10
- const pseudoLocale = config.pseudoLocale.locale || "pseudo";
11
- return config.locales.filter((value) => value != sourceLocale && value != pseudoLocale);
10
+ const pseudoLocales = new Set(config.pseudoLocale.map((item) => item.locale));
11
+ return config.locales.filter((value) => value != sourceLocale && !pseudoLocales.has(value));
12
12
  };
13
13
  // Main sync method, call "Init" or "Sync" depending on the project context
14
14
  export default async function syncProcess(config, options, extractionResult) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lingui/cli",
3
- "version": "6.5.0",
4
- "description": "Lingui CLI to extract messages, compile catalogs, and manage translation workflows",
3
+ "version": "6.7.0",
4
+ "description": "Lingui CLI: extracts i18n messages from source code into catalogs and compiles the translated catalogs for the runtime",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "lingui",
@@ -54,12 +54,12 @@
54
54
  "@babel/generator": "^7.28.5",
55
55
  "@babel/parser": "^7.22.0",
56
56
  "@babel/types": "^7.21.2",
57
- "@lingui/babel-plugin-extract-messages": "6.5.0",
58
- "@lingui/babel-plugin-lingui-macro": "6.5.0",
59
- "@lingui/conf": "6.5.0",
60
- "@lingui/core": "6.5.0",
61
- "@lingui/format-po": "6.5.0",
62
- "@lingui/message-utils": "6.5.0",
57
+ "@lingui/babel-plugin-extract-messages": "6.7.0",
58
+ "@lingui/babel-plugin-lingui-macro": "6.7.0",
59
+ "@lingui/conf": "6.7.0",
60
+ "@lingui/core": "6.7.0",
61
+ "@lingui/format-po": "6.7.0",
62
+ "@lingui/message-utils": "6.7.0",
63
63
  "chokidar": "5.0.0",
64
64
  "cli-table3": "^0.6.5",
65
65
  "commander": "^14.0.2",
@@ -68,7 +68,7 @@
68
68
  "ms": "^2.1.3",
69
69
  "normalize-path": "^3.0.0",
70
70
  "ora": "^9.1.0",
71
- "pseudolocale": "^2.2.0",
71
+ "pseudolocale": "^3.1.0",
72
72
  "source-map": "^0.7.6",
73
73
  "tinypool": "^2.1.0"
74
74
  },
@@ -97,5 +97,5 @@
97
97
  "rolldown": "^1.1.1",
98
98
  "vitest": "catalog:"
99
99
  },
100
- "gitHead": "9687149054666bbaf89196573ba597d0d6fd09e4"
100
+ "gitHead": "a2d4b6d42e052fed1d432050b20cc56cfdacfe98"
101
101
  }