diff --git a/package.json b/package.json index 334ffc22d..87748a352 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@testing-library/react": "^11.0.4", "@types/babel-types": "^7.0.9", "@types/jest": "^26.0.14", + "@types/mock-fs": "^4.13.1", "@types/node": "14.14.31", "@types/ramda": "^0.27.23", "@types/react": "^16.9.51", @@ -72,7 +73,7 @@ "lerna": "^3.22.1", "memory-fs": "^0.5.0", "minimist": "^1.2.5", - "mock-fs": "^4.13.0", + "mock-fs": "^5.2.0", "mockdate": "^3.0.2", "ncp": "^2.0.0", "npm-cli-login": "^0.1.1", diff --git a/packages/cli/src/api/catalog.ts b/packages/cli/src/api/catalog.ts index fa4f98366..b3b4ff587 100644 --- a/packages/cli/src/api/catalog.ts +++ b/packages/cli/src/api/catalog.ts @@ -280,10 +280,6 @@ export class Catalog { ) { const catalog = catalogs[locale] || {} - if (!catalog.hasOwnProperty(key)) { - console.error(`Message with key ${key} is missing in locale ${locale}`) - } - const getTranslation = (_locale: string) => { const configLocales = this.config.locales.join('", "') const localeCatalog = catalogs[_locale] || {} @@ -299,7 +295,6 @@ export class Catalog { return null } if (!localeCatalog.hasOwnProperty(key)) { - console.error(`Message with key ${key} is missing in locale ${_locale}`) return null } diff --git a/packages/cli/src/lingui-compile.ts b/packages/cli/src/lingui-compile.ts index 416ddca6d..8833bcf77 100644 --- a/packages/cli/src/lingui-compile.ts +++ b/packages/cli/src/lingui-compile.ts @@ -18,9 +18,18 @@ const noMessages: (catalogs: Object[]) => boolean = R.pipe( R.all(R.equals(true)) ) -function command(config: LinguiConfig, options) { +export type CliCompileOptions = { + verbose?: boolean + allowEmpty?: boolean, + typescript?: boolean, + watch?: boolean + namespace?: string, +} + +export function command(config: LinguiConfig, options: CliCompileOptions) { const catalogs = getCatalogs(config) + // fixme: this is definitely doesn't work if (noMessages(catalogs)) { console.error("Nothing to compile, message catalogs are empty!\n") console.error( @@ -35,12 +44,13 @@ function command(config: LinguiConfig, options) { const doMerge = !!config.catalogsMergePath let mergedCatalogs = {} - console.error("Compiling message catalogs…") + console.log("Compiling message catalogs…") - config.locales.forEach((locale) => { + for (const locale of config.locales) { const [language] = locale.split(/[_-]/) + // todo: this validation should be in @lingui/conf if (locale !== config.pseudoLocale && !plurals[language]) { - console.log( + console.error( chalk.red( `Error: Invalid locale ${chalk.bold(locale)} (missing plural rules)!` ) @@ -48,10 +58,10 @@ function command(config: LinguiConfig, options) { console.error() } - catalogs.forEach((catalog) => { + for (const catalog of catalogs) { const messages = catalog.getTranslations(locale, { fallbackLocales: config.fallbackLocales, - sourceLocale: config.sourceLocale, + sourceLocale: config.sourceLocale }) if (!options.allowEmpty) { @@ -68,14 +78,14 @@ function command(config: LinguiConfig, options) { if (options.verbose) { console.error(chalk.red("Missing translations:")) - missingMsgIds.forEach((msgId) => console.log(msgId)) + missingMsgIds.forEach((msgId) => console.error(msgId)) } else { console.error( chalk.red(`Missing ${missingMsgIds.length} translation(s)`) ) } console.error() - process.exit(1) + return false } } @@ -112,7 +122,7 @@ function command(config: LinguiConfig, options) { options.verbose && console.error(chalk.green(`${locale} ⇒ ${compiledPath}`)) } - }) + } if (doMerge) { const compileCatalog = getCatalogForMerge(config) @@ -130,7 +140,7 @@ function command(config: LinguiConfig, options) { ) options.verbose && console.log(chalk.green(`${locale} ⇒ ${compiledPath}`)) } - }) + } return true } diff --git a/packages/cli/src/test/__snapshots__/compile.test.ts.snap b/packages/cli/src/test/__snapshots__/compile.test.ts.snap new file mode 100644 index 000000000..e25245b27 --- /dev/null +++ b/packages/cli/src/test/__snapshots__/compile.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CLI Command: Compile Locales Validation Should throw error for invalid locale 1`] = ` +Error: Invalid locale abra (missing plural rules)! + +`; + +exports[`CLI Command: Compile allowEmpty = false Should show error and stop compilation of catalog if message doesnt have a translation (with template) 1`] = ` +Object { + en: /*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello World\\":\\"Hello World\\"}")};, + pl: undefined, +} +`; + +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 pl! +Missing 1 translation(s) + +`; + +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) + +`; + +exports[`CLI Command: Compile allowEmpty = false Should show missing messages verbosely when verbose = true 1`] = ` +en ⇒ /test/en.js +Error: Failed to compile catalog for locale pl! +Missing translations: +Hello World +Test String + +`; diff --git a/packages/cli/src/test/compile.test.ts b/packages/cli/src/test/compile.test.ts new file mode 100644 index 000000000..c1a94b35c --- /dev/null +++ b/packages/cli/src/test/compile.test.ts @@ -0,0 +1,203 @@ +import {command} from "../lingui-compile" +import {makeConfig} from "@lingui/conf" +import path from "path" +import {getConsoleMockCalls, mockConsole} from "@lingui/jest-mocks" +import mockFs from "mock-fs" +import fs from 'fs' + +function readFsToJson(directory: string, filter?: (filename: string) => boolean) { + const out = {} + + fs + .readdirSync(directory) + .map((filename) => { + const filepath = path.join(directory, filename) + + if (fs.lstatSync(filepath).isDirectory()) { + out[filename] = readFsToJson(filepath) + return out + } + + if (!filter || filter(filename)) { + out[filename] = fs.readFileSync(filepath).toString() + } + }) + + return out +} + +describe('CLI Command: Compile', () => { + xit('Should return error if no messages', () => { + const config = makeConfig({ + locales: ['en', 'pl'], + rootDir: path.join(__dirname, 'fixtures/compile'), + catalogs: [{ + path: '/{locale}', + include: [''] + }] + }) + + const result = command(config, {}) + expect(result).toBeFalsy() + }) + + describe('Merge Catalogs', () => { + // todo + }) + + describe('Locales Validation', () => { + // todo: should be moved to @lingui/conf + it('Should throw error for invalid locale', () => { + const config = makeConfig({ + locales: ['abra'], + rootDir: '/test', + catalogs: [{ + path: '/{locale}', + include: [''], + exclude: [] + }] + }) + + mockFs() + + mockConsole((console) => { + const result = command(config, {}) + mockFs.restore() + const log = getConsoleMockCalls(console.error) + expect(log).toMatchSnapshot() + + expect(result).toBeTruthy() + }) + }) + + it('Should not throw error for pseudolocale', () => { + const config = makeConfig({ + locales: ['abracadabra'], + rootDir: '/test', + pseudoLocale: 'abracadabra', + catalogs: [{ + path: '/{locale}', + include: [''], + exclude: [] + }] + }) + + mockFs() + + mockConsole((console) => { + const result = command(config, {}) + mockFs.restore() + expect(console.error).not.toBeCalled() + expect(result).toBeTruthy() + }) + }) + }) + + describe('allowEmpty = false', () => { + const config = makeConfig({ + locales: ['en', 'pl'], + sourceLocale: 'en', + rootDir: '/test', + catalogs: [{ + path: '/{locale}', + include: [''], + exclude: [] + }] + }) + + it('Should show error and stop compilation of catalog ' + + 'if message doesnt have a translation (no template)', () => { + mockFs({ + '/test': { + 'en.po': ` +msgid "Hello World" +msgstr "Hello World" + `, + 'pl.po': ` +msgid "Hello World" +msgstr "Cześć świat" + +msgid "Test String" +msgstr "" + ` + } + }) + + mockConsole((console) => { + const result = command(config, { + allowEmpty: false + }) + const actualFiles = readFsToJson('/test') + + expect(actualFiles['pl.js']).toBeFalsy() + expect(actualFiles['en.js']).toBeTruthy() + mockFs.restore() + + const log = getConsoleMockCalls(console.error) + expect(log).toMatchSnapshot() + expect(result).toBeFalsy() + }) + }) + + it('Should show error and stop compilation of catalog ' + + ' if message doesnt have a translation (with template)', () => { + mockFs({ + '/test': { + 'messages.pot': ` +msgid "Hello World" +msgstr "" + `, + 'pl.po': `` + } + }) + + mockConsole((console) => { + const result = command(config, { + allowEmpty: false + }) + + const actualFiles = readFsToJson('/test') + + expect({ + pl: actualFiles['pl.js'], + en: actualFiles['en.js'] + }).toMatchSnapshot() + + mockFs.restore() + + const log = getConsoleMockCalls(console.error) + expect(log).toMatchSnapshot() + expect(result).toBeFalsy() + }) + }) + + + it('Should show missing messages verbosely when verbose = true', () => { + mockFs({ + '/test': { + 'pl.po': ` +msgid "Hello World" +msgstr "" + +msgid "Test String" +msgstr "" + ` + } + }) + + mockConsole((console) => { + const result = command(config, { + allowEmpty: false, + verbose: true + }) + + mockFs.restore() + + const log = getConsoleMockCalls(console.error) + expect(log).toMatchSnapshot() + expect(result).toBeFalsy() + }) + }) + + }) +}) diff --git a/packages/conf/index.d.ts b/packages/conf/index.d.ts index 2d2c07084..9f4e8a829 100644 --- a/packages/conf/index.d.ts +++ b/packages/conf/index.d.ts @@ -62,6 +62,11 @@ export declare function getConfig({ cwd, configPath, skipValidation, }?: { configPath?: string; skipValidation?: boolean; }): LinguiConfig; + +export declare function makeConfig(userConfig: Partial, opts?: { + skipValidation?: boolean +}): LinguiConfig; + export declare const configValidation: { exampleConfig: { extractBabelOptions: { diff --git a/packages/conf/src/index.ts b/packages/conf/src/index.ts index 9384eeff2..fca64ab83 100644 --- a/packages/conf/src/index.ts +++ b/packages/conf/src/index.ts @@ -50,6 +50,8 @@ export type LinguiConfig = { compilerBabelOptions: GeneratorOptions fallbackLocales?: FallbackLocales extractors?: ExtractorType[] | string[] + prevFormat?: CatalogFormat; + localeDir?: string; format: CatalogFormat formatOptions: CatalogFormatOptions locales: string[] @@ -136,13 +138,22 @@ export function getConfig({ ? configExplorer.load(configPath) : configExplorer.search(defaultRootDir) const userConfig = result ? result.config : {} + + return makeConfig({ + rootDir: result ? path.dirname(result.filepath) : defaultRootDir, + ...userConfig, + }, {skipValidation}) +} + +export function makeConfig(userConfig: Partial, opts: { + skipValidation?: boolean +} = {}): LinguiConfig { const config: LinguiConfig = { ...defaultConfig, - rootDir: result ? path.dirname(result.filepath) : defaultRootDir, ...userConfig, } - if (!skipValidation) { + if (!opts.skipValidation) { validate(config, configValidation) return pipe( diff --git a/packages/jest-mocks/index.ts b/packages/jest-mocks/index.ts index 895fe89d8..a4950858b 100644 --- a/packages/jest-mocks/index.ts +++ b/packages/jest-mocks/index.ts @@ -7,7 +7,7 @@ export function mockConfig(config: Partial = {}) { } } -export function getConsoleMockCalls({ mock }) { +export function getConsoleMockCalls({ mock }: jest.MockInstance) { if (!mock.calls.length) return return mock.calls.map((call) => call[0]).join("\n") } diff --git a/yarn.lock b/yarn.lock index 997c9b0e7..ac8608e41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2717,6 +2717,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/mock-fs@^4.13.1": + version "4.13.1" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be" + integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>= 8": version "18.11.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" @@ -8387,10 +8394,10 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5: dependencies: minimist "^1.2.6" -mock-fs@^4.13.0: - version "4.14.0" - resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" - integrity sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw== +mock-fs@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.2.0.tgz#3502a9499c84c0a1218ee4bf92ae5bf2ea9b2b5e" + integrity sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw== mockdate@^3.0.2: version "3.0.5"