Skip to content
Open
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
37 changes: 37 additions & 0 deletions packages/cli/src/api/catalog/getTranslationsForCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,43 @@ describe("getTranslationsForCatalog", () => {
`)
})

it("Should report raw missing catalog entries even when fallbackLocales.default resolves them", async () => {
// prettier-ignore
const catalogStub = getCatalogStub({
...lang("pl", [
message("hashid1", "Lorem"),
message("hashid2", "Ipsum")
]),
...lang("ru", [
message("hashid1", "Lorem"),
message("hashid2", "Ipsum", true)
])
})

const actual = await getTranslationsForCatalog(catalogStub, "ru", {
sourceLocale: "en",
fallbackLocales: {
default: "pl",
},
missingBehavior: "catalog",
})

expect(actual).toMatchInlineSnapshot(`
{
messages: {
hashid1: ru: translation: Lorem,
hashid2: pl: translation: Ipsum,
},
missing: [
{
id: hashid2,
source: Ipsum,
},
],
}
`)
})

it("Should fallback to single fallbackLocales", async () => {
// prettier-ignore
const catalogStub = getCatalogStub({
Expand Down
23 changes: 18 additions & 5 deletions packages/cli/src/api/catalog/getTranslationsForCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ export type TranslationMissingEvent = {
id: string
}

export type MissingBehavior = "resolved" | "catalog"

export type GetTranslationsOptions = {
sourceLocale: string
fallbackLocales: FallbackLocales
missingBehavior?: MissingBehavior
}

export async function getTranslationsForCatalog(
Expand Down Expand Up @@ -76,7 +79,7 @@ function getTranslation(
) {
const { fallbackLocales, sourceLocale } = options

const getTranslation = (_locale: string) => {
const getCatalogTranslation = (_locale: string) => {
const localeCatalog = catalogs[_locale]
return localeCatalog?.[key]?.translation
}
Expand All @@ -87,8 +90,10 @@ function getTranslation(
if (!fL.length) return null

for (const fallbackLocale of fL) {
if (catalogs[fallbackLocale] && getTranslation(fallbackLocale)) {
return getTranslation(fallbackLocale)
const fallbackTranslation = getCatalogTranslation(fallbackLocale)

if (catalogs[fallbackLocale] && fallbackTranslation) {
return fallbackTranslation
}
}
}
Expand All @@ -99,16 +104,24 @@ function getTranslation(
// -> template message
// ** last resort **
// -> id
const catalogTranslation = getCatalogTranslation(locale)

const translation =
// Get translation in target locale
getTranslation(locale) ||
catalogTranslation ||
// We search in fallbackLocales as dependent of each locale
getMultipleFallbacks(locale) ||
(sourceLocale &&
sourceLocale === locale &&
sourceLocaleFallback(catalogs[sourceLocale], key))

if (!translation) {
const missingBehavior = options.missingBehavior ?? "resolved"
const isMissingTranslation =
missingBehavior === "catalog"
? locale !== sourceLocale && !catalogTranslation
: !translation

if (isMissingTranslation) {
onMissing({
id: key,
source:
Expand Down
18 changes: 15 additions & 3 deletions packages/cli/src/api/compile/compileLocale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { createCompiledCatalog } from "../compile.js"

import normalizePath from "normalize-path"
import nodepath from "path"
import { createCompilationErrorMessage } from "../messages.js"
import {
createCompilationErrorMessage,
getMissingBehaviorDescription,
} from "../messages.js"
import { getTranslationsForCatalog } from "../catalog/getTranslationsForCatalog.js"
import { Logger } from "../logger.js"

Expand All @@ -27,6 +30,7 @@ export async function compileLocale(
await getTranslationsForCatalog(catalog, locale, {
fallbackLocales: config.fallbackLocales,
sourceLocale: config.sourceLocale,
missingBehavior: options.missingBehavior,
})

if (
Expand All @@ -42,7 +46,12 @@ export async function compileLocale(
)

if (options.verbose) {
logger.error(styleText("red", "Missing translations:"))
logger.error(
styleText(
"red",
`Missing translations ${getMissingBehaviorDescription(options.missingBehavior ?? "resolved")}:`,
),
)
missingMessages.forEach((missing) => {
const source =
missing.source || missing.source === missing.id
Expand All @@ -53,7 +62,10 @@ export async function compileLocale(
})
} else {
logger.error(
styleText("red", `Missing ${missingMessages.length} translation(s)`),
styleText(
"red",
`Missing ${missingMessages.length} translation(s) ${getMissingBehaviorDescription(options.missingBehavior ?? "resolved")}`,
),
)
}
logger.error("")
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export {
createMissingErrorMessage,
createCompilationErrorMessage,
} from "./messages.js"
export type { MissingBehavior } from "./catalog/getTranslationsForCatalog.js"
export * from "./types.js"
5 changes: 2 additions & 3 deletions packages/cli/src/api/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@ describe("createMissingErrorMessage", () => {
source: "World",
},
],
"bla bla",
"resolved",
)

expect(message).toMatchInlineSnapshot(`
Failed to compile catalog for locale en!

Missing 2 translation(s):
Missing 2 translation(s) after applying fallbackLocales:

1: Hello
World: World

`)
})
})
Expand Down
27 changes: 23 additions & 4 deletions packages/cli/src/api/messages.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { TranslationMissingEvent } from "./catalog/getTranslationsForCatalog.js"
import type {
MissingBehavior,
TranslationMissingEvent,
} from "./catalog/getTranslationsForCatalog.js"
import { styleText } from "node:util"
import { MessageCompilationError } from "./compile.js"
import type { MessageCompilationError } from "./compile.js"

export function getMissingBehaviorDescription(
missingBehavior: MissingBehavior,
) {
return missingBehavior === "catalog"
? "before applying fallbackLocales"
: "after applying fallbackLocales"
}

function isMissingBehavior(value: string): value is MissingBehavior {
return value === "resolved" || value === "catalog"
}

export function createMissingErrorMessage(
locale: string,
missingMessages: TranslationMissingEvent[],
configurationMsg: string,
missingBehaviorOrConfigurationMsg: MissingBehavior | string = "resolved",
) {
const missingBehavior = isMissingBehavior(missingBehaviorOrConfigurationMsg)
? missingBehaviorOrConfigurationMsg
: "resolved"

let message = `Failed to compile catalog for locale ${styleText("bold", locale)}!

Missing ${missingMessages.length} translation(s):
Missing ${missingMessages.length} translation(s) ${getMissingBehaviorDescription(missingBehavior)}:
\n`

missingMessages.forEach((missing) => {
Expand Down
37 changes: 29 additions & 8 deletions packages/cli/src/lingui-compile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { styleText } from "node:util"
import { watch } from "chokidar"
import { program } from "commander"
import { Option, program } from "commander"

import { getConfig, LinguiConfigNormalized } from "@lingui/conf"
import { helpRun } from "./api/help.js"
Expand All @@ -13,10 +13,15 @@ import {
} from "./api/resolveWorkersOptions.js"
import ms from "ms"
import { getPathsForCompileWatcher } from "./api/getPathsForCompileWatcher.js"
import { ProgramExit } from "./api/ProgramExit.js"
import type { MissingBehavior } from "./api/index.js"

const failOnMissingModes: MissingBehavior[] = ["resolved", "catalog"]

export type CliCompileOptions = {
verbose?: boolean
allowEmpty?: boolean
missingBehavior?: MissingBehavior
failOnCompileError?: boolean
typescript?: boolean
watch?: boolean
Expand Down Expand Up @@ -46,7 +51,7 @@ export async function command(
try {
await compileLocale(catalogs, locale, options, config, doMerge, console)
} catch (err) {
if ((err as Error).name === "ProgramExit") {
if (err instanceof ProgramExit) {
errored = true
} else {
throw err
Expand Down Expand Up @@ -108,7 +113,8 @@ type CliArgs = {
typescript?: boolean
watch?: boolean
namespace?: string
strict?: string
strict?: boolean
failOnMissing?: MissingBehavior
config?: string
debounce?: number
workers?: number
Expand All @@ -119,7 +125,16 @@ if (import.meta.main) {
program
.description("Compile message catalogs to compiled bundle.")
.option("--config <path>", "Path to the config file")
.option("--strict", "Disable defaults for missing translations")
.option(
"--strict",
"Fail if translations are missing after applying fallbackLocales, or if compilation errors occur",
)
.addOption(
new Option(
"--fail-on-missing <mode>",
"Fail if translations are missing; modes: resolved (after fallbackLocales) or catalog (before fallbackLocales)",
).choices(failOnMissingModes),
)
.option("--verbose", "Verbose output")
.option("--typescript", "Create Typescript definition for compiled bundle")
.option(
Expand All @@ -146,10 +161,13 @@ if (import.meta.main) {
)
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(" # Compile translations but fail when resolved output")
console.log(" # still has missing translations or compilation errors")
console.log(` $ ${helpRun("compile --strict")}`)
console.log("")
console.log(" # Compile translations but fail when target catalogs")
console.log(" # have missing translations before fallbackLocales")
console.log(` $ ${helpRun("compile --fail-on-missing catalog")}`)
})
.parse(process.argv)

Expand All @@ -160,10 +178,13 @@ if (import.meta.main) {
let previousRun = Promise.resolve(true)

const compile = () => {
const shouldFailOnMissing = Boolean(options.strict || options.failOnMissing)

previousRun = previousRun.then(() =>
command(config, {
verbose: options.watch || options.verbose || false,
allowEmpty: !options.strict,
allowEmpty: !shouldFailOnMissing,
missingBehavior: options.failOnMissing ?? "resolved",
failOnCompileError: !!options.strict,
workersOptions: resolveWorkersOptions(options),
typescript:
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/test/__snapshots__/compile.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`CLI Command: Compile > allowEmpty = false > Should show error and stop compilation of catalog if message doesnt have a translation (no template) 1`] = `
Error: Failed to compile catalog for locale pl!
Missing 1 translation(s)
Missing 1 translation(s) after applying fallbackLocales

`;

Expand All @@ -15,17 +15,17 @@ exports[`CLI Command: Compile > allowEmpty = false > Should show error and stop

exports[`CLI Command: Compile > allowEmpty = false > Should show error and stop compilation of catalog if message doesnt have a translation (with template) 2`] = `
Error: Failed to compile catalog for locale en!
Missing 1 translation(s)
Missing 1 translation(s) after applying fallbackLocales

Error: Failed to compile catalog for locale pl!
Missing 1 translation(s)
Missing 1 translation(s) after applying fallbackLocales

`;

exports[`CLI Command: Compile > allowEmpty = false > Should show missing messages verbosely when verbose = true 1`] = `
en ⇒ en.js
Error: Failed to compile catalog for locale pl!
Missing translations:
Missing translations after applying fallbackLocales:
mY42CM: (Hello World)
2ZeN02: (Test String)

Expand Down
Loading
Loading