Skip to content

Commit

Permalink
feat: implement @lingui/vite-plugin (#1306)
Browse files Browse the repository at this point in the history
* implement @lingui/vite-plugin
* chore: build node package with tsc to produce typings
  • Loading branch information
timofei-iatsenko authored Jan 30, 2023
1 parent 6f0a48c commit db5d3c3
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 6 deletions.
File renamed without changes.
Empty file.
50 changes: 50 additions & 0 deletions packages/vite-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[![License][badge-license]][license]
[![Version][badge-version]][package]
[![Downloads][badge-downloads]][package]

# @lingui/vite-plugin

> Vite plugin which compiles on the fly the .po files for auto-refreshing. In summary, `lingui compile` command isn't required when using this plugin
`@lingui/vite-plugin` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.

## Installation

```sh
npm install --save-dev @lingui/vite-plugin
# yarn add --dev @lingui/vite-plugin
```

## Usage

### Via `vite.config.ts`

```ts
import { UserConfig } from 'vite';
import lingui from '@lingui/vite-plugin'

const config: UserConfig = {
plugins: [lingui()]
}
```

### Then in Vite-processed code:

```ts
const { messages } = await import(`./locales/${language}.po`);
```
> See Vite's official documentation for more info about Vite dynamic imports
> https://vitejs.dev/guide/features.html#dynamic-import

## License

[MIT][license]

[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
[linguijs]: https://github.com/lingui/js-lingui
[documentation]: https://lingui.dev/
[package]: https://www.npmjs.com/package/@lingui/vite-plugin
[badge-downloads]: https://img.shields.io/npm/dw/@lingui/vite-plugin.svg
[badge-version]: https://img.shields.io/npm/v/@lingui/vite-plugin.svg
[badge-license]: https://img.shields.io/npm/l/@lingui/vite-plugin.svg
1 change: 1 addition & 0 deletions packages/vite-plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./src"
39 changes: 39 additions & 0 deletions packages/vite-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@lingui/vite-plugin",
"version": "3.16.1",
"description": "Vite plugin for Lingui message catalogs",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"license": "MIT",
"keywords": [
"vite-plugin",
"i18n",
"vite",
"linguijs",
"internationalization",
"i10n",
"localization",
"i9n",
"translation"
],
"scripts": {
"build": "rimraf ./build && tsc"
},
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"@lingui/cli": "^3.16.1",
"@lingui/conf": "^3.16.1"
},
"devDependencies": {
"vite": "3.2.4"
}
}
54 changes: 54 additions & 0 deletions packages/vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { getConfig } from '@lingui/conf';
import { createCompiledCatalog, getCatalogs, getCatalogForFile } from '@lingui/cli/api';
import path from 'path';
import type { Plugin } from 'vite';

const fileRegex = /\.(po)$/;

type LinguiConfigOpts = {
cwd?: string;
configPath?: string;
skipValidation?: boolean;
}

export default function lingui(linguiConfig: LinguiConfigOpts = {}): Plugin {
const config = getConfig(linguiConfig);

return {
name: 'vite-plugin-lingui',

transform(src, id) {
if (fileRegex.test(id)) {
const catalogRelativePath = path.relative(config.rootDir, id);

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

const { locale, catalog } = fileCatalog;
const catalogs = catalog.readAll();

const messages = Object.keys(catalogs[locale]).reduce((acc, key) => {
acc[key] = catalog.getTranslation(catalogs, locale, key, {
fallbackLocales: config.fallbackLocales,
sourceLocale: config.sourceLocale,
});

return acc;
}, {});

const compiled = createCompiledCatalog(locale, messages, {
strict: false,
namespace: 'es',
pseudoLocale: config.pseudoLocale,
});

return {
code: compiled,
map: null, // provide source map if available
};
}
},
};
}
7 changes: 7 additions & 0 deletions packages/vite-plugin/test/.linguirc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"locales": ["en", "cs"],
"catalogs": [{
"path": "<rootDir>/locale/{locale}/messages"
}],
"format": "po"
}
8 changes: 8 additions & 0 deletions packages/vite-plugin/test/__snapshots__/index.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`vite-plugin should return compiled catalog 1`] = `
Object {
code: /*eslint-disable*/export const messages=JSON.parse("{\\"Hello World\\":\\"Hello World\\",\\"My name is {name}\\":[\\"My name is \\",[\\"name\\"]]}");,
map: null,
}
`;
15 changes: 15 additions & 0 deletions packages/vite-plugin/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from "path"
import vitePlugin from "../src"

describe("vite-plugin", () => {
it("should return compiled catalog", async() => {
const p = vitePlugin({
configPath: path.resolve(
__dirname,
".linguirc",
),
})
const result = await (p.transform as any)('', path.join(__dirname, "locale", "en", "messages.po"))
expect(result).toMatchSnapshot()
})
})
5 changes: 5 additions & 0 deletions packages/vite-plugin/test/locale/cs/messages.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
msgid "Hello World"
msgstr "Hello World"

msgid "My name is {name}"
msgstr "My name is {name}"
5 changes: 5 additions & 0 deletions packages/vite-plugin/test/locale/en/messages.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
msgid "Hello World"
msgstr "Hello World"

msgid "My name is {name}"
msgstr "My name is {name}"
18 changes: 18 additions & 0 deletions packages/vite-plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2019",
"sourceMap": true,
"noEmit": false,
"declaration": true,
"outDir": "./build",
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
11 changes: 11 additions & 0 deletions scripts/build/bundles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum BundleType {
UNIVERSAL = "UNIVERSAL",
NODE = "NODE",
CUSTOM = "CUSTOM",
NOOP = "NOOP",
ESM = "ESM",
}
Expand All @@ -9,6 +10,11 @@ export type BundleDef = {
type: BundleType,
externals?: readonly string[]

/**
* Command to execute, used with {@link BundleType.CUSTOM}
*/
cmd?: string;

/**
* Optional. Default index.js
*/
Expand Down Expand Up @@ -52,6 +58,11 @@ export const bundles: readonly BundleDef[] = [
type: BundleType.NODE,
packageName: 'snowpack-plugin',
},
{
type: BundleType.CUSTOM,
packageName: 'vite-plugin',
cmd: 'yarn workspace @lingui/vite-plugin build',
},
{
type: BundleType.NODE,
packageName: 'macro',
Expand Down
34 changes: 34 additions & 0 deletions scripts/build/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {exec} from "child_process"
import {BundleDef} from "./bundles"
import chalk from "chalk"
import ora from "ora"

function asyncExecuteCommand(command: string) {
return new Promise((resolve, reject) =>
exec(command, (error, stdout, stderr) => {
stdout = stdout.trim()
stderr = stderr.trim()

if (error) {
reject({stdout, stderr})
return
}
resolve({stdout, stderr})
})
)
}

export default async function(bundle: BundleDef) {
const logKey = chalk.white.bold(bundle.packageName)

const spinner = ora(logKey).start()

try {
await asyncExecuteCommand(bundle.cmd);
} catch (error) {
spinner.fail(error.stdout)
process.exit(1);
}

spinner.succeed()
}
14 changes: 8 additions & 6 deletions scripts/build/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
const argv = require("minimist")(process.argv.slice(2))

import {BundleType, bundles, BundleDef} from "./bundles"
import {prepareNpmPackages} from "./packaging";
import {asyncRimRaf} from "./utils";
import {prepareNpmPackages} from "./packaging"
import {asyncRimRaf} from "./utils"

import rollup from "./rollup";
import babel from "./babel";
import noop from "./noop";
import rollup from "./rollup"
import babel from "./babel"
import noop from "./noop"
import customBuilder from "./custom"

const builders = {
[BundleType.UNIVERSAL]: rollup,
[BundleType.NODE]: babel,
[BundleType.NOOP]: noop
[BundleType.NOOP]: noop,
[BundleType.CUSTOM]: customBuilder
}

const requestedEntries = (argv._[0] || "")
Expand Down
45 changes: 45 additions & 0 deletions website/docs/ref/vite-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Vite Plugin

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/vite-plugin` is a Vite plugin, which compiles `.po` catalogs on the fly:

## Installation

Install `@lingui/vite-plugin` as a development dependency:

```bash npm2yarn
npm install --save-dev @lingui/vite-plugin
```

## Usage

Simply add `@lingui/vite-plugin` inside your `vite.config.ts`:

```ts title="vite.config.ts"
import { UserConfig } from 'vite';
import lingui from '@lingui/vite-plugin'

const config: UserConfig = {
plugins: [lingui()]
}
```

Then in your code all you need is to use [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) to load only necessary catalog. Extension is mandatory.

```ts
export async function dynamicActivate(locale: string) {
const { messages } = await import(`./locales/${locale}.po`);

i18n.load(locale, messages)
i18n.activate(locale)
}
```

See the [guide about dynamic loading catalogs](/docs/guides/dynamic-loading-catalogs.md) for more info.

See [Vite's official documentation](https://vitejs.dev/guide/features.html#dynamic-import) for more info about Vite dynamic imports.

:::note
You also need to set up [babel-plugin-macros](https://github.com/kentcdodds/babel-plugin-macros) to support [macros](/docs/ref/macro.md).
:::
5 changes: 5 additions & 0 deletions website/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ const sidebar = [
label: '@lingui/snowpack-plugin',
id: 'ref/snowpack-plugin',
},
{
type: 'doc',
label: '@lingui/vite-plugin',
id: 'ref/vite-plugin',
},
{
type: 'doc',
label: 'Lingui Configuration',
Expand Down
Loading

1 comment on commit db5d3c3

@vercel
Copy link

@vercel vercel bot commented on db5d3c3 Jan 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.