From 7e8259d8d9e49214d2cb4b6134c7b890fd8f81fd Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Thu, 16 Mar 2023 13:53:59 +0100 Subject: [PATCH 1/2] fix(webpack-loader): support json catalogs. Improve coverage --- .../api/__snapshots__/compile.test.ts.snap | 10 +-- packages/cli/src/api/compile.test.ts | 8 ++- packages/cli/src/api/compile.ts | 6 +- packages/cli/src/api/utils.ts | 1 + packages/loader/package.json | 3 +- packages/loader/src/webpackLoader.ts | 65 ++++--------------- .../test/__snapshots__/loader.test.ts.snap | 14 +++- packages/loader/test/compiler.ts | 16 ++--- packages/loader/test/json-format/.linguirc | 7 ++ .../loader/test/json-format/entrypoint.js | 3 + .../loader/test/json-format/locale/en.json | 4 ++ packages/loader/test/loader.test.ts | 18 ++++- packages/loader/test/locale/es/messages.po | 5 -- .../loader/test/{ => po-format}/.linguirc | 2 +- packages/loader/test/po-format/entrypoint.js | 3 + .../en/messages.po => po-format/locale/en.po} | 0 website/docs/ref/loader.md | 19 ++++-- yarn.lock | 28 +------- 18 files changed, 98 insertions(+), 114 deletions(-) create mode 100644 packages/loader/test/json-format/.linguirc create mode 100644 packages/loader/test/json-format/entrypoint.js create mode 100644 packages/loader/test/json-format/locale/en.json delete mode 100644 packages/loader/test/locale/es/messages.po rename packages/loader/test/{ => po-format}/.linguirc (57%) create mode 100644 packages/loader/test/po-format/entrypoint.js rename packages/loader/test/{locale/en/messages.po => po-format/locale/en.po} (100%) diff --git a/packages/cli/src/api/__snapshots__/compile.test.ts.snap b/packages/cli/src/api/__snapshots__/compile.test.ts.snap index a43a4c6d1..a3dce0f11 100644 --- a/packages/cli/src/api/__snapshots__/compile.test.ts.snap +++ b/packages/cli/src/api/__snapshots__/compile.test.ts.snap @@ -4,13 +4,15 @@ exports[`createCompiledCatalog options.compilerBabelOptions by default should re exports[`createCompiledCatalog options.compilerBabelOptions should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:JSON.parse("{\\"Hello\\":\\"Aloh\\xE0\\"}")};`; -exports[`createCompiledCatalog options.namespace should compile with es 1`] = `/*eslint-disable*/export const messages=JSON.parse("{}");`; +exports[`createCompiledCatalog options.namespace should compile with es 1`] = `/*eslint-disable*/export const messages=JSON.parse("{\\"key\\":[\\"Hello \\",[\\"name\\"]]}");`; -exports[`createCompiledCatalog options.namespace should compile with global 1`] = `/*eslint-disable*/global.test={messages:JSON.parse("{}")};`; +exports[`createCompiledCatalog options.namespace should compile with global 1`] = `/*eslint-disable*/global.test={messages:JSON.parse("{\\"key\\":[\\"Hello \\",[\\"name\\"]]}")};`; -exports[`createCompiledCatalog options.namespace should compile with ts 1`] = `/*eslint-disable*/export const messages=JSON.parse("{}");`; +exports[`createCompiledCatalog options.namespace should compile with json 1`] = `{"messages":{"key":["Hello ",["name"]]}}`; -exports[`createCompiledCatalog options.namespace should compile with window 1`] = `/*eslint-disable*/window.test={messages:JSON.parse("{}")};`; +exports[`createCompiledCatalog options.namespace should compile with ts 1`] = `/*eslint-disable*/export const messages=JSON.parse("{\\"key\\":[\\"Hello \\",[\\"name\\"]]}");`; + +exports[`createCompiledCatalog options.namespace should compile with window 1`] = `/*eslint-disable*/window.test={messages:JSON.parse("{\\"key\\":[\\"Hello \\",[\\"name\\"]]}")};`; exports[`createCompiledCatalog options.namespace should error with invalid value 1`] = `Invalid namespace param: "global"`; diff --git a/packages/cli/src/api/compile.test.ts b/packages/cli/src/api/compile.test.ts index 13c5e83ba..205409a1d 100644 --- a/packages/cli/src/api/compile.test.ts +++ b/packages/cli/src/api/compile.test.ts @@ -184,12 +184,18 @@ describe("createCompiledCatalog", () => { const getCompiledCatalog = (namespace: CompiledCatalogNamespace) => createCompiledCatalog( "fr", - {}, + { + key: "Hello {name}", + }, { namespace, } ) + it("should compile with json", () => { + expect(getCompiledCatalog("json")).toMatchSnapshot() + }) + it("should compile with es", () => { expect(getCompiledCatalog("es")).toMatchSnapshot() }) diff --git a/packages/cli/src/api/compile.ts b/packages/cli/src/api/compile.ts index 3dacf49f3..e73e45e8b 100644 --- a/packages/cli/src/api/compile.ts +++ b/packages/cli/src/api/compile.ts @@ -4,7 +4,7 @@ import { compileMessage } from "@lingui/core/compile" import pseudoLocalize from "./pseudoLocalize" import { CompiledMessage } from "@lingui/core/src/i18n" -export type CompiledCatalogNamespace = "cjs" | "es" | "ts" | string +export type CompiledCatalogNamespace = "cjs" | "es" | "ts" | "json" | string type CompiledCatalogType = { [msgId: string]: string @@ -40,6 +40,10 @@ export function createCompiledCatalog( return obj }, {}) + if (namespace === "json") { + return JSON.stringify({ messages: compiledMessages }) + } + const ast = buildExportStatement( //build JSON.parse() statement t.callExpression( diff --git a/packages/cli/src/api/utils.ts b/packages/cli/src/api/utils.ts index 8c258f346..02b86f4d4 100644 --- a/packages/cli/src/api/utils.ts +++ b/packages/cli/src/api/utils.ts @@ -116,6 +116,7 @@ export function normalizeRelativePath(sourcePath: string): string { return normalize(sourcePath, false) } + // https://github.com/lingui/js-lingui/issues/809 const isDir = isDirectory(sourcePath) return ( diff --git a/packages/loader/package.json b/packages/loader/package.json index 9709db322..2d5d32c41 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -37,8 +37,7 @@ "dependencies": { "@babel/runtime": "^7.20.13", "@lingui/cli": "4.0.0-next.1", - "@lingui/conf": "4.0.0-next.1", - "loader-utils": "^2.0.0" + "@lingui/conf": "4.0.0-next.1" }, "devDependencies": { "webpack": "^5.76.1" diff --git a/packages/loader/src/webpackLoader.ts b/packages/loader/src/webpackLoader.ts index 6f5f51977..81fcc1a34 100644 --- a/packages/loader/src/webpackLoader.ts +++ b/packages/loader/src/webpackLoader.ts @@ -1,68 +1,28 @@ import path from "path" -import { CatalogFormat, getConfig } from "@lingui/conf" +import { getConfig } from "@lingui/conf" import { createCompiledCatalog, getCatalogs, getCatalogForFile, } from "@lingui/cli/api" -import loaderUtils from "loader-utils" -// Check if webpack 5 -const isWebpack5 = parseInt(require("webpack").version) === 5 +import { LoaderDefinitionFunction } from "webpack" -// Check if JavascriptParser and JavascriptGenerator exists -> Webpack 4 -let JavascriptParser -let JavascriptGenerator -try { - JavascriptParser = require("webpack/lib/Parser") - JavascriptGenerator = require("webpack/lib/JavascriptGenerator") -} catch (error) { - if (error.code !== "MODULE_NOT_FOUND") { - throw error - } +type LinguiLoaderOptions = { + config?: string } -const requiredType = "javascript/auto" - -export default async function (source) { - const callback = this.async() - - const options = loaderUtils.getOptions(this) || {} - - if (!isWebpack5 && JavascriptParser && JavascriptGenerator) { - // Webpack 4 uses json-loader automatically, which breaks this loader because it - // doesn't return JSON, but JS module. This is a temporary workaround before - // official API is added (https://github.com/webpack/webpack/issues/7057#issuecomment-381883220) - // See https://github.com/webpack/webpack/issues/7057 - this._module.type = requiredType - this._module.parser = new JavascriptParser() - this._module.generator = new JavascriptGenerator() - } +const loader: LoaderDefinitionFunction = async function ( + source +) { + const options = this.getOptions() || {} const config = getConfig({ configPath: options.config, cwd: path.dirname(this.resourcePath), }) - const EMPTY_EXT = /\.[0-9a-z]+$/.test(this.resourcePath) - const JS_EXT = /\.js+$/.test(this.resourcePath) - const catalogRelativePath = path.relative(config.rootDir, this.resourcePath) - if (!EMPTY_EXT || JS_EXT) { - const formats = { - minimal: ".json", - po: ".po", - lingui: ".json", - } - // we replace the .js, because webpack appends automatically the .js on imports without extension - throw new Error( - `File extension is mandatory, for ex: import("@lingui/loader!./${catalogRelativePath.replace( - ".js", - formats[config.format as CatalogFormat] - )}")` - ) - } - const { locale, catalog } = getCatalogForFile( catalogRelativePath, getCatalogs(config) @@ -79,11 +39,12 @@ export default async function (source) { // of I18nProvider (React) or setupI18n (core) and therefore we need to get // empty translations if missing. const strict = process.env.NODE_ENV !== "production" - const compiled = createCompiledCatalog(locale, messages, { + + return createCompiledCatalog(locale, messages, { strict, - namespace: config.compileNamespace, + namespace: this._module.type === "json" ? "json" : "es", pseudoLocale: config.pseudoLocale, }) - - callback(null, compiled) } + +export default loader diff --git a/packages/loader/test/__snapshots__/loader.test.ts.snap b/packages/loader/test/__snapshots__/loader.test.ts.snap index 632c4169a..f73822ee8 100644 --- a/packages/loader/test/__snapshots__/loader.test.ts.snap +++ b/packages/loader/test/__snapshots__/loader.test.ts.snap @@ -1,6 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`lingui-loader should compile catalog 1`] = ` +exports[`lingui-loader should compile catalog in json format 1`] = ` +{ + key: Message, + key2: [ + Hello , + [ + name, + ], + ], +} +`; + +exports[`lingui-loader should compile catalog in po format 1`] = ` { mVmaLu: [ My name is , diff --git a/packages/loader/test/compiler.ts b/packages/loader/test/compiler.ts index c380b627c..58b0b1608 100644 --- a/packages/loader/test/compiler.ts +++ b/packages/loader/test/compiler.ts @@ -11,22 +11,16 @@ export default async ( mode: "development", target: "node", entry: entryPoint, + resolveLoader: { + alias: { + "@lingui/loader": path.resolve(__dirname, "../src/webpackLoader.ts"), + }, + }, output: { path: mkdtempSync(path.join(os.tmpdir(), `lingui-test-${process.pid}`)), filename: "bundle.js", libraryTarget: "commonjs", }, - module: { - rules: [ - { - test: /\.po$/, - use: { - loader: path.resolve(__dirname, "../src/webpackLoader.ts"), - options, - }, - }, - ], - }, }) return new Promise((resolve, reject) => { diff --git a/packages/loader/test/json-format/.linguirc b/packages/loader/test/json-format/.linguirc new file mode 100644 index 000000000..ecd054188 --- /dev/null +++ b/packages/loader/test/json-format/.linguirc @@ -0,0 +1,7 @@ +{ + "locales": ["en"], + "catalogs": [{ + "path": "/locale/{locale}" + }], + "format": "minimal" +} diff --git a/packages/loader/test/json-format/entrypoint.js b/packages/loader/test/json-format/entrypoint.js new file mode 100644 index 000000000..403379e79 --- /dev/null +++ b/packages/loader/test/json-format/entrypoint.js @@ -0,0 +1,3 @@ +export async function load() { + return (await import("@lingui/loader!./locale/en.json")).default +} diff --git a/packages/loader/test/json-format/locale/en.json b/packages/loader/test/json-format/locale/en.json new file mode 100644 index 000000000..c82897831 --- /dev/null +++ b/packages/loader/test/json-format/locale/en.json @@ -0,0 +1,4 @@ +{ + "key": "Message", + "key2": "Hello {name}" +} diff --git a/packages/loader/test/loader.test.ts b/packages/loader/test/loader.test.ts index 5ef701693..52aee3039 100644 --- a/packages/loader/test/loader.test.ts +++ b/packages/loader/test/loader.test.ts @@ -2,10 +2,24 @@ import path from "path" import compiler from "./compiler" describe("lingui-loader", () => { - it("should compile catalog", async () => { + it("should compile catalog in po format", async () => { expect.assertions(2) - const stats = await compiler(path.join(__dirname, "entrypoint.js")) + const stats = await compiler( + path.join(__dirname, "po-format/entrypoint.js") + ) + + const data = await import(path.join(stats.outputPath, "bundle.js")) + expect(stats.errors).toEqual([]) + expect((await data.load()).messages).toMatchSnapshot() + }) + + it("should compile catalog in json format", async () => { + expect.assertions(2) + + const stats = await compiler( + path.join(__dirname, "./json-format/entrypoint.js") + ) const data = await import(path.join(stats.outputPath, "bundle.js")) expect(stats.errors).toEqual([]) diff --git a/packages/loader/test/locale/es/messages.po b/packages/loader/test/locale/es/messages.po deleted file mode 100644 index 6647c1086..000000000 --- a/packages/loader/test/locale/es/messages.po +++ /dev/null @@ -1,5 +0,0 @@ -msgid "Hello World" -msgstr "" - -msgid "My name is {name}" -msgstr "My name is {name}" diff --git a/packages/loader/test/.linguirc b/packages/loader/test/po-format/.linguirc similarity index 57% rename from packages/loader/test/.linguirc rename to packages/loader/test/po-format/.linguirc index 01d77326d..ddd83ead8 100644 --- a/packages/loader/test/.linguirc +++ b/packages/loader/test/po-format/.linguirc @@ -1,7 +1,7 @@ { "locales": ["en"], "catalogs": [{ - "path": "/locale/{locale}/messages" + "path": "/locale/{locale}" }], "format": "po" } diff --git a/packages/loader/test/po-format/entrypoint.js b/packages/loader/test/po-format/entrypoint.js new file mode 100644 index 000000000..618f93210 --- /dev/null +++ b/packages/loader/test/po-format/entrypoint.js @@ -0,0 +1,3 @@ +export async function load() { + return import("@lingui/loader?option=foo!./locale/en.po") +} diff --git a/packages/loader/test/locale/en/messages.po b/packages/loader/test/po-format/locale/en.po similarity index 100% rename from packages/loader/test/locale/en/messages.po rename to packages/loader/test/po-format/locale/en.po diff --git a/website/docs/ref/loader.md b/website/docs/ref/loader.md index bc5788d1e..37a4db556 100644 --- a/website/docs/ref/loader.md +++ b/website/docs/ref/loader.md @@ -1,8 +1,6 @@ # Webpack Loader -It's a good practice to use compiled message catalogs during development. However, running [`compile`](/docs/ref/cli.md#compile) everytime messages are changed soon becomes tedious. - -`@lingui/loader` is a webpack loader, which compiles messages on the fly: +Webpack loader which compiles catalogs on the fly. In summary, `lingui compile` command isn't required when using this loader ## Installation @@ -16,14 +14,21 @@ npm install --save-dev @lingui/loader Simply prepend `@lingui/loader!` in front of path to message catalog you want to import. Here's an example of dynamic import: -Extension is mandatory. If you use minimal or lingui file format, use `.json`. In case of using po format, use `.po`. - -``` jsx +Extension is mandatory. +```ts export async function dynamicActivate(locale: string) { - const { messages } = await import(`@lingui/loader!./locales/${locale}/messages.json`) + const { messages } = await import(`@lingui/loader!./locales/${locale}/messages.po`) i18n.load(locale, messages) i18n.activate(locale) } ``` +:::note +Catalogs with `.json` extension treated differently by Webpack. They loaded as ES module with default export, so your import should look like that: + +```ts +const { messages } = (await import(`@lingui/loader!./locales/${locale}/messages.po`)).default +``` +::: + See the [guide about dynamic loading catalogs](/docs/guides/dynamic-loading-catalogs.md) for more info. diff --git a/yarn.lock b/yarn.lock index 322e4dd1f..4fb05ebe4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2434,7 +2434,6 @@ __metadata: "@babel/runtime": ^7.20.13 "@lingui/cli": 4.0.0-next.1 "@lingui/conf": 4.0.0-next.1 - loader-utils: ^2.0.0 webpack: ^5.76.1 peerDependencies: webpack: ^5.0.0 @@ -4766,13 +4765,6 @@ __metadata: languageName: node linkType: hard -"big.js@npm:^5.2.2": - version: 5.2.2 - resolution: "big.js@npm:5.2.2" - checksum: b89b6e8419b097a8fb4ed2399a1931a68c612bce3cfd5ca8c214b2d017531191070f990598de2fc6f3f993d91c0f08aa82697717f6b3b8732c9731866d233c9e - languageName: node - linkType: hard - "bin-links@npm:^3.0.0": version: 3.0.3 resolution: "bin-links@npm:3.0.3" @@ -6094,13 +6086,6 @@ __metadata: languageName: node linkType: hard -"emojis-list@npm:^3.0.0": - version: 3.0.0 - resolution: "emojis-list@npm:3.0.0" - checksum: ddaaa02542e1e9436c03970eeed445f4ed29a5337dfba0fe0c38dfdd2af5da2429c2a0821304e8a8d1cadf27fdd5b22ff793571fa803ae16852a6975c65e8e70 - languageName: node - linkType: hard - "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -9521,7 +9506,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.1.2, json5@npm:^2.2.2, json5@npm:^2.2.3": +"json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -9853,17 +9838,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^2.0.0": - version: 2.0.4 - resolution: "loader-utils@npm:2.0.4" - dependencies: - big.js: ^5.2.2 - emojis-list: ^3.0.0 - json5: ^2.1.2 - checksum: a5281f5fff1eaa310ad5e1164095689443630f3411e927f95031ab4fb83b4a98f388185bb1fe949e8ab8d4247004336a625e9255c22122b815bb9a4c5d8fc3b7 - languageName: node - linkType: hard - "locate-path@npm:^2.0.0": version: 2.0.0 resolution: "locate-path@npm:2.0.0" From 8658981c3eeeb8ffb5f025c496bbdf22de81bd34 Mon Sep 17 00:00:00 2001 From: Timofei Iatsenko Date: Fri, 17 Mar 2023 10:54:18 +0100 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Andrii Bodnar --- website/docs/ref/loader.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/ref/loader.md b/website/docs/ref/loader.md index 37a4db556..7e2444d4b 100644 --- a/website/docs/ref/loader.md +++ b/website/docs/ref/loader.md @@ -1,6 +1,6 @@ # Webpack Loader -Webpack loader which compiles catalogs on the fly. In summary, `lingui compile` command isn't required when using this loader +The Webpack loader compiles catalogs on the fly. In summary, the `lingui compile` command isn't needed when using this loader. ## Installation @@ -14,7 +14,8 @@ npm install --save-dev @lingui/loader Simply prepend `@lingui/loader!` in front of path to message catalog you want to import. Here's an example of dynamic import: -Extension is mandatory. +The extension is mandatory. + ```ts export async function dynamicActivate(locale: string) { const { messages } = await import(`@lingui/loader!./locales/${locale}/messages.po`) @@ -24,10 +25,10 @@ export async function dynamicActivate(locale: string) { ``` :::note -Catalogs with `.json` extension treated differently by Webpack. They loaded as ES module with default export, so your import should look like that: +Catalogs with the `.json` extension are treated differently by Webpack. They load as ES module with default export, so your import should look like this: ```ts -const { messages } = (await import(`@lingui/loader!./locales/${locale}/messages.po`)).default +const { messages } = (await import(`@lingui/loader!./locales/${locale}/messages.json`)).default ``` :::