Skip to content

Commit 53ffaa8

Browse files
authored
Merge pull request #814 from ThiefMaster/extract-pot
Add `lingui extract-template` command
2 parents be377f4 + 1ed1bb7 commit 53ffaa8

8 files changed

Lines changed: 183 additions & 2 deletions

File tree

docs/ref/cli.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ Convert message catalogs from previous format (see :conf:`format` option).
7373

7474
Prints additional information.
7575

76+
``extract-template``
77+
--------------------
78+
79+
.. lingui-cli:: extract-template [--verbose]
80+
81+
This command extracts messages from source files and creates a ``.pot`` template file.
82+
83+
.. lingui-cli-option:: --verbose
84+
85+
Prints additional information.
86+
7687
``compile``
7788
-----------
7889

packages/cli/src/api/__snapshots__/catalog.test.ts.snap

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,39 @@ Object {
211211
}
212212
`;
213213

214+
exports[`Catalog makeTemplate should collect and write a template 1`] = `null`;
215+
216+
exports[`Catalog makeTemplate should collect and write a template 2`] = `
217+
Object {
218+
Component A: Object {
219+
comment: undefined,
220+
comments: Array [],
221+
flags: Array [],
222+
obsolete: false,
223+
origin: Array [
224+
Array [
225+
collect/componentA/componentA.js,
226+
1,
227+
],
228+
],
229+
translation: ,
230+
},
231+
Hello World: Object {
232+
comment: undefined,
233+
comments: Array [],
234+
flags: Array [],
235+
obsolete: false,
236+
origin: Array [
237+
Array [
238+
collect/componentA/index.js,
239+
1,
240+
],
241+
],
242+
translation: ,
243+
},
244+
}
245+
`;
246+
214247
exports[`Catalog read should read file in given format 1`] = `
215248
Object {
216249
obsolete: Object {

packages/cli/src/api/catalog.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createCompiledCatalog } from "./compile"
1717
import {
1818
copyFixture,
1919
defaultMakeOptions,
20+
defaultMakeTemplateOptions,
2021
makeNextMessage,
2122
defaultMergeOptions,
2223
makeCatalog,
@@ -80,6 +81,32 @@ describe("Catalog", function () {
8081
})
8182
})
8283

84+
describe("makeTemplate", function () {
85+
it("should collect and write a template", function () {
86+
const localeDir = copyFixture(fixture("locales", "initial"))
87+
const catalog = new Catalog(
88+
{
89+
name: "messages",
90+
path: path.join(localeDir, "{locale}", "messages"),
91+
include: [
92+
fixture("collect/componentA/"),
93+
fixture("collect/componentB"),
94+
],
95+
exclude: [],
96+
},
97+
mockConfig({
98+
locales: ["en", "cs"],
99+
})
100+
)
101+
102+
// Everything should be empty
103+
expect(catalog.readTemplate()).toMatchSnapshot()
104+
105+
catalog.makeTemplate(defaultMakeTemplateOptions)
106+
expect(catalog.readTemplate()).toMatchSnapshot()
107+
})
108+
})
109+
83110
describe("collect", function () {
84111
it("should extract messages from source files", function () {
85112
const catalog = new Catalog(

packages/cli/src/api/catalog.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,25 @@ import {
2121
CatalogType,
2222
} from "./types"
2323
import { CliExtractOptions } from "../lingui-extract"
24+
import { CliExtractTemplateOptions } from "../lingui-extract-template"
2425

2526
const NAME = "{name}"
2627
const LOCALE = "{locale}"
28+
const LOCALE_SUFFIX_RE = /\{locale\}.*$/
2729
const PATHSEP = "/" // force posix everywhere
2830

2931
export type MakeOptions = CliExtractOptions & {
3032
projectType?: string
3133
orderBy?: OrderBy
3234
}
3335

36+
export type MakeTemplateOptions = CliExtractTemplateOptions & {
37+
projectType?: string
38+
orderBy?: OrderBy
39+
}
40+
41+
type CollectOptions = MakeOptions | MakeTemplateOptions
42+
3443
export type MergeOptions = {
3544
overwrite: boolean
3645
}
@@ -87,10 +96,16 @@ export class Catalog {
8796
this.writeAll(cleanAndSort(catalogs))
8897
}
8998

99+
makeTemplate(options: MakeTemplateOptions) {
100+
const catalog = this.collect(options)
101+
const sort = order(options.orderBy) as (catalog: CatalogType) => CatalogType
102+
this.writeTemplate(sort(catalog as CatalogType))
103+
}
104+
90105
/**
91106
* Collect messages from source paths. Return a raw message catalog as JSON.
92107
*/
93-
collect(options: MakeOptions) {
108+
collect(options: CollectOptions) {
94109
const tmpDir = path.join(os.tmpdir(), `lingui-${process.pid}`)
95110

96111
if (fs.existsSync(tmpDir)) {
@@ -251,6 +266,17 @@ export class Catalog {
251266
this.locales.forEach((locale) => this.write(locale, catalogs[locale]))
252267
}
253268

269+
writeTemplate(messages: CatalogType) {
270+
const filename = this.templateFile
271+
const basedir = path.dirname(filename)
272+
if (!fs.existsSync(basedir)) {
273+
fs.mkdirpSync(basedir)
274+
}
275+
const options = { ...this.config.formatOptions, locale: undefined }
276+
const poFormat = getFormat("po")
277+
poFormat.write(filename, messages, options)
278+
}
279+
254280
writeCompiled(locale: string, compiledCatalog: string, namespace?: string) {
255281
const ext = `.${namespace === "es" ? "mjs" : "js"}`
256282
const filename = this.path.replace(LOCALE, locale) + ext
@@ -279,6 +305,12 @@ export class Catalog {
279305
) as AllCatalogsType
280306
}
281307

308+
readTemplate() {
309+
const filename = this.templateFile
310+
if (!fs.existsSync(filename)) return null
311+
return this.format.read(filename)
312+
}
313+
282314
get sourcePaths() {
283315
const includeGlobs = this.include.map(
284316
(includePath) =>
@@ -291,6 +323,10 @@ export class Catalog {
291323
return glob.sync(patterns, { ignore: this.exclude, mark: true })
292324
}
293325

326+
get templateFile() {
327+
return this.path.replace(LOCALE_SUFFIX_RE, "messages.pot")
328+
}
329+
294330
get localeDir() {
295331
const localePatternIndex = this.path.indexOf(LOCALE)
296332
if (localePatternIndex === -1) {

packages/cli/src/api/formats/po.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ const po: CatalogFormatter = {
8989
} else {
9090
po = new PO()
9191
po.headers = getCreateHeaders(options.locale)
92+
if (options.locale === undefined) {
93+
delete po.headers.Language;
94+
}
9295
po.headerOrder = R.keys(po.headers)
9396
}
9497
po.items = serialize(catalog, options)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import chalk from "chalk"
2+
import program from "commander"
3+
4+
import { getConfig, LinguiConfig } from "@lingui/conf"
5+
6+
import { getCatalogs } from "./api/catalog"
7+
import { detect } from "./api/detect"
8+
9+
export type CliExtractTemplateOptions = {
10+
verbose: boolean
11+
}
12+
13+
export default function command(
14+
config: LinguiConfig,
15+
options: Partial<CliExtractTemplateOptions>
16+
): boolean {
17+
// `react-app` babel plugin used by CRA requires either BABEL_ENV or NODE_ENV to be
18+
// set. We're setting it here, because lingui macros are going to use them as well.
19+
if (!process.env.BABEL_ENV && !process.env.NODE_ENV) {
20+
process.env.BABEL_ENV = "development"
21+
}
22+
23+
// We need macros to keep imports, so extract-messages plugin know what componets
24+
// to collect. Users usually use both BABEN_ENV and NODE_ENV, so it's probably
25+
// safer to introduce a new env variable. LINGUI_EXTRACT=1 during `lingui extract`
26+
process.env.LINGUI_EXTRACT = "1"
27+
28+
options.verbose && console.error("Extracting messages from source files…")
29+
const catalogs = getCatalogs(config)
30+
const catalogStats: { [path: string]: Number } = {}
31+
catalogs.forEach((catalog) => {
32+
catalog.makeTemplate({
33+
...options,
34+
orderBy: config.orderBy,
35+
projectType: detect(),
36+
})
37+
38+
catalogStats[catalog.templateFile] = Object.keys(catalog.readTemplate()).length
39+
})
40+
41+
Object.entries(catalogStats).forEach(([key, value]) => {
42+
console.log(
43+
`Catalog statistics for ${chalk.bold(key)}: ${chalk.green(
44+
value
45+
)} messages`
46+
)
47+
console.log()
48+
})
49+
return true
50+
}
51+
52+
if (require.main === module) {
53+
program
54+
.option("--config <path>", "Path to the config file")
55+
.option("--verbose", "Verbose output")
56+
.parse(process.argv)
57+
58+
const config = getConfig({ configPath: program.config })
59+
60+
const result = command(config, {
61+
verbose: program.verbose || false,
62+
})
63+
64+
if (!result) process.exit(1)
65+
}

packages/cli/src/lingui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ program
1515
.version(version)
1616
.command("add-locale", "Deprecated, run it for instructions")
1717
.command("extract [files...]", "Extracts messages from source files")
18+
.command("extract-template", "Extracts messages from source files to a .pot template")
1819
.command("compile", "Compile message catalogs")
1920
.parse(process.argv)
2021

packages/cli/src/tests.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "fs-extra"
33
import path from "path"
44

55
import { mockConfig } from "@lingui/jest-mocks"
6-
import { Catalog, MakeOptions, MergeOptions } from "./api/catalog"
6+
import { Catalog, MakeOptions, MakeTemplateOptions, MergeOptions } from "./api/catalog"
77
import { ExtractedMessageType, MessageType } from "./api/types"
88

99
export function copyFixture(fixtureDir) {
@@ -25,6 +25,11 @@ export const defaultMakeOptions: MakeOptions = {
2525
orderBy: "messageId",
2626
}
2727

28+
export const defaultMakeTemplateOptions: MakeTemplateOptions = {
29+
verbose: false,
30+
orderBy: "messageId",
31+
}
32+
2833
export const defaultMergeOptions: MergeOptions = {
2934
overwrite: false,
3035
}

0 commit comments

Comments
 (0)