-
Notifications
You must be signed in to change notification settings - Fork 363
/
lingui-compile.ts
257 lines (221 loc) · 7.46 KB
/
lingui-compile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import chalk from "chalk"
import chokidar from "chokidar"
import fs from "fs"
import { program } from "commander"
import { getConfig, LinguiConfigNormalized } from "@lingui/conf"
import { createCompiledCatalog } from "./api/compile"
import { helpRun } from "./api/help"
import { getCatalogs, getFormat } from "./api"
import { TranslationMissingEvent } from "./api/catalog/getTranslationsForCatalog"
import { getCatalogForMerge } from "./api/catalog/getCatalogs"
import { normalizeSlashes } from "./api/utils"
import nodepath from "path"
export type CliCompileOptions = {
verbose?: boolean
allowEmpty?: boolean
typescript?: boolean
watch?: boolean
namespace?: string
}
export async function command(
config: LinguiConfigNormalized,
options: CliCompileOptions
) {
const catalogs = await getCatalogs(config)
// Check config.compile.merge if catalogs for current locale are to be merged into a single compiled file
const doMerge = !!config.catalogsMergePath
let mergedCatalogs = {}
console.log("Compiling message catalogs…")
for (const locale of config.locales) {
for (const catalog of catalogs) {
const missingMessages: TranslationMissingEvent[] = []
const messages = await catalog.getTranslations(locale, {
fallbackLocales: config.fallbackLocales,
sourceLocale: config.sourceLocale,
onMissing: (missing) => {
missingMessages.push(missing)
},
})
if (!options.allowEmpty && missingMessages.length > 0) {
console.error(
chalk.red(
`Error: Failed to compile catalog for locale ${chalk.bold(locale)}!`
)
)
if (options.verbose) {
console.error(chalk.red("Missing translations:"))
missingMessages.forEach((missing) => {
const source =
missing.source || missing.source === missing.id
? `: (${missing.source})`
: ""
console.error(`${missing.id}${source}`)
})
} else {
console.error(
chalk.red(`Missing ${missingMessages.length} translation(s)`)
)
}
console.error()
return false
}
if (doMerge) {
mergedCatalogs = { ...mergedCatalogs, ...messages }
} else {
const namespace = options.typescript
? "ts"
: options.namespace || config.compileNamespace
const compiledCatalog = createCompiledCatalog(locale, messages, {
strict: false,
namespace,
pseudoLocale: config.pseudoLocale,
compilerBabelOptions: config.compilerBabelOptions,
})
let compiledPath = await catalog.writeCompiled(
locale,
compiledCatalog,
namespace
)
if (options.typescript) {
const typescriptPath = compiledPath.replace(/\.ts?$/, "") + ".d.ts"
fs.writeFileSync(
typescriptPath,
`import type { Messages } from '@lingui/core';
declare const messages: Messages;
export { messages };
`
)
}
compiledPath = normalizeSlashes(
nodepath.relative(config.rootDir, compiledPath)
)
options.verbose &&
console.error(chalk.green(`${locale} ⇒ ${compiledPath}`))
}
}
if (doMerge) {
const compileCatalog = await getCatalogForMerge(config)
const namespace = options.namespace || config.compileNamespace
const compiledCatalog = createCompiledCatalog(locale, mergedCatalogs, {
strict: false,
namespace: namespace,
pseudoLocale: config.pseudoLocale,
compilerBabelOptions: config.compilerBabelOptions,
})
let compiledPath = await compileCatalog.writeCompiled(
locale,
compiledCatalog,
namespace
)
compiledPath = normalizeSlashes(
nodepath.relative(config.rootDir, compiledPath)
)
options.verbose && console.log(chalk.green(`${locale} ⇒ ${compiledPath}`))
}
}
return true
}
type CliOptions = {
verbose?: boolean
allowEmpty?: boolean
typescript?: boolean
watch?: boolean
namespace?: string
strict?: string
config?: string
debounce?: number
}
if (require.main === module) {
program
.description(
"Add compile message catalogs and add language data (plurals) to compiled bundle."
)
.option("--config <path>", "Path to the config file")
.option("--strict", "Disable defaults for missing translations")
.option("--verbose", "Verbose output")
.option("--typescript", "Create Typescript definition for compiled bundle")
.option(
"--namespace <namespace>",
"Specify namespace for compiled bundle. Ex: cjs(default) -> module.exports, es -> export, window.test -> window.test"
)
.option("--watch", "Enables Watch Mode")
.option(
"--debounce <delay>",
"Debounces compilation for given amount of milliseconds"
)
.on("--help", function () {
console.log("\n Examples:\n")
console.log(
" # Compile translations and use defaults or message IDs for missing translations"
)
console.log(` $ ${helpRun("compile")}`)
console.log("")
console.log(" # Compile translations but fail when there are missing")
console.log(" # translations (don't replace missing translations with")
console.log(" # default messages or message IDs)")
console.log(` $ ${helpRun("compile --strict")}`)
})
.parse(process.argv)
const options = program.opts<CliOptions>()
const config = getConfig({ configPath: options.config })
let previousRun = Promise.resolve(true)
const compile = () => {
previousRun = previousRun.then(() =>
command(config, {
verbose: options.watch || options.verbose || false,
allowEmpty: !options.strict,
typescript:
options.typescript || config.compileNamespace === "ts" || false,
namespace: options.namespace, // we want this to be undefined if user does not specify so default can be used
})
)
return previousRun
}
let debounceTimer: NodeJS.Timer
const dispatchCompile = () => {
// Skip debouncing if not enabled
if (!options.debounce) compile()
// CLear the previous timer if there is any, and schedule the next
debounceTimer && clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => compile(), options.debounce)
}
// Check if Watch Mode is enabled
if (options.watch) {
console.info(chalk.bold("Initializing Watch Mode..."))
;(async function initWatch() {
const format = await getFormat(
config.format,
config.formatOptions,
config.sourceLocale
)
const catalogs = await getCatalogs(config)
const paths: string[] = []
config.locales.forEach((locale) => {
catalogs.forEach((catalog) => {
paths.push(
`${catalog.path
.replace(/{locale}/g, locale)
.replace(/{name}/g, "*")}${format.getCatalogExtension()}`
)
})
})
const watcher = chokidar.watch(paths, {
persistent: true,
})
const onReady = () => {
console.info(chalk.green.bold("Watcher is ready!"))
watcher
.on("add", () => dispatchCompile())
.on("change", () => dispatchCompile())
}
watcher.on("ready", () => onReady())
})()
} else {
compile().then((results) => {
if (!results) {
process.exit(1)
}
console.log("Done!")
})
}
}