Skip to content

Commit

Permalink
feat: add metro transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
vonovak committed Aug 12, 2024
1 parent 088efe5 commit 7365339
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 31 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ module.exports = {
"<rootDir>/packages/cli",
"<rootDir>/packages/conf",
"<rootDir>/packages/loader",
"<rootDir>/packages/metro-transformer",
"<rootDir>/packages/macro",
"<rootDir>/packages/vite-plugin",
"<rootDir>/packages/format-po",
Expand Down
2 changes: 1 addition & 1 deletion packages/conf/src/getConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function getConfig({
)

// gracefully stop further executing
throw new Error("No Config")
throw new Error("No Lingui config found")
}

const userConfig = result ? result.config : {}
Expand Down
8 changes: 8 additions & 0 deletions packages/metro-transformer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

### Features

- use fallback locales from cldr ([#820](https://github.com/lingui/js-lingui/issues/820)) ([2d9e124](https://github.com/lingui/js-lingui/commit/2d9e124b91f1ba7a65e9f997a3ba952679c6c23a))
34 changes: 34 additions & 0 deletions packages/metro-transformer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[![License][badge-license]][license]
[![Version][badge-version]][package]
[![Downloads][badge-downloads]][package]

# @lingui/metro-transformer

> Metro bundler transformer for LinguiJS catalogs
`@lingui/metro-transformer` is part of [LinguiJS][linguijs]. See the
[documentation][documentation] for all information, tutorials and examples.

## Installation

```sh
npm install --save-dev @lingui/metro-transformer
# yarn add --dev @lingui/metro-transformer
```

## Usage

See the [reference][reference] documentation.

## License

This package is licensed under [MIT][license] license.

[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
[linguijs]: https://github.com/lingui/js-lingui
[documentation]: https://lingui.dev
[reference]: https://lingui.dev/ref/metro-transformer
[package]: https://www.npmjs.com/package/@lingui/metro-transformer
[badge-downloads]: https://img.shields.io/npm/dw/@lingui/metro-transformer.svg
[badge-version]: https://img.shields.io/npm/v/@lingui/metro-transformer.svg
[badge-license]: https://img.shields.io/npm/l/@lingui/metro-transformer.svg
67 changes: 67 additions & 0 deletions packages/metro-transformer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@lingui/metro-transformer",
"version": "0.1.0",
"description": "Metro bundler transformer for LinguiJS catalogs",
"exports": {
"./expo": {
"require": "./dist/expo/index.cjs",
"import": "./dist/expo/index.mjs",
"types": "./dist/expo/index.d.ts"
},
"./react-native": {
"require": "./dist/react-native/index.cjs",
"import": "./dist/react-native/index.mjs",
"types": "./dist/react-native/index.d.ts"
}
},
"sideEffects": false,
"author": {
"name": "Vojtech Novak",
"email": "[email protected]"
},
"license": "MIT",
"keywords": [
"metro",
"react native",
"metro bundler",
"i18n",
"internationalization",
"i10n",
"localization",
"i9n",
"translation",
"multilingual"
],
"scripts": {
"build": "rimraf ./dist && unbuild",
"stub": "unbuild --stub"
},
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"LICENSE",
"README.md",
"dist/"
],
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui/cli": "4.11.2",
"@lingui/conf": "4.11.2"
},
"devDependencies": {
"@lingui/format-json": "4.11.2",
"rimraf": "^6.0.1",
"unbuild": "2.0.0"
},
"peerDependencies": {
"metro": "*"
}
}
5 changes: 5 additions & 0 deletions packages/metro-transformer/src/expo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createLinguiMetroTransformer } from "../metroTransformer"

const expoTransformer = require("@expo/metro-config/babel-transformer")

export const transform = createLinguiMetroTransformer(expoTransformer)
64 changes: 64 additions & 0 deletions packages/metro-transformer/src/metroTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getConfig } from "@lingui/conf"
import {
createCompiledCatalog,
getCatalogForFile,
getCatalogs,
} from "@lingui/cli/api"
import type { BabelTransformer, BabelTransformerArgs } from "./types"

export const createLinguiMetroTransformer = (
upstreamTransformer: BabelTransformer
): BabelTransformer["transform"] => {
return async function linguiMetroTransformer(params) {
if (!params.filename.endsWith(".po")) {
return upstreamTransformer.transform(params)
}
const jsSource = await transformFile(params)
return upstreamTransformer.transform({
...params,
src: jsSource,
})
}
}

export async function transformFile(
params: Pick<BabelTransformerArgs, "filename">
) {
const config = getConfig()

const catalogRelativePath = params.filename

const fileCatalog = getCatalogForFile(
catalogRelativePath,
await getCatalogs(config)
)

if (!fileCatalog) {
throw new Error(
`Requested resource ${catalogRelativePath} is not matched to any of your catalogs paths specified in "lingui.config".
Your catalogs:
${config.catalogs.map((c) => c.path).join("\n")}
Working dir is:
${process.cwd()}
Please check that \`catalogs.path\` is filled properly and restart the Metro server.\n`
)
}

const { locale, catalog } = fileCatalog

const messages = await catalog.getTranslations(locale, {
fallbackLocales: config.fallbackLocales,
sourceLocale: config.sourceLocale,
})

const strict = process.env.NODE_ENV !== "production"

return createCompiledCatalog(locale, messages, {
strict,
namespace: "es",
pseudoLocale: config.pseudoLocale,
})
}
11 changes: 11 additions & 0 deletions packages/metro-transformer/src/react-native/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createLinguiMetroTransformer } from "../metroTransformer"

const reactNativeTransformer = (() => {
try {
return require("@react-native/metro-babel-transformer")
} catch (error) {
return require("metro-react-native-babel-transformer")
}
})()

export const transform = createLinguiMetroTransformer(reactNativeTransformer)
16 changes: 16 additions & 0 deletions packages/metro-transformer/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// taken and edited from https://github.com/facebook/metro/blob/main/packages/metro-babel-transformer/types/index.d.ts

export type BabelTransformerArgs = {
readonly filename: string
readonly options: unknown // a more precise type would be BabelTransformerOptions but we don't care about its shape
readonly plugins?: unknown
readonly src: string
}

export type BabelTransformer = {
transform: (args: BabelTransformerArgs) => Promise<{
ast: unknown
metadata: unknown
}>
getCacheKey?: () => string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
locales: ["en", "cs"],
sourceLocale: "en",
catalogs: [
{
path: "locales/{locale}/messages",
include: ["src"],
},
],
fallbackLocales: {
cs: "en",
},
format: "po",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-04-27 00:37+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: cs\n"
"Project-Id-Version: linguidemo\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-04-28 17:32\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
"X-Crowdin-Project: linguidemo\n"
"X-Crowdin-Project-ID: 573605\n"
"X-Crowdin-Language: cs\n"
"X-Crowdin-File: messages.po\n"
"X-Crowdin-File-ID: 25\n"

#: src/MainScreen.tsx:76
msgid "Add a message to your inbox"
msgstr "Přidat zprávu do doručené pošty"

#: src/MainScreen.tsx:23
msgid "Cancel"
msgstr ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2023-04-27 00:37+0200\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"

#: src/MainScreen.tsx:76
msgid "Add a message to your inbox"
msgstr "Add a message to your inbox"

#: src/MainScreen.tsx:23
msgid "Cancel"
msgstr "Cancel"
54 changes: 54 additions & 0 deletions packages/metro-transformer/test/metroTransformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import path from "path"
import { transformFile } from "../src/metroTransformer"

describe("Lingui Metro transformer tests", () => {
const priorEnv = process.env.LINGUI_CONFIG
const priorCwd = process.cwd()
const testDir = path.join(__dirname, "__fixtures__", "test-project")
const catalogDir = path.join(testDir, "locales")

beforeAll(() => {
process.chdir(testDir)
})
afterAll(() => {
process.env.LINGUI_CONFIG = priorEnv
process.chdir(priorCwd)
})

it.each([
[
"English PO file",
"en",
`/*eslint-disable*/export const messages=JSON.parse("{\\"p1AaTM\\":\\"Add a message to your inbox\\",\\"dEgA5A\\":\\"Cancel\\"}");`,
],
[
"Czech PO file with fallback to English",
"cs",
`/*eslint-disable*/export const messages=JSON.parse("{\\"p1AaTM\\":\\"Přidat zprávu do doručené pošty\\",\\"dEgA5A\\":\\"Cancel\\"}");`,
],
])(
"should transform %s to a JS export",
async (_lang, langCode, expectedSnapshot) => {
const filename = path.relative(
testDir,
path.join(catalogDir, langCode, "messages.po")
)
const result = await transformFile({
filename,
})

expect(result).toMatchInlineSnapshot(expectedSnapshot)
}
)

it("should throw error when provided path is not relative to project root", async () => {
// CLI's getCatalogForFile uses path.relative which returns results with respect to cwd.
// Even is path is relative to project root, it will not be considered as matched if cwd is different.
const filename = path.join(catalogDir, "en", "messages.po")
await expect(
transformFile({
filename,
})
).rejects.toThrow(/is not matched to any of your catalogs paths/)
})
})
Loading

0 comments on commit 7365339

Please sign in to comment.