Skip to content

Commit 18d7faa

Browse files
committed
feat(cli): add missing check modes
1 parent 8670aa0 commit 18d7faa

14 files changed

Lines changed: 344 additions & 79 deletions

File tree

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

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { getTranslationsForCatalog } from "./getTranslationsForCatalog.js"
2-
import { Catalog } from "../catalog.js"
32
import type { AllCatalogsType, CatalogType } from "../types.js"
43

5-
function getCatalogStub(
6-
catalogs: AllCatalogsType,
7-
template: CatalogType = {},
8-
): Catalog {
9-
const catalogStub: Partial<Catalog> = {
4+
function getCatalogStub(catalogs: AllCatalogsType, template: CatalogType = {}) {
5+
return {
106
async readAll(): Promise<AllCatalogsType> {
117
return catalogs
128
},
@@ -15,8 +11,6 @@ function getCatalogStub(
1511
return template
1612
},
1713
}
18-
19-
return catalogStub as Catalog
2014
}
2115

2216
function lang(
@@ -44,6 +38,18 @@ function message(id: string, source: string, noTranslation = false) {
4438
})
4539
}
4640

41+
function obsoleteMessage(id: string, source: string, noTranslation = false) {
42+
return (locale: string): CatalogType => ({
43+
[id]: {
44+
message: source,
45+
translation: noTranslation
46+
? undefined
47+
: `${locale}: translation: ${source}`,
48+
obsolete: true,
49+
},
50+
})
51+
}
52+
4753
describe("getTranslationsForCatalog", () => {
4854
it("Should return translated catalog if all translation exists", async () => {
4955
// prettier-ignore
@@ -418,4 +424,93 @@ describe("getTranslationsForCatalog", () => {
418424
}
419425
`)
420426
})
427+
428+
it("Should ignore obsolete messages that are not active anywhere", async () => {
429+
// prettier-ignore
430+
const catalogStub = getCatalogStub({
431+
...lang("pl", [
432+
message("hashid1", "Lorem"),
433+
obsoleteMessage("hashid2", "Ipsum", true)
434+
])
435+
})
436+
437+
const actual = await getTranslationsForCatalog(catalogStub, "pl", {
438+
sourceLocale: "en",
439+
fallbackLocales: {},
440+
})
441+
442+
expect(actual).toMatchInlineSnapshot(`
443+
{
444+
messages: {
445+
hashid1: pl: translation: Lorem,
446+
},
447+
missing: [],
448+
}
449+
`)
450+
})
451+
452+
it("Should not use obsolete target translations for active messages", async () => {
453+
// prettier-ignore
454+
const catalogStub = getCatalogStub({
455+
...lang("pl", [
456+
obsoleteMessage("hashid1", "Lorem")
457+
])
458+
}, lang("tpl", [
459+
message("hashid1", "Lorem", true)
460+
]).tpl)
461+
462+
const actual = await getTranslationsForCatalog(catalogStub, "pl", {
463+
sourceLocale: "en",
464+
fallbackLocales: {},
465+
})
466+
467+
expect(actual).toMatchInlineSnapshot(`
468+
{
469+
messages: {
470+
hashid1: Lorem,
471+
},
472+
missing: [
473+
{
474+
id: hashid1,
475+
source: Lorem,
476+
},
477+
],
478+
}
479+
`)
480+
})
481+
482+
it("Should not use obsolete fallback translations", async () => {
483+
// prettier-ignore
484+
const catalogStub = getCatalogStub({
485+
...lang("pl", [
486+
obsoleteMessage("hashid1", "Lorem")
487+
]),
488+
...lang("ru", [
489+
message("hashid1", "Lorem", true)
490+
])
491+
}, lang("tpl", [
492+
message("hashid1", "Lorem", true)
493+
]).tpl)
494+
495+
const actual = await getTranslationsForCatalog(catalogStub, "ru", {
496+
sourceLocale: "en",
497+
fallbackLocales: {
498+
default: "pl",
499+
},
500+
})
501+
502+
expect(actual).toMatchInlineSnapshot(`
503+
{
504+
messages: {
505+
hashid1: Lorem,
506+
},
507+
missing: [
508+
{
509+
id: hashid1,
510+
source: Lorem,
511+
},
512+
],
513+
}
514+
`)
515+
})
421516
})

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

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Catalog } from "../catalog.js"
21
import { FallbackLocales } from "@lingui/conf"
32
import type { AllCatalogsType, CatalogType, MessageType } from "../types.js"
43
import { getFallbackListForLocale } from "./getFallbackListForLocale.js"
@@ -10,14 +9,23 @@ export type TranslationMissingEvent = {
109

1110
export type MissingBehavior = "resolved" | "catalog"
1211

12+
export function isMissingBehavior(value: string): value is MissingBehavior {
13+
return value === "resolved" || value === "catalog"
14+
}
15+
1316
export type GetTranslationsOptions = {
1417
sourceLocale: string
1518
fallbackLocales: FallbackLocales
1619
missingBehavior?: MissingBehavior
1720
}
1821

22+
type CatalogTranslationsReader = {
23+
readAll(locales: string[]): Promise<AllCatalogsType>
24+
readTemplate(): Promise<CatalogType | undefined>
25+
}
26+
1927
export async function getTranslationsForCatalog(
20-
catalog: Catalog,
28+
catalog: CatalogTranslationsReader,
2129
locale: string,
2230
options: GetTranslationsOptions,
2331
) {
@@ -27,22 +35,24 @@ export async function getTranslationsForCatalog(
2735
...getFallbackListForLocale(options.fallbackLocales, locale),
2836
])
2937

30-
const [catalogs, template] = await Promise.all([
38+
const [rawCatalogs, rawTemplate] = await Promise.all([
3139
catalog.readAll(Array.from(locales)),
3240
catalog.readTemplate(),
3341
])
3442

43+
const catalogs = withoutObsolete(rawCatalogs)
44+
const template = withoutObsoleteCatalog(rawTemplate)
3545
const sourceLocaleCatalog = catalogs[options.sourceLocale] || {}
3646

3747
const input = { ...template, ...sourceLocaleCatalog, ...catalogs[locale] }
3848

3949
const missing: TranslationMissingEvent[] = []
4050

41-
const messages = Object.keys(input).reduce<{ [id: string]: string }>(
42-
(acc, key) => {
51+
const messages = Object.entries(input).reduce<{ [id: string]: string }>(
52+
(acc, [key, msg]) => {
4353
acc[key] = getTranslation(
4454
catalogs,
45-
input[key]!,
55+
msg,
4656
locale,
4757
key,
4858
(event) => {
@@ -61,12 +71,41 @@ export async function getTranslationsForCatalog(
6171
}
6272
}
6373

74+
function isActiveMessage(
75+
message: MessageType | undefined,
76+
): message is MessageType {
77+
return Boolean(message && !message.obsolete)
78+
}
79+
80+
function withoutObsolete(catalogs: AllCatalogsType): AllCatalogsType {
81+
return Object.fromEntries(
82+
Object.entries(catalogs).map(([locale, catalog]) => [
83+
locale,
84+
withoutObsoleteCatalog(catalog),
85+
]),
86+
)
87+
}
88+
89+
function withoutObsoleteCatalog(catalog: CatalogType | undefined): CatalogType {
90+
const activeCatalog: CatalogType = {}
91+
92+
Object.entries(catalog ?? {}).forEach(([id, message]) => {
93+
if (isActiveMessage(message)) {
94+
activeCatalog[id] = message
95+
}
96+
})
97+
98+
return activeCatalog
99+
}
100+
64101
function sourceLocaleFallback(catalog: CatalogType | undefined, key: string) {
65-
if (!catalog?.[key]) {
102+
const message = catalog?.[key]
103+
104+
if (!isActiveMessage(message)) {
66105
return undefined
67106
}
68107

69-
return catalog[key].translation || catalog[key].message
108+
return message.translation || message.message
70109
}
71110

72111
function getTranslation(
@@ -81,7 +120,13 @@ function getTranslation(
81120

82121
const getCatalogTranslation = (_locale: string) => {
83122
const localeCatalog = catalogs[_locale]
84-
return localeCatalog?.[key]?.translation
123+
const message = localeCatalog?.[key]
124+
125+
if (!isActiveMessage(message)) {
126+
return undefined
127+
}
128+
129+
return message.translation
85130
}
86131

87132
const getMultipleFallbacks = (_locale: string) => {

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

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
getTranslationsForCatalog,
77
TranslationMissingEvent,
88
} from "./getTranslationsForCatalog.js"
9-
import type { CatalogType } from "../types.js"
9+
import type { MissingBehavior } from "./getTranslationsForCatalog.js"
1010

1111
export type MissingTranslationFinding = CheckFindingBase & {
1212
code: "missing_translation"
@@ -22,50 +22,24 @@ function createMissingTranslationMessage(messageId: string, source?: string) {
2222
export async function getCatalogTranslationsWithMissing(
2323
catalog: Catalog,
2424
locale: string,
25+
missingBehavior: MissingBehavior = "resolved",
2526
) {
2627
const { messages, missing } = await getTranslationsForCatalog(
2728
catalog,
2829
locale,
2930
{
3031
fallbackLocales: catalog.config.fallbackLocales,
3132
sourceLocale: catalog.config.sourceLocale,
32-
missingBehavior: "catalog",
33+
missingBehavior,
3334
},
3435
)
3536

36-
const [selectedCatalog, sourceLocaleCatalog, templateCatalog] =
37-
await Promise.all([
38-
catalog.read(locale),
39-
catalog.read(catalog.config.sourceLocale),
40-
catalog.readTemplate(),
41-
])
42-
43-
const activeMessageIds = getActiveMessageIds(
44-
selectedCatalog,
45-
sourceLocaleCatalog,
46-
templateCatalog,
47-
)
48-
4937
return {
5038
messages,
51-
missing: missing.filter((entry) => activeMessageIds.has(entry.id)),
39+
missing,
5240
}
5341
}
5442

55-
function getActiveMessageIds(...catalogs: Array<CatalogType | undefined>) {
56-
const activeMessageIds = new Set<string>()
57-
58-
catalogs.forEach((catalog) => {
59-
Object.entries(catalog ?? {}).forEach(([id, message]) => {
60-
if (!message.obsolete) {
61-
activeMessageIds.add(id)
62-
}
63-
})
64-
})
65-
66-
return activeMessageIds
67-
}
68-
6943
export function createMissingTranslationFinding(
7044
catalog: Catalog,
7145
locale: string,

packages/cli/src/api/check/index.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,36 @@ import {
88
checkSpecificOptions,
99
} from "./types.js"
1010

11-
const registeredChecks = [
12-
syncCheck,
13-
missingCheck,
14-
] as const satisfies readonly CheckDefinition[]
11+
export const checkDefinitionsByName = {
12+
sync: syncCheck,
13+
missing: missingCheck,
14+
} satisfies Record<CheckName, CheckDefinition>
1515

16-
export const checkDefinitionsByName: Record<CheckName, CheckDefinition> =
17-
Object.fromEntries(
18-
registeredChecks.map((check) => [check.name, check]),
19-
) as Record<CheckName, CheckDefinition>
16+
const registeredChecks: readonly CheckDefinition[] = [
17+
checkDefinitionsByName.sync,
18+
checkDefinitionsByName.missing,
19+
]
2020

2121
export function getRegisteredChecks(): readonly CheckDefinition[] {
2222
return registeredChecks
2323
}
2424

2525
function getSupportedOptions(check: CheckDefinition) {
26-
return check.cli.options.map((option) => option.name)
26+
return check.cli.options.map((option) => option.runOption)
27+
}
28+
29+
function getCliOptionName(option: CheckSpecificOption) {
30+
for (const check of registeredChecks) {
31+
const cliOption = check.cli.options.find(
32+
(currentOption) => currentOption.runOption === option,
33+
)
34+
35+
if (cliOption) {
36+
return cliOption.name
37+
}
38+
}
39+
40+
return option
2741
}
2842

2943
function findSupportedCheck(option: CheckSpecificOption): CheckName {
@@ -48,17 +62,22 @@ export function validateSupportedOptions(
4862
}
4963

5064
const supportedCheck = findSupportedCheck(option)
65+
const cliOptionName = getCliOptionName(option)
5166

5267
throw new Error(
53-
`Option \`--${option}\` can only be used with the \`${supportedCheck}\` check.`,
68+
`Option \`--${cliOptionName}\` can only be used with the \`${supportedCheck}\` check.`,
5469
)
5570
})
5671
}
5772

73+
function isCheckName(inputCheck: string): inputCheck is CheckName {
74+
return inputCheck === "sync" || inputCheck === "missing"
75+
}
76+
5877
export function getCheck(inputCheck: string): CheckDefinition {
59-
if (!(inputCheck in checkDefinitionsByName)) {
78+
if (!isCheckName(inputCheck)) {
6079
throw new Error(`Unknown check ${inputCheck}.`)
6180
}
6281

63-
return checkDefinitionsByName[inputCheck as CheckName]
82+
return checkDefinitionsByName[inputCheck]
6483
}

0 commit comments

Comments
 (0)