diff --git a/docs/index.rst b/docs/index.rst index f421750a4..6101bea0d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -113,6 +113,7 @@ Documentation contents Configuration Locale Detection Webpack Loader + Snowpack Plugin Catalog Formats ICU MessageFormat diff --git a/docs/ref/snowpack-plugin.rst b/docs/ref/snowpack-plugin.rst new file mode 100644 index 000000000..3139f682e --- /dev/null +++ b/docs/ref/snowpack-plugin.rst @@ -0,0 +1,46 @@ +*********************************************** +API Reference - Snowpack Plugin (@lingui/snowpack-plugin) +*********************************************** + +It's a good practice to use compiled message catalogs during development. However, +running :cli:`compile` everytime messages are changed soon becomes tedious. + +``@lingui/snowpack-plugin`` is a snowpack loader, which compiles messages on the fly: + +Installation +============ + +Install ``@lingui/snowpack-plugin`` as a development dependency: + +.. code-block:: sh + + npm install --save-dev @lingui/snowpack-plugin + + # Or using yarn + # yarn add --dev @lingui/snowpack-plugin + +Usage +===== + +Simply add ``@lingui/snowpack-plugin`` inside your ``snowpack.config.js``: + +.. code-block:: js + + module.exports = { + plugins: [ + '@lingui/snowpack-plugin', + ], + } + +Then in your code all you need is to use dynamic() import. Extension is mandatory. In case of using po format, use ``.po``. + +.. code-block:: jsx + + export async function dynamicActivate(locale: string) { + const { messages } = await import(`./locales/${locale}/messages.po`) + i18n.load(locale, messages) + i18n.activate(locale) + } + +See the `guide about dynamic loading catalogs <../guides/dynamic-loading-catalogs.html>`_ +for more info. diff --git a/packages/cli/src/api/catalog.ts b/packages/cli/src/api/catalog.ts index 0ecf0710d..40b312af2 100644 --- a/packages/cli/src/api/catalog.ts +++ b/packages/cli/src/api/catalog.ts @@ -522,8 +522,8 @@ export function getCatalogForFile(file: string, catalogs: Array) { const catalogFile = `${catalog.path}${catalog.format.catalogExtension}` const catalogGlob = catalogFile.replace(LOCALE, "*") const match = micromatch.capture( - normalizeRelativePath(catalogGlob), - normalizeRelativePath(file) + normalizeRelativePath(path.relative(catalog.config.rootDir, catalogGlob)), + normalizeRelativePath(file), ) if (match) { return { diff --git a/packages/snowpack-plugin/README.md b/packages/snowpack-plugin/README.md new file mode 100644 index 000000000..60c5b44fb --- /dev/null +++ b/packages/snowpack-plugin/README.md @@ -0,0 +1,42 @@ +[![License][badge-license]][license] +[![Version][badge-version]][package] +[![Downloads][badge-downloads]][package] + +# @lingui/snowpack-plugin + +> Snowpack 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/snowpack-plugin` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples. + +## Installation + +```sh +npm install --save-dev @lingui/snowpack-plugin +# yarn add --dev @lingui/snowpack-plugin +``` + +## Usage + +### Via `snowpack.config.js` (Recommended) + +**snowpack.config.js** + +```js +module.exports = { + plugins: [ + '@lingui/snowpack-plugin', + ], +} +``` + +## License + +[MIT][license] + +[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE +[linguijs]: https://github.com/lingui/js-lingui +[documentation]: https://lingui.js.org/ +[package]: https://www.npmjs.com/package/@lingui/snowpack-plugin +[badge-downloads]: https://img.shields.io/npm/dw/@lingui/snowpack-plugin.svg +[badge-version]: https://img.shields.io/npm/v/@lingui/snowpack-plugin.svg +[badge-license]: https://img.shields.io/npm/l/@lingui/snowpack-plugin.svg diff --git a/packages/snowpack-plugin/index.js b/packages/snowpack-plugin/index.js new file mode 100644 index 000000000..c14e96c06 --- /dev/null +++ b/packages/snowpack-plugin/index.js @@ -0,0 +1 @@ +export { default } from "./src" diff --git a/packages/snowpack-plugin/package.json b/packages/snowpack-plugin/package.json new file mode 100644 index 000000000..0b85d37b5 --- /dev/null +++ b/packages/snowpack-plugin/package.json @@ -0,0 +1,33 @@ +{ + "name": "@lingui/snowpack-plugin", + "version": "3.4.0", + "description": "Snowpack plugin for Lingui message catalogs", + "main": "index.js", + "license": "MIT", + "keywords": [ + "snowpack-plugin", + "i18n", + "snowpack", + "linguijs", + "internationalization", + "i10n", + "localization", + "i9n", + "translation" + ], + "repository": { + "type": "git", + "url": "https://github.com/lingui/js-lingui.git" + }, + "bugs": { + "url": "https://github.com/lingui/js-lingui/issues" + }, + "engines": { + "node": ">=10.0.0" + }, + "dependencies": { + "@babel/runtime": "^7", + "@lingui/cli": "^3.4.0", + "@lingui/conf": "^3.4.0" + } +} diff --git a/packages/snowpack-plugin/src/index.ts b/packages/snowpack-plugin/src/index.ts new file mode 100644 index 000000000..aaf86642b --- /dev/null +++ b/packages/snowpack-plugin/src/index.ts @@ -0,0 +1,65 @@ +import path from "path" +import { getConfig } from "@lingui/conf" +import { createCompiledCatalog, getCatalogs, getCatalogForFile } from "@lingui/cli/api" + +type LinguiConfigOpts = { + cwd?: string; + configPath?: string; + skipValidation?: boolean; +} +type SnowpackLoadOpts = { + filePath: string +} +function extractLinguiMessages(snowpackConfig?, linguiConfig: LinguiConfigOpts = {}) { + const strict = process.env.NODE_ENV !== 'production' + const config = getConfig(linguiConfig) + + return { + name: '@lingui/snowpack-plugin', + resolve: { + input: ['.po', '.json'], + output: ['.js'], + }, + async load({ filePath }: SnowpackLoadOpts) { + const catalogRelativePath = path.relative(config.rootDir, filePath) + const EMPTY_EXT = /\.[0-9a-z]+$/.test(filePath) + const JS_EXT = /\.js+$/.test(filePath) + + if (!EMPTY_EXT || JS_EXT) { + const formats = { + minimal: ".json", + po: ".po", + lingui: ".json" + } + throw new Error(`@lingui/snowpack-plugin: File extension is mandatory, for ex: import('./locales/en/messages${formats[config.format]}')`) + } + + 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, + namespace: config.compileNamespace, + pseudoLocale: config.pseudoLocale, + }) + + return compiled + } + } +} + +export default extractLinguiMessages diff --git a/packages/snowpack-plugin/test/.linguirc b/packages/snowpack-plugin/test/.linguirc new file mode 100644 index 000000000..d6df6ed74 --- /dev/null +++ b/packages/snowpack-plugin/test/.linguirc @@ -0,0 +1,7 @@ +{ + "locales": ["en", "cs"], + "catalogs": [{ + "path": "/locale/{locale}/messages" + }], + "format": "po" +} diff --git a/packages/snowpack-plugin/test/__snapshots__/index.ts.snap b/packages/snowpack-plugin/test/__snapshots__/index.ts.snap new file mode 100644 index 000000000..98036548b --- /dev/null +++ b/packages/snowpack-plugin/test/__snapshots__/index.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snowpack-plugin should return compiled catalog 1`] = `/*eslint-disable*/module.exports={messages:{"Hello World":"Hello World","My name is {name}":["My name is ",["name"]]}};`; + +exports[`snowpack-plugin should return error if import doesn't contain extension 1`] = `@lingui/snowpack-plugin: File extension is mandatory, for ex: import('./locales/en/messages.po')`; + +exports[`snowpack-plugin to match snapshot 1`] = ` +Object { + load: [Function], + name: @lingui/snowpack-plugin, + resolve: Object { + input: Array [ + .po, + .json, + ], + output: Array [ + .js, + ], + }, +} +`; diff --git a/packages/snowpack-plugin/test/index.ts b/packages/snowpack-plugin/test/index.ts new file mode 100644 index 000000000..4b10045e7 --- /dev/null +++ b/packages/snowpack-plugin/test/index.ts @@ -0,0 +1,25 @@ +import path from "path" +import snowpackPlugin from "../src" + +describe("snowpack-plugin", () => { + it("to match snapshot", () => { + const p = snowpackPlugin(); + expect(p).toMatchSnapshot(); + }) + + it("should return error if import doesn't contain extension", async () => { + const p = snowpackPlugin() + expect(async () => p.load({ filePath: "./fixtures/locale/en/messages" })).rejects.toThrowErrorMatchingSnapshot() + }) + + it("should return compiled catalog", async() => { + const p = snowpackPlugin(null, { + configPath: path.resolve( + __dirname, + ".linguirc", + ), + }) + const result = await p.load({ filePath: path.join(__dirname, "locale", "en", "messages.po") }) + expect(result).toMatchSnapshot() + }) +}) \ No newline at end of file diff --git a/packages/snowpack-plugin/test/locale/cs/messages.po b/packages/snowpack-plugin/test/locale/cs/messages.po new file mode 100644 index 000000000..b4612b13a --- /dev/null +++ b/packages/snowpack-plugin/test/locale/cs/messages.po @@ -0,0 +1,5 @@ +msgid "Hello World" +msgstr "Hello World" + +msgid "My name is {name}" +msgstr "My name is {name}" diff --git a/packages/snowpack-plugin/test/locale/en/messages.po b/packages/snowpack-plugin/test/locale/en/messages.po new file mode 100644 index 000000000..b4612b13a --- /dev/null +++ b/packages/snowpack-plugin/test/locale/en/messages.po @@ -0,0 +1,5 @@ +msgid "Hello World" +msgstr "Hello World" + +msgid "My name is {name}" +msgstr "My name is {name}" diff --git a/scripts/build/bundles.js b/scripts/build/bundles.js index ae7cfee5b..dd7d63b91 100644 --- a/scripts/build/bundles.js +++ b/scripts/build/bundles.js @@ -33,6 +33,10 @@ const bundles = [ type: bundleTypes.NODE, entry: "@lingui/babel-plugin-extract-messages" }, + { + type: bundleTypes.NODE, + entry: "@lingui/snowpack-plugin" + }, { type: bundleTypes.NODE, diff --git a/yarn.lock b/yarn.lock index 1de0e5ee4..b4847d649 100644 --- a/yarn.lock +++ b/yarn.lock @@ -991,6 +991,13 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" +"@babel/runtime@^7": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"