Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions packages/cli/src/api/compile/compileLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,17 @@ export async function compileLocale(
`Error: Failed to compile catalog for locale ${styleText("bold", locale)}!`,
),
)
logger.error(
styleText("red", `Missing ${missingMessages.length} translation(s)`),
)
missingMessages.forEach((missing) => {
const source =
missing.source || missing.source === missing.id
? `: (${missing.source})`
: ""

if (options.verbose) {
logger.error(styleText("red", "Missing translations:"))
missingMessages.forEach((missing) => {
const source =
missing.source || missing.source === missing.id
? `: (${missing.source})`
: ""

logger.error(`${missing.id}${source}`)
})
} else {
logger.error(
styleText("red", `Missing ${missingMessages.length} translation(s)`),
)
}
logger.verbose(`${missing.id}${source}`)
})
logger.error("")
throw new ProgramExit()
}
Expand Down Expand Up @@ -139,7 +134,6 @@ async function compileAndWrite(

compiledPath = normalizePath(nodepath.relative(config.rootDir, compiledPath))

options.verbose &&
logger.error(styleText("green", `${locale} ⇒ ${compiledPath}`))
logger.verbose(styleText("green", `${locale} ⇒ ${compiledPath}`))
return true
}
38 changes: 37 additions & 1 deletion packages/cli/src/api/logger.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
export const LOG_LEVELS = [
"silent",
"error",
"warning",
"info",
"verbose",
] as const

export type LogLevel = (typeof LOG_LEVELS)[number]

export type Logger = {
error: (msg: string) => void
error: (...args: unknown[]) => void
warn: (...args: unknown[]) => void
info: (...args: unknown[]) => void
verbose: (...args: unknown[]) => void
}

function isAtLeast(current: LogLevel, minimum: LogLevel): boolean {
return LOG_LEVELS.indexOf(current) >= LOG_LEVELS.indexOf(minimum)
}

const noop = () => {}

export function initLogger(logLevel: LogLevel): Logger {
return {
error: isAtLeast(logLevel, "error")
? (...args) => console.error(...args)
: noop,
warn: isAtLeast(logLevel, "warning")
? (...args) => console.log(...args)
: noop,
info: isAtLeast(logLevel, "info")
? (...args) => console.log(...args)
: noop,
verbose: isAtLeast(logLevel, "verbose")
? (...args) => console.log(...args)
: noop,
}
}
32 changes: 32 additions & 0 deletions packages/cli/src/api/workerLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { WorkerLogger } from "./workerLogger.js"

describe("WorkerLogger", () => {
it("error joins args with space", () => {
const logger = new WorkerLogger()
logger.error("a", "b", "c")
expect(logger.flush().errors).toBe("a b c")
})

it("flush clears errors after returning them", () => {
const logger = new WorkerLogger()
logger.error("first")
const first = logger.flush()
expect(first.errors).toBe("first")
expect(logger.flush().errors).toBe("")
})

it("multiple errors joined by newline", () => {
const logger = new WorkerLogger()
logger.error("line1")
logger.error("line2")
expect(logger.flush().errors).toBe("line1\nline2")
})

it("warn/info/verbose are no-ops", () => {
const logger = new WorkerLogger()
logger.warn("ignored")
logger.info("ignored")
logger.verbose("ignored")
expect(logger.flush().errors).toBe("")
})
})
8 changes: 6 additions & 2 deletions packages/cli/src/api/workerLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ export type SerializedLogs = {
export class WorkerLogger implements Logger {
private errors: string[] = []

error(msg: string): void {
this.errors.push(msg)
error(...args: unknown[]): void {
this.errors.push(args.join(" "))
}

warn(): void {}
info(): void {}
verbose(): void {}

flush(): SerializedLogs {
const errors = this.errors.join("\n")
this.errors = []
Expand Down
45 changes: 34 additions & 11 deletions packages/cli/src/lingui-compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
} from "./api/resolveWorkersOptions.js"
import ms from "ms"
import { getPathsForCompileWatcher } from "./api/getPathsForCompileWatcher.js"
import { initLogger, LOG_LEVELS, LogLevel } from "./api/logger.js"

export type CliCompileOptions = {
verbose?: boolean
logLevel: LogLevel
allowEmpty?: boolean
failOnCompileError?: boolean
typescript?: boolean
Expand All @@ -30,11 +31,12 @@ export async function command(
options: CliCompileOptions,
) {
const startTime = Date.now()
const logger = initLogger(options.logLevel)

// Check config.compile.merge if catalogs for current locale are to be merged into a single compiled file
const doMerge = !!config.catalogsMergePath

console.log("Compiling message catalogs…")
logger.info("Compiling message catalogs…")

let errored = false

Expand All @@ -44,7 +46,7 @@ export async function command(

for (const locale of config.locales) {
try {
await compileLocale(catalogs, locale, options, config, doMerge, console)
await compileLocale(catalogs, locale, options, config, doMerge, logger)
} catch (err) {
if ((err as Error).name === "ProgramExit") {
errored = true
Expand All @@ -61,8 +63,7 @@ export async function command(
)
}

options.verbose &&
console.log(`Use worker pool of size ${options.workersOptions.poolSize}`)
logger.verbose(`Use worker pool of size ${options.workersOptions.poolSize}`)

const pool = createCompileWorkerPool({
poolSize: options.workersOptions.poolSize,
Expand All @@ -79,7 +80,7 @@ export async function command(
)

if (logs.errors) {
console.error(logs.errors)
logger.error(logs.errors)
}

if (exitProgram) {
Expand All @@ -97,12 +98,13 @@ export async function command(
}
}

console.log(`Done in ${ms(Date.now() - startTime)}`)
logger.info(`Done in ${ms(Date.now() - startTime)}`)

return !errored
}

type CliArgs = {
logLevel?: LogLevel
verbose?: boolean
allowEmpty?: boolean
typescript?: boolean
Expand All @@ -120,7 +122,8 @@ if (import.meta.main) {
.description("Compile message catalogs to compiled bundle.")
.option("--config <path>", "Path to the config file")
.option("--strict", "Disable defaults for missing translations")
.option("--verbose", "Verbose output")
.option("--log-level <level>", `Set log level (${LOG_LEVELS.join("|")})`)
.option("--verbose", "Verbose output (alias for --log-level=verbose)")
.option("--typescript", "Create Typescript definition for compiled bundle")
.option(
"--workers <n>",
Expand Down Expand Up @@ -155,14 +158,32 @@ if (import.meta.main) {

const options = program.opts<CliArgs>()

if (options.logLevel && !LOG_LEVELS.includes(options.logLevel)) {
console.error(
`Invalid --log-level "${options.logLevel}". Valid levels: ${LOG_LEVELS.join(", ")}`,
)
process.exit(1)
}

let logLevel: LogLevel = options.logLevel ?? "info"

if (options.verbose) {
if (options.logLevel && options.logLevel !== "verbose") {
console.warn(
`Warning: --verbose conflicts with --log-level=${options.logLevel}. Using --log-level=verbose.`,
)
}
logLevel = "verbose"
}

const config = getConfig({ configPath: options.config })

let previousRun = Promise.resolve(true)

const compile = () => {
previousRun = previousRun.then(() =>
command(config, {
verbose: options.watch || options.verbose || false,
logLevel,
allowEmpty: !options.strict,
failOnCompileError: !!options.strict,
workersOptions: resolveWorkersOptions(options),
Expand All @@ -187,9 +208,11 @@ if (import.meta.main) {
debounceTimer = setTimeout(() => compile(), options.debounce)
}

const logger = initLogger(logLevel)

// Check if Watch Mode is enabled
if (options.watch) {
console.info(styleText("bold", "Initializing Watch Mode..."))
logger.info(styleText("bold", "Initializing Watch Mode..."))
;(async function initWatch() {
const { paths } = await getPathsForCompileWatcher(config)

Expand All @@ -198,7 +221,7 @@ if (import.meta.main) {
})

const onReady = () => {
console.info(styleText(["green", "bold"], "Watcher is ready!"))
logger.info(styleText(["green", "bold"], "Watcher is ready!"))
watcher
.on("add", () => dispatchCompile())
.on("change", () => dispatchCompile())
Expand Down
42 changes: 32 additions & 10 deletions packages/cli/src/lingui-extract-experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
} from "./api/resolveWorkersOptions.js"
import { extractFromBundleAndWrite } from "./extract-experimental/extractFromBundleAndWrite.js"
import { createExtractExperimentalWorkerPool } from "./api/workerPools.js"
import { initLogger, LOG_LEVELS, LogLevel } from "./api/logger.js"

type CliExtractTemplateOptions = {
verbose?: boolean
type CliExtractExperimentalOptions = {
logLevel: LogLevel
files?: string[]
template?: boolean
locales?: string[]
Expand All @@ -28,9 +29,11 @@ type CliExtractTemplateOptions = {

export default async function command(
linguiConfig: LinguiConfigNormalized,
options: CliExtractTemplateOptions,
options: CliExtractExperimentalOptions,
): Promise<boolean> {
options.verbose && console.log("Extracting messages from source files…")
const logger = initLogger(options.logLevel)

logger.verbose("Extracting messages from source files…")

const extractorConfig = linguiConfig.experimental?.extractor

Expand All @@ -40,7 +43,7 @@ export default async function command(
)
}

console.log(
logger.warn(
styleText(
"yellow",
[
Expand Down Expand Up @@ -82,8 +85,7 @@ export default async function command(
)
}

options.verbose &&
console.log(`Use worker pool of size ${options.workersOptions.poolSize}`)
logger.verbose(`Use worker pool of size ${options.workersOptions.poolSize}`)

const pool = createExtractExperimentalWorkerPool({
poolSize: options.workersOptions.poolSize,
Expand Down Expand Up @@ -160,14 +162,15 @@ export default async function command(
stats
.sort((a, b) => a.entry.localeCompare(b.entry))
.forEach(({ entry, content }) => {
console.log([`Catalog statistics for ${entry}:`, content, ""].join("\n"))
logger.info([`Catalog statistics for ${entry}:`, content, ""].join("\n"))
})

return commandSuccess
}

type CliArgs = {
config?: string
logLevel?: LogLevel
verbose?: boolean
template?: boolean
locale?: string
Expand All @@ -183,7 +186,8 @@ if (import.meta.main) {
.option("--overwrite", "Overwrite translations for source locale")
.option("--clean", "Remove obsolete translations")
.option("--locale <locale, [...]>", "Only extract the specified locales")
.option("--verbose", "Verbose output")
.option("--log-level <level>", `Set log level (${LOG_LEVELS.join("|")})`)
.option("--verbose", "Verbose output (alias for --log-level=verbose)")
.option(
"--workers <n>",
"Number of worker threads to use (default: CPU count - 1, capped at 8). Pass `--workers 1` to disable worker threads and run everything in a single process",
Expand All @@ -192,12 +196,30 @@ if (import.meta.main) {

const options = program.opts<CliArgs>()

if (options.logLevel && !LOG_LEVELS.includes(options.logLevel)) {
console.error(
`Invalid --log-level "${options.logLevel}". Valid levels: ${LOG_LEVELS.join(", ")}`,
)
process.exit(1)
}

let logLevel: LogLevel = options.logLevel ?? "info"

if (options.verbose) {
if (options.logLevel && options.logLevel !== "verbose") {
console.warn(
`Warning: --verbose conflicts with --log-level=${options.logLevel}. Using --log-level=verbose.`,
)
}
logLevel = "verbose"
}

const config = getConfig({
configPath: options.config,
})

const result = command(config, {
verbose: options.verbose || false,
logLevel,
template: options.template,
locales: options.locale?.split(","),
overwrite: options.overwrite,
Expand Down
Loading
Loading