Repository: lingui/js-lingui
Pull request #947 (2021-01-18 11:47:14)

feat: Introduced @lingui/snowpack-plugin
Introduced @lingui/snowpack-loader. Let's snowpack users compile on the fly his catalogs (the same as @lingui/loader)

Alright is almost ready!

![Kapture 2021-01-18 at 14 49 38](https://user-images.githubusercontent.com/22656541/104923520-73f2e300-599c-11eb-9642-597b2118fdeb.gif)

- [x] Pending docs

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/lingui-js/js-lingui/gl2b0jmbp](https://vercel.com/lingui-js/js-lingui/gl2b0jmbp)  
✅ Preview: *Failed*


codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=h1) Report
> Merging [#947](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=desc) (1101606) into [main](https://codecov.io/gh/lingui/js-lingui/commit/8ac50ab838b845c88640e32fcca99bf4d83b4b09?el=desc) (8ac50ab) will **increase** coverage by `0.38%`.
> The diff coverage is `92.30%`.

[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/947/graphs/tree.svg?width=650&height=150&src=pr&token=CuJ1VD5BY5)](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #947      +/-   ##
==========================================
+ Coverage   82.99%   83.38%   +0.38%     
==========================================
  Files          52       53       +1     
  Lines        1582     1595      +13     
  Branches      427      427              
==========================================
+ Hits         1313     1330      +17     
+ Misses        162      155       -7     
- Partials      107      110       +3     
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/snowpack-loader/src/index.ts](https://codecov.io/gh/lingui/js-lingui/pull/947/diff?src=pr&el=tree#diff-cGFja2FnZXMvc25vd3BhY2stbG9hZGVyL3NyYy9pbmRleC50cw==) | `92.30% <92.30%> (ø)` | |
| [packages/cli/src/api/catalog.ts](https://codecov.io/gh/lingui/js-lingui/pull/947/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2xpL3NyYy9hcGkvY2F0YWxvZy50cw==) | `83.73% <0.00%> (+2.39%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=footer). Last update [8ac50ab...1101606](https://codecov.io/gh/lingui/js-lingui/pull/947?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

github-actions[bot]: **Size Change:** 0 B 

**Total Size:** 7.42 kB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./packages/core/build/cjs/core.production.min.js` | 2.09 kB | 0 B |
| `./packages/core/build/cjs/dev.production.min.js` | 2.09 kB | 0 B |
| `./packages/detect-locale/build/cjs/detect-locale.production.min.js` | 961 B | 0 B |
| `./packages/react/build/cjs/react.production.min.js` | 2.28 kB | 0 B |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
amankkg: Regarding package name. Snowpack and its community are using `snowpack-plugin-xyz` or `@xyz/snowpack-plugin` naming template.  
Maybe we can also align to it? To me, it seems like `xyz-loader` is something specific to webpack

Patch:
diff --git a/docs/index.rst b/docs/index.rst
index f421750a..6101bea0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -113,6 +113,7 @@ Documentation contents
    Configuration <ref/conf>
    Locale Detection <ref/locale-detector>
    Webpack Loader <ref/loader>
+   Snowpack Plugin <ref/snowpack-plugin>
    Catalog Formats <ref/catalog-formats>
    ICU MessageFormat <ref/message-format>
 
diff --git a/docs/ref/snowpack-plugin.rst b/docs/ref/snowpack-plugin.rst
new file mode 100644
index 00000000..3139f682
--- /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 0ecf0710..40b312af 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<Catalog>) {
     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 00000000..60c5b44f
--- /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 00000000..c14e96c0
--- /dev/null
+++ b/packages/snowpack-plugin/index.js
@@ -0,0 +1,1 @@
+export { default } from "./src"
diff --git a/packages/snowpack-plugin/package.json b/packages/snowpack-plugin/package.json
new file mode 100644
index 00000000..0b85d37b
--- /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 00000000..aaf86642
--- /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/scripts/build/bundles.js b/scripts/build/bundles.js
index ae7cfee5..dd7d63b9 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 1de0e5ee..b4847d64 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"


Test patch:
diff --git a/packages/snowpack-plugin/test/.linguirc b/packages/snowpack-plugin/test/.linguirc
new file mode 100644
index 00000000..d6df6ed7
--- /dev/null
+++ b/packages/snowpack-plugin/test/.linguirc
@@ -0,0 +1,7 @@
+{
+  "locales": ["en", "cs"],
+  "catalogs": [{
+    "path": "<rootDir>/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 00000000..98036548
--- /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 00000000..4b10045e
--- /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 00000000..b4612b13
--- /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 00000000..b4612b13
--- /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}"