Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(webpack-loader): support json catalogs. Improve coverage #1525

Merged
Merged
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
10 changes: 6 additions & 4 deletions packages/cli/src/api/__snapshots__/compile.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"`;

Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/api/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/api/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,6 +40,10 @@ export function createCompiledCatalog(
return obj
}, {})

if (namespace === "json") {
return JSON.stringify({ messages: compiledMessages })
}

const ast = buildExportStatement(
//build JSON.parse(<compiledMessages>) statement
t.callExpression(
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
3 changes: 1 addition & 2 deletions packages/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
65 changes: 13 additions & 52 deletions packages/loader/src/webpackLoader.ts
Original file line number Diff line number Diff line change
@@ -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<LinguiLoaderOptions> = 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)
Expand All @@ -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
14 changes: 13 additions & 1 deletion packages/loader/test/__snapshots__/loader.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 ,
Expand Down
16 changes: 5 additions & 11 deletions packages/loader/test/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/loader/test/json-format/.linguirc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"locales": ["en"],
"catalogs": [{
"path": "<rootDir>/locale/{locale}"
}],
"format": "minimal"
}
3 changes: 3 additions & 0 deletions packages/loader/test/json-format/entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function load() {
return (await import("@lingui/loader!./locale/en.json")).default
}
4 changes: 4 additions & 0 deletions packages/loader/test/json-format/locale/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key": "Message",
"key2": "Hello {name}"
}
18 changes: 16 additions & 2 deletions packages/loader/test/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
Expand Down
5 changes: 0 additions & 5 deletions packages/loader/test/locale/es/messages.po

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"locales": ["en"],
"catalogs": [{
"path": "<rootDir>/locale/{locale}/messages"
"path": "<rootDir>/locale/{locale}"
}],
"format": "po"
}
3 changes: 3 additions & 0 deletions packages/loader/test/po-format/entrypoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function load() {
return import("@lingui/loader?option=foo!./locale/en.po")
}
18 changes: 12 additions & 6 deletions website/docs/ref/loader.md
Original file line number Diff line number Diff line change
@@ -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:
The Webpack loader compiles catalogs on the fly. In summary, the `lingui compile` command isn't needed when using this loader.

## Installation

Expand All @@ -16,14 +14,22 @@ 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`.
The extension is mandatory.

``` jsx
```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 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.json`)).default
```
:::

See the [guide about dynamic loading catalogs](/docs/guides/dynamic-loading-catalogs.md) for more info.
28 changes: 1 addition & 27 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down