├── .cargo
└── config.toml
├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CONTRIBUTING.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── examples
└── nextjs
│ ├── .gitignore
│ ├── README.md
│ ├── lingui.config.js
│ ├── locales
│ ├── cs
│ │ └── messages.po
│ └── en
│ │ └── messages.po
│ ├── next.config.js
│ ├── package.json
│ ├── public
│ └── favicon.ico
│ ├── src
│ ├── i18n.ts
│ ├── pages
│ │ ├── _app.tsx
│ │ └── index.tsx
│ └── styles
│ │ └── globals.css
│ ├── tsconfig.json
│ └── yarn.lock
├── package.json
├── rust-toolchain.toml
└── src
├── ast_utils.rs
├── builder.rs
├── generate_id.rs
├── js_macro_folder.rs
├── jsx_visitor.rs
├── lib.rs
├── macro_utils.rs
├── options.rs
├── tests
├── common
│ └── mod.rs
├── imports.rs
├── js_define_message.rs
├── js_icu.rs
├── js_t.rs
├── jsx.rs
├── jsx_icu.rs
├── mod.rs
├── runtime_config.rs
└── use_lingui.rs
└── tokens.rs
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | # These command aliases are not final, may change
2 | [alias]
3 | # Alias to build actual plugin binary for the specified target.
4 | build-wasi = "build --target wasm32-wasip1"
5 | build-wasm32 = "build --target wasm32-unknown-unknown"
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | - next
8 | paths-ignore:
9 | - 'README.md'
10 | - 'LICENSE'
11 |
12 | jobs:
13 | check:
14 | name: Check
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | # Uses values from rust-toolchain.toml
20 | - uses: actions-rust-lang/setup-rust-toolchain@v1
21 |
22 | - uses: Swatinem/rust-cache@v2
23 |
24 | - name: Check formatting
25 | run: cargo fmt --check
26 |
27 | - name: Run cargo test
28 | run: cargo test
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [released, prereleased]
6 |
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | # Uses values from rust-toolchain.toml
15 | - uses: actions-rust-lang/setup-rust-toolchain@v1
16 |
17 | - name: Set up Node
18 | uses: actions/setup-node@v4
19 | with:
20 | registry-url: 'https://registry.npmjs.org'
21 | node-version: 16.x
22 |
23 | - uses: Swatinem/rust-cache@v2
24 |
25 | - name: Build and publish 'latest' tag
26 | if: ${{ github.event.action == 'released' }}
27 | run: |
28 | npm publish
29 | env:
30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
31 |
32 | - name: Build and publish 'next' tag
33 | if: ${{ github.event.action == 'prereleased' }}
34 | run: |
35 | npm publish --tag next
36 | env:
37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | ^target/
3 | target
4 | .idea
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Install rust
2 |
3 | You can follow instructions at ['Install Rust' page from the official rust website](https://www.rust-lang.org/tools/install)
4 |
5 | ## Add wasm target to rust
6 |
7 | ```bash
8 | rustup target add wasm32-wasip1
9 | ```
10 |
11 | ## Running tests
12 | ```bash
13 | # run all test suite
14 | cargo test
15 |
16 | # run individual test
17 | cargo test js_choices_may_contain_expressions
18 |
19 | # you may specify only prefix of test name to target more cases
20 | cargo test jsx_
21 | ```
22 |
23 | ## Building for production
24 |
25 | ```bash
26 | # (alias for `cargo build --target wasm32-wasip1`)
27 | cargo build-wasi --release
28 | ```
29 | Then wasm binary would be on the path: `./target/wasm32-wasip1/release/lingui_macro_plugin.wasm`
30 |
31 | You can check it in your own project or in the `examples/nextjs-13` example in this repo by specifying full path to the WASM binary:
32 |
33 | ```ts
34 | /** @type {import('next').NextConfig} */
35 | const nextConfig = {
36 | experimental: {
37 | swcPlugins: [
38 | ['/Users/tim/projects/lingui-macro-plugin/target/wasm32-wasip1/release/lingui_macro_plugin.wasm', {}],
39 | ],
40 | },
41 | };
42 |
43 | module.exports = nextConfig;
44 | ```
45 |
46 | ## Rust Version
47 |
48 | It's important to build a plugin with the same Rust version used to build SWC itself.
49 |
50 | This project uses `rust-toolchain` file in the root of project to define rust version.
51 |
52 | To update Rust, put new version into `rust-toolchain` and call `rustup update` command
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "lingui_macro_plugin"
3 | version = "5.5.2"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [profile.release]
10 | # This removes more dead code
11 | codegen-units = 1
12 | lto = false
13 | # Optimize for size
14 | opt-level = "s"
15 | # Optimize for performance, this is default so you don't need to specify it
16 | # opt-level = "z"
17 |
18 | [dependencies]
19 | data-encoding = "2.3.3"
20 | sha2 = "0.10.8"
21 | serde = "1.0.207"
22 | serde_json = "1.0.125"
23 | regex = "1.10.6"
24 | once_cell = "1.19.0"
25 | swc_core = { version = "15.0.1", features = [
26 | "ecma_plugin_transform",
27 | "ecma_utils",
28 | "ecma_visit",
29 | "ecma_ast",
30 | "ecma_parser",
31 | "common",
32 | "testing_transform",
33 | ] }
34 | # .cargo/config defines few alias to build plugin.
35 | # cargo build-wasi generates wasm-wasi32 binary
36 | # cargo build-wasm32 generates wasm32-unknown-unknown binary.
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Lingui
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
A SWC Plugin For LinguiJS
2 |
3 |
4 |
5 | A Rust versions of [LinguiJS Macro](https://lingui.dev/ref/macro) [

](https://github.com/lingui/swc-plugin)
6 |
7 | [](https://www.npmjs.com/package/@lingui/swc-plugin)
8 | [](https://www.npmjs.com/package/@lingui/swc-plugin)
9 | [](https://github.com/lingui/swc-plugin/actions/workflows/ci.yml)
10 | [](https://github.com/lingui/swc-plugin/graphs/contributors)
11 | [](https://github.com/lingui/swc-plugin/blob/main/LICENSE)
12 |
13 |
14 |
15 | ## Installation
16 |
17 | Install plugin:
18 | ```bash
19 | npm install --save-dev @lingui/swc-plugin
20 | # or
21 | yarn add -D @lingui/swc-plugin
22 | ```
23 |
24 | You still need to install `@lingui/macro` for typings support:
25 | ```bash
26 | npm install @lingui/macro
27 | # or
28 | yarn add @lingui/macro
29 | ```
30 |
31 | ## Usage
32 |
33 | `.swcrc`
34 | https://swc.rs/docs/configuration/swcrc
35 |
36 | ```json5
37 | {
38 | "$schema": "https://json.schemastore.org/swcrc",
39 | "jsc": {
40 | "experimental": {
41 | "plugins": [
42 | [
43 | "@lingui/swc-plugin",
44 | {
45 | // Optional
46 | // Unlike the JS version this option must be passed as object only.
47 | // Docs https://lingui.dev/ref/conf#runtimeconfigmodule
48 | // "runtimeModules": {
49 | // "i18n": ["@lingui/core", "i18n"],
50 | // "trans": ["@lingui/react", "Trans"]
51 | // }
52 | // Lingui strips non-essential fields in production builds for performance.
53 | // You can override the default behavior with:
54 | // "stripNonEssentialFields": false/true
55 | },
56 | ],
57 | ],
58 | },
59 | },
60 | }
61 | ```
62 |
63 | Or Next JS Usage:
64 |
65 | `next.config.js`
66 | ```js
67 | /** @type {import('next').NextConfig} */
68 | const nextConfig = {
69 | reactStrictMode: true,
70 | experimental: {
71 | swcPlugins: [
72 | ['@lingui/swc-plugin', {
73 | // the same options as in .swcrc
74 | }],
75 | ],
76 | },
77 | };
78 |
79 | module.exports = nextConfig;
80 | ```
81 |
82 | > **Note**
83 | > Consult with full working example for NextJS in the `/examples` folder in this repo.
84 |
85 |
86 | ## Compatibility
87 | SWC Plugin support is still experimental. They do not guarantee a semver backwards compatibility between different `swc-core` versions.
88 |
89 | So you need to select an appropriate version of the plugin to match compatible `swc_core` using a https://plugins.swc.rs/.
90 |
91 | Below is a table referencing the swc_core version used during the plugin build, along with a link to the plugin's site to check compatibility with runtimes for this swc_core range.
92 |
93 | | Plugin Version | used `swc_core` |
94 | |---------------------------------------------------|-------------------------------------------------------|
95 | | `0.1.0`, `4.0.0-next.0` | `0.52.8` |
96 | | `0.2.*`, `4.0.0-next.1` ~ `4.0.0-next.3` | `0.56.1` |
97 | | `4.0.0` | `0.75.33` |
98 | | `4.0.1` | `0.76.0` |
99 | | `4.0.2` | `0.76.41` |
100 | | `4.0.3` | `0.78.28` |
101 | | `4.0.4` | `0.79.x` |
102 | | `4.0.5`, `4.0.6` | [`0.87.x`](https://plugins.swc.rs/versions/range/10) |
103 | | `4.0.7`, `4.0.8`, `5.0.0-next.0` ~ `5.0.0-next.1` | [`0.90.35`](https://plugins.swc.rs/versions/range/12) |
104 | | `4.0.9` | [`0.96.9`](https://plugins.swc.rs/versions/range/15) |
105 | | `4.0.10` | [`0.101.4`](https://plugins.swc.rs/versions/range/94) |
106 | | `4.1.0`, `5.0.0` ~ `5.2.0` | [`0.106.3`](https://plugins.swc.rs/versions/range/95) |
107 | | `5.3.0` | [`5.0.4`](https://plugins.swc.rs/versions/range/116) |
108 | | `5.4.0` | [`14.1.0`](https://plugins.swc.rs/versions/range/138) |
109 | | `5.5.0` ~ `5.5.2` | [`15.0.1`](https://plugins.swc.rs/versions/range/271) |
110 |
111 |
112 | > **Note**
113 | > next `v13.2.4` ~ `v13.3.1` cannot execute SWC Wasm plugins, due to a [bug of next-swc](https://github.com/vercel/next.js/issues/46989#issuecomment-1486989081).
114 | >
115 | > next `v13.4.3` ~ `v13.4.5-canary.7` cannot execute SWC Wasm plugins, due to [missing filesystem cache](https://github.com/vercel/next.js/pull/50651).
116 |
117 | - Version `0.1.0` ~ `0.*` compatible with `@lingui/core@3.*`
118 | - Version `4.*` compatible with `@lingui/core@4.*`
119 | - Version `5.*` compatible with `@lingui/core@5.*`
120 |
121 | ## License
122 |
123 | The project is licensed under the [MIT](https://github.com/lingui/swc-plugin/blob/main/LICENSE) license.
124 |
--------------------------------------------------------------------------------
/examples/nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/nextjs/README.md:
--------------------------------------------------------------------------------
1 | ## Example project using Next 13 SWC Compiler with LinguiJS Plugin
2 |
3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
4 |
5 | ## Getting Started
6 |
7 | First, run the development server:
8 |
9 | ```bash
10 | npm run dev
11 | # or
12 | yarn dev
13 | # or
14 | pnpm dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | ## LinguiJS Integration
20 | LinguiJs integrated with standard nextjs i18n support. Nextjs do routing for every language,
21 | LinguiJs activated with `router.locale`.
22 |
23 | Open [http://localhost:3000/cs](http://localhost:3000/cs) with your browser to prerender page in different language.
24 |
25 | ## LinguiJS Related Commands
26 |
27 | Extract messages from sourcecode:
28 | ```bash
29 | npm run lingui:extract
30 | # or
31 | yarn lingui:extract
32 | # or
33 | pnpm lingui:extract
34 | ```
35 |
36 | ## Important Notes
37 | - You **should not have** a babel config in the project, otherwise Next will turn off SWC compiler in favor of babel.
38 | - The actual code is compiled with SWC + Lingui SWC plugin.
39 |
40 | ## Learn More
41 |
42 | To learn more about Next.js, take a look at the following resources:
43 |
44 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
45 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
46 |
47 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
48 |
49 | ## Deploy on Vercel
50 |
51 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
52 |
53 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
54 |
--------------------------------------------------------------------------------
/examples/nextjs/lingui.config.js:
--------------------------------------------------------------------------------
1 | // use nextjs config as single source of truth for defining locales
2 | const nextConfig = require('./next.config');
3 |
4 | /** @type {import('@lingui/conf').LinguiConfig} */
5 | module.exports = {
6 | locales: nextConfig.i18n.locales,
7 | sourceLocale: nextConfig.i18n.defaultLocale,
8 | catalogs: [
9 | {
10 | path: "/locales/{locale}/messages",
11 | include: ["/src"],
12 | exclude: ["**/node_modules/**"],
13 | },
14 | ],
15 | format: "po",
16 | }
17 |
--------------------------------------------------------------------------------
/examples/nextjs/locales/cs/messages.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: \n"
6 | "PO-Revision-Date: \n"
7 | "Last-Translator: \n"
8 | "Language: \n"
9 | "Language-Team: \n"
10 | "Content-Type: \n"
11 | "Content-Transfer-Encoding: \n"
12 | "Plural-Forms: \n"
13 |
14 | #: src/pages/index.tsx:46
15 | msgid "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"
16 | msgstr "{count, plural, zero {Nejsou žádné knihy} one {Je tu jedna kniha} other {Existuje # knih}}"
17 |
18 | #: src/pages/index.tsx:52
19 | msgid "Date formatter example:"
20 | msgstr "Příklad formátovače data:"
21 |
22 | #: src/pages/index.tsx:43
23 | msgid "Decrement"
24 | msgstr "Úbytek"
25 |
26 | #: src/pages/index.tsx:60
27 | msgid "I have a balance of {0}"
28 | msgstr "Mám zůstatek {0}"
29 |
30 | #: src/pages/index.tsx:40
31 | msgid "Increment"
32 | msgstr "Přírůstek"
33 |
34 | #: src/pages/index.tsx:28
35 | msgid "Language switcher example:"
36 | msgstr "Příklad přepínače jazyků:"
37 |
38 | #: src/pages/index.tsx:58
39 | msgid "Number formatter example:"
40 | msgstr "Příklad formátovače čísel:"
41 |
42 | #: src/pages/index.tsx:37
43 | msgid "Plurals example:"
44 | msgstr "Příklad množného čísla:"
45 |
46 | #: src/pages/index.tsx:54
47 | msgid "Today is {0}"
48 | msgstr "Dnes je {0}"
49 |
--------------------------------------------------------------------------------
/examples/nextjs/locales/en/messages.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "POT-Creation-Date: 2023-01-13 14:55+0100\n"
4 | "MIME-Version: 1.0\n"
5 | "Content-Type: text/plain; charset=utf-8\n"
6 | "Content-Transfer-Encoding: 8bit\n"
7 | "X-Generator: @lingui/cli\n"
8 | "Language: en\n"
9 | "Project-Id-Version: \n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "PO-Revision-Date: \n"
12 | "Last-Translator: \n"
13 | "Language-Team: \n"
14 | "Plural-Forms: \n"
15 |
16 | #: src/pages/index.tsx:46
17 | msgid "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"
18 | msgstr "{count, plural, zero {There are no books} one {There's one book} other {There are # books}}"
19 |
20 | #: src/pages/index.tsx:52
21 | msgid "Date formatter example:"
22 | msgstr "Date formatter example:"
23 |
24 | #: src/pages/index.tsx:43
25 | msgid "Decrement"
26 | msgstr "Decrement"
27 |
28 | #: src/pages/index.tsx:60
29 | msgid "I have a balance of {0}"
30 | msgstr "I have a balance of {0}"
31 |
32 | #: src/pages/index.tsx:40
33 | msgid "Increment"
34 | msgstr "Increment"
35 |
36 | #: src/pages/index.tsx:28
37 | msgid "Language switcher example:"
38 | msgstr "Language switcher example:"
39 |
40 | #: src/pages/index.tsx:58
41 | msgid "Number formatter example:"
42 | msgstr "Number formatter example:"
43 |
44 | #: src/pages/index.tsx:37
45 | msgid "Plurals example:"
46 | msgstr "Plurals example:"
47 |
48 | #: src/pages/index.tsx:54
49 | msgid "Today is {0}"
50 | msgstr "Today is {0}"
51 |
--------------------------------------------------------------------------------
/examples/nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | const plugin = process.env.USE_LOCAL_PLUGIN_BINARY
4 | ? path.join(__dirname, '../../target/wasm32-wasip1/release/lingui_macro_plugin.wasm')
5 | : '@lingui/swc-plugin';
6 |
7 |
8 | /** @type {import('next').NextConfig} */
9 | const nextConfig = {
10 | reactStrictMode: true,
11 | i18n: {
12 | locales: ["en", "cs"],
13 | defaultLocale: 'en',
14 | },
15 | experimental: {
16 | swcPlugins: [
17 | [plugin, {}],
18 | ],
19 | },
20 | };
21 |
22 | module.exports = nextConfig;
23 |
--------------------------------------------------------------------------------
/examples/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "dev:local-binary": "USE_LOCAL_PLUGIN_BINARY=true next dev",
8 | "build": "next build",
9 | "build:local-binary": "USE_LOCAL_PLUGIN_BINARY=true next build",
10 | "start": "next start",
11 | "lint": "next lint",
12 | "lingui:extract": "lingui extract"
13 | },
14 | "dependencies": {
15 | "@lingui/core": "^4.10.0",
16 | "@lingui/react": "^4.10.0",
17 | "@types/node": "18.11.18",
18 | "@types/react": "18.0.26",
19 | "@types/react-dom": "18.0.10",
20 | "make-plural": "^7.2.0",
21 | "next": "15",
22 | "react": "18.2.0",
23 | "react-dom": "18.2.0",
24 | "typescript": "4.9.4"
25 | },
26 | "devDependencies": {
27 | "@lingui/cli": "^4.10.0",
28 | "@lingui/loader": "^4.10.0",
29 | "@lingui/macro": "^4.10.0",
30 | "@lingui/swc-plugin": "4.0.6"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lingui/swc-plugin/93eae5023a06fa6cdd73426b8aa6e65472fe059a/examples/nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/examples/nextjs/src/i18n.ts:
--------------------------------------------------------------------------------
1 | import { i18n, Messages } from '@lingui/core';
2 | import { useRouter } from 'next/router';
3 | import { useEffect } from 'react';
4 |
5 | export const locales = [
6 | { twoLettersCode: 'en', label: 'English' },
7 | { twoLettersCode: 'cs', label: 'Česky' },
8 | ];
9 |
10 | export async function loadCatalog(locale: string) {
11 | const { messages } = await import(`@lingui/loader!../locales/${locale}/messages.po`);
12 | return messages;
13 | }
14 |
15 | export function useLinguiInit(messages: Messages) {
16 | const router = useRouter()
17 | const locale = router.locale || router.defaultLocale!
18 | const isClient = typeof window !== 'undefined'
19 |
20 | if (!isClient && locale !== i18n.locale) {
21 | // there is single instance of i18n on the server
22 | // note: on the server, we could have an instance of i18n per supported locale
23 | // to avoid calling loadAndActivate for (worst case) each request, but right now that's what we do
24 | i18n.loadAndActivate({ locale, messages })
25 | }
26 | if (isClient && !i18n.locale) {
27 | // first client render
28 | i18n.loadAndActivate({ locale, messages })
29 | }
30 |
31 | useEffect(() => {
32 | const localeDidChange = locale !== i18n.locale
33 | if (localeDidChange) {
34 | i18n.loadAndActivate({ locale, messages })
35 | }
36 | }, [locale])
37 |
38 | return i18n
39 | }
40 |
--------------------------------------------------------------------------------
/examples/nextjs/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 | import type { AppProps } from 'next/app';
3 | import { I18nProvider } from '@lingui/react';
4 | import { useLinguiInit } from '../i18n';
5 |
6 | export default function MyApp({ Component, pageProps, router }: AppProps) {
7 | const i18n = useLinguiInit(pageProps.translation)
8 |
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/nextjs/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Trans, Plural } from "@lingui/macro";
3 |
4 | import { locales, loadCatalog } from '../i18n';
5 | import { useRouter } from 'next/router';
6 | import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
7 | import { useLingui } from '@lingui/react';
8 |
9 | export async function getServerSideProps(ctx: GetServerSidePropsContext): Promise> {
10 | return {
11 | props: {
12 | // we need to pass catalog to the client side to be able to **synchronously** consume it
13 | // on hydration phase. Otherwise, hydration mismatch would happend.
14 | translation: await loadCatalog(ctx.locale as string),
15 | }
16 | };
17 | }
18 |
19 | function Home() {
20 | const router = useRouter();
21 | const [count, setCount] = useState(0);
22 | const { i18n } = useLingui();
23 |
24 | return (
25 |
26 |
27 |
28 | Language switcher example:
29 |
30 | {locales.map((locale, ) => (
31 |
35 | ))}
36 |
37 | Plurals example:
38 |
39 |
42 |
45 |
46 |
52 | Date formatter example:
53 |
54 |
55 | Today is {i18n.date(new Date(), {})}
56 |
57 |
58 | Number formatter example:
59 |
60 |
61 | I have a balance of {i18n.number(1_000_000, { style: "currency", currency: "EUR" })}
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
69 | export default Home;
70 |
--------------------------------------------------------------------------------
/examples/nextjs/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | }
42 |
43 | @media (prefers-color-scheme: dark) {
44 | :root {
45 | --foreground-rgb: 255, 255, 255;
46 | --background-start-rgb: 0, 0, 0;
47 | --background-end-rgb: 0, 0, 0;
48 |
49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50 | --secondary-glow: linear-gradient(
51 | to bottom right,
52 | rgba(1, 65, 255, 0),
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0.3)
55 | );
56 |
57 | --tile-start-rgb: 2, 13, 46;
58 | --tile-end-rgb: 2, 5, 19;
59 | --tile-border: conic-gradient(
60 | #ffffff80,
61 | #ffffff40,
62 | #ffffff30,
63 | #ffffff20,
64 | #ffffff10,
65 | #ffffff10,
66 | #ffffff80
67 | );
68 |
69 | --callout-rgb: 20, 20, 20;
70 | --callout-border-rgb: 108, 108, 108;
71 | --card-rgb: 100, 100, 100;
72 | --card-border-rgb: 200, 200, 200;
73 | }
74 | }
75 |
76 | * {
77 | box-sizing: border-box;
78 | padding: 0;
79 | margin: 0;
80 | }
81 |
82 | html,
83 | body {
84 | max-width: 100vw;
85 | overflow-x: hidden;
86 | }
87 |
88 | body {
89 | color: rgb(var(--foreground-rgb));
90 | background: linear-gradient(
91 | to bottom,
92 | transparent,
93 | rgb(var(--background-end-rgb))
94 | )
95 | rgb(var(--background-start-rgb));
96 | }
97 |
98 | a {
99 | color: inherit;
100 | text-decoration: none;
101 | }
102 |
103 | @media (prefers-color-scheme: dark) {
104 | html {
105 | color-scheme: dark;
106 | }
107 | }
108 |
109 | .App {
110 | text-align: center;
111 | }
112 |
113 | .App-logo {
114 | height: 10vmin;
115 | pointer-events: none;
116 | }
117 |
118 | .App-header {
119 | background-color: #282c34;
120 | min-height: 100vh;
121 | display: flex;
122 | flex-direction: column;
123 | align-items: center;
124 | justify-content: center;
125 | font-size: calc(10px + 2vmin);
126 | color: white;
127 | }
128 |
129 |
130 | .lang-container {
131 | display: flex;
132 | }
133 |
134 | .lang-container button {
135 | margin: 1rem;
136 | background: rgb(164, 13, 13);
137 | color: white;
138 | border-radius: 1rem;
139 | box-shadow: none;
140 | border: 0;
141 | padding: 1rem;
142 | text-transform: uppercase;
143 | cursor: pointer;
144 | }
145 |
--------------------------------------------------------------------------------
/examples/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": [
19 | "./src/*"
20 | ]
21 | },
22 | "lib": [
23 | "dom",
24 | "dom.iterable",
25 | "esnext"
26 | ]
27 | },
28 | "include": [
29 | "next-env.d.ts",
30 | "**/*.ts",
31 | "**/*.tsx"
32 | ],
33 | "exclude": [
34 | "node_modules"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@lingui/swc-plugin",
3 | "version": "5.5.2",
4 | "description": "A SWC Plugin for LinguiJS",
5 | "author": {
6 | "name": "Timofei Iatsenko",
7 | "email": "timiatsenko@gmail.com"
8 | },
9 | "repository": "lingui/swc-plugin",
10 | "bugs": "https://github.com/lingui/swc-plugin/issues",
11 | "license": "MIT",
12 | "keywords": [
13 | "swc-plugin",
14 | "swc",
15 | "nextjs",
16 | "lingui",
17 | "lingui-js",
18 | "icu",
19 | "message-format",
20 | "i18n",
21 | "internalization"
22 | ],
23 | "main": "target/wasm32-wasip1/release/lingui_macro_plugin.wasm",
24 | "exports": {
25 | ".": "./target/wasm32-wasip1/release/lingui_macro_plugin.wasm"
26 | },
27 | "scripts": {
28 | "prepublishOnly": "cargo build-wasi --release"
29 | },
30 | "files": [],
31 | "peerDependencies": {
32 | "@lingui/core": "5"
33 | },
34 | "peerDependenciesMeta": {
35 | "@swc/core": {
36 | "optional": true
37 | },
38 | "next": {
39 | "optional": true
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.85"
3 | targets = ["wasm32-wasip1"]
4 |
--------------------------------------------------------------------------------
/src/ast_utils.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 | use swc_core::common::DUMMY_SP;
3 | use swc_core::ecma::ast::*;
4 | use swc_core::ecma::atoms::Atom;
5 | use swc_core::ecma::utils::quote_ident;
6 |
7 | pub fn get_jsx_attr<'a>(el: &'a JSXOpeningElement, name: &str) -> Option<&'a JSXAttr> {
8 | for attr in &el.attrs {
9 | if let JSXAttrOrSpread::JSXAttr(attr) = &attr {
10 | if let JSXAttrName::Ident(ident) = &attr.name {
11 | if (&ident.sym) == name {
12 | return Some(attr);
13 | }
14 | }
15 | }
16 | }
17 |
18 | return None;
19 | }
20 |
21 | // get_local_ident_from_object_pat_prop(prop, "t")
22 | // const {t} = useLingui() // => Ident("t")
23 | // const {t: _} = useLingui() // => Ident("_")
24 | pub fn get_local_ident_from_object_pat_prop(
25 | prop: &ObjectPatProp,
26 | imported_symbol: &str,
27 | ) -> Option {
28 | return match prop {
29 | ObjectPatProp::KeyValue(key_value)
30 | if key_value
31 | .key
32 | .as_ident()
33 | .is_some_and(|ident| ident.sym == imported_symbol.to_string()) =>
34 | {
35 | Some(key_value.value.as_ident().unwrap().clone())
36 | }
37 | ObjectPatProp::Assign(assign) if assign.key.sym == imported_symbol.to_string() => {
38 | Some(assign.key.clone())
39 | }
40 | _ => None,
41 | };
42 | }
43 |
44 | pub fn get_jsx_attr_value_as_string(val: &JSXAttrValue) -> Option {
45 | match val {
46 | // offset="5"
47 | JSXAttrValue::Lit(Lit::Str(Str { value, .. })) => {
48 | return Some(value.to_string());
49 | }
50 | // offset={..}
51 | JSXAttrValue::JSXExprContainer(JSXExprContainer {
52 | expr: JSXExpr::Expr(expr),
53 | ..
54 | }) => {
55 | match expr.as_ref() {
56 | // offset={"5"}
57 | Expr::Lit(Lit::Str(Str { value, .. })) => {
58 | return Some(value.to_string());
59 | }
60 | // offset={5}
61 | Expr::Lit(Lit::Num(Number { value, .. })) => {
62 | return Some(value.to_string());
63 | }
64 | _ => None,
65 | }
66 | }
67 | _ => None,
68 | }
69 | }
70 |
71 | pub fn get_expr_as_string(val: &Box) -> Option {
72 | match val.as_ref() {
73 | // "Hello"
74 | Expr::Lit(Lit::Str(Str { value, .. })) => {
75 | return Some(value.to_string());
76 | }
77 |
78 | // `Hello`
79 | Expr::Tpl(Tpl { quasis, .. }) => {
80 | if quasis.len() == 1 {
81 | return Some(quasis.get(0).unwrap().raw.to_string());
82 | } else {
83 | None
84 | }
85 | }
86 |
87 | _ => None,
88 | }
89 | }
90 |
91 | pub fn pick_jsx_attrs(
92 | mut attrs: Vec,
93 | names: HashSet<&str>,
94 | ) -> Vec {
95 | attrs.retain(|attr| {
96 | if let JSXAttrOrSpread::JSXAttr(attr) = attr {
97 | if let JSXAttrName::Ident(ident) = &attr.name {
98 | let name: &str = &ident.sym.to_string();
99 | if let Some(_) = names.get(name) {
100 | return true;
101 | }
102 | }
103 | }
104 | return false;
105 | });
106 |
107 | attrs
108 | }
109 |
110 | pub fn create_jsx_attribute(name: &str, exp: Box) -> JSXAttrOrSpread {
111 | JSXAttrOrSpread::JSXAttr(JSXAttr {
112 | span: DUMMY_SP,
113 | name: JSXAttrName::Ident(IdentName::new(name.into(), DUMMY_SP)),
114 | value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
115 | span: DUMMY_SP,
116 | expr: JSXExpr::Expr(exp),
117 | })),
118 | })
119 | }
120 |
121 | pub fn match_callee_name bool>(call: &CallExpr, predicate: F) -> Option<&Ident> {
122 | if let Callee::Expr(expr) = &call.callee {
123 | if let Expr::Ident(ident) = expr.as_ref() {
124 | if predicate(&ident) {
125 | return Some(ident);
126 | }
127 | }
128 | }
129 |
130 | None
131 | }
132 |
133 | pub fn to_key_value_prop(prop_or_spread: &PropOrSpread) -> Option<&KeyValueProp> {
134 | if let PropOrSpread::Prop(prop) = prop_or_spread {
135 | if let Prop::KeyValue(prop) = prop.as_ref() {
136 | return Some(prop);
137 | }
138 | }
139 |
140 | None
141 | }
142 |
143 | pub fn get_object_prop<'a>(props: &'a Vec, name: &str) -> Option<&'a KeyValueProp> {
144 | props
145 | .iter()
146 | .filter_map(|prop_or_spread| to_key_value_prop(prop_or_spread))
147 | .find(|prop| {
148 | get_prop_key(prop)
149 | .and_then(|key| if key == name { Some(key) } else { None })
150 | .is_some()
151 | })
152 | }
153 |
154 | pub fn get_prop_key(prop: &KeyValueProp) -> Option<&Atom> {
155 | match &prop.key {
156 | PropName::Ident(IdentName { sym, .. }) | PropName::Str(Str { value: sym, .. }) => Some(sym),
157 | _ => None,
158 | }
159 | }
160 |
161 | // recursively expands TypeScript's as expressions until it reaches a real value
162 | pub fn expand_ts_as_expr(mut expr: Box) -> Box {
163 | while let Expr::TsAs(TsAsExpr {
164 | expr: inner_expr, ..
165 | }) = *expr
166 | {
167 | expr = inner_expr;
168 | }
169 | expr
170 | }
171 |
172 | pub fn create_key_value_prop(key: &str, value: Box) -> PropOrSpread {
173 | return PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
174 | key: PropName::Ident(quote_ident!(key)),
175 | value,
176 | })));
177 | }
178 |
179 | pub fn create_import(source: Atom, imported: IdentName, local: IdentName) -> ModuleItem {
180 | ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
181 | span: DUMMY_SP,
182 | phase: ImportPhase::default(),
183 | specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
184 | span: DUMMY_SP,
185 | local: local.into(),
186 | imported: Some(ModuleExportName::Ident(imported.into())),
187 | is_type_only: false,
188 | })],
189 | src: Box::new(Str {
190 | span: DUMMY_SP,
191 | value: source,
192 | raw: None,
193 | }),
194 | with: None,
195 | type_only: false,
196 | }))
197 | }
198 |
--------------------------------------------------------------------------------
/src/builder.rs:
--------------------------------------------------------------------------------
1 | use crate::ast_utils::expand_ts_as_expr;
2 | use crate::tokens::{CaseOrOffset, IcuChoice, MsgToken};
3 | use std::collections::HashSet;
4 | use swc_core::{
5 | common::{SyntaxContext, DUMMY_SP},
6 | ecma::ast::*,
7 | };
8 |
9 | fn dedup_values(mut v: Vec) -> Vec {
10 | let mut uniques = HashSet::new();
11 | v.retain(|e| uniques.insert(e.placeholder.clone()));
12 |
13 | v
14 | }
15 |
16 | pub struct ValueWithPlaceholder {
17 | pub placeholder: String,
18 | pub value: Box,
19 | }
20 |
21 | impl ValueWithPlaceholder {
22 | pub fn to_prop(self) -> PropOrSpread {
23 | let ident = IdentName::new(self.placeholder.into(), DUMMY_SP);
24 |
25 | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
26 | key: PropName::Ident(ident),
27 | value: self.value,
28 | })))
29 | }
30 | }
31 |
32 | pub struct MessageBuilderResult {
33 | pub message_str: String,
34 | pub message: Box,
35 | pub values: Option>,
36 | pub components: Option>,
37 | }
38 |
39 | pub struct MessageBuilder {
40 | message: String,
41 |
42 | components_stack: Vec,
43 | components: Vec,
44 |
45 | values: Vec,
46 | values_indexed: Vec,
47 | }
48 |
49 | impl MessageBuilder {
50 | pub fn parse(tokens: Vec) -> MessageBuilderResult {
51 | let mut builder = MessageBuilder {
52 | message: String::new(),
53 | components_stack: Vec::new(),
54 | components: Vec::new(),
55 | values: Vec::new(),
56 | values_indexed: Vec::new(),
57 | };
58 |
59 | builder.from_tokens(tokens);
60 | builder.to_args()
61 | }
62 |
63 | pub fn to_args(mut self) -> MessageBuilderResult {
64 | let message_str = self.message;
65 |
66 | let message = Box::new(Expr::Lit(Lit::Str(Str {
67 | span: DUMMY_SP,
68 | value: message_str.clone().into(),
69 | raw: None,
70 | })));
71 |
72 | self.values.append(&mut self.values_indexed);
73 |
74 | let values = if self.values.len() > 0 {
75 | Some(Box::new(Expr::Object(ObjectLit {
76 | span: DUMMY_SP,
77 | props: dedup_values(self.values)
78 | .into_iter()
79 | .map(|item| item.to_prop())
80 | .collect(),
81 | })))
82 | } else {
83 | None
84 | };
85 |
86 | let components = if self.components.len() > 0 {
87 | Some(Box::new(Expr::Object(ObjectLit {
88 | span: DUMMY_SP,
89 | props: self
90 | .components
91 | .into_iter()
92 | .map(|item| item.to_prop())
93 | .collect(),
94 | })))
95 | } else {
96 | None
97 | };
98 |
99 | MessageBuilderResult {
100 | message_str: message_str.to_string(),
101 | message,
102 | values,
103 | components,
104 | }
105 | }
106 |
107 | fn from_tokens(&mut self, tokens: Vec) {
108 | for token in tokens {
109 | match token {
110 | MsgToken::String(str) => {
111 | self.push_msg(&str);
112 | }
113 |
114 | MsgToken::Expression(val) => {
115 | let placeholder = self.push_exp(val);
116 | self.push_msg(&format!("{{{placeholder}}}"));
117 | }
118 |
119 | MsgToken::TagOpening(val) => {
120 | self.push_tag_opening(val.el, val.self_closing);
121 | }
122 | MsgToken::TagClosing => {
123 | self.push_tag_closing();
124 | }
125 | MsgToken::IcuChoice(icu) => {
126 | self.push_icu(icu);
127 | }
128 | }
129 | }
130 | }
131 |
132 | fn push_msg(&mut self, val: &str) {
133 | self.message.push_str(val);
134 | }
135 |
136 | fn push_tag_opening(&mut self, el: JSXOpeningElement, self_closing: bool) {
137 | let current = self.components.len();
138 | if self_closing {
139 | self.push_msg(&format!("<{current}/>"));
140 | } else {
141 | self.components_stack.push(current);
142 | self.push_msg(&format!("<{current}>"));
143 | }
144 |
145 | // todo: it looks very dirty and bad to cloning this jsx values
146 | self.components.push(ValueWithPlaceholder {
147 | placeholder: self.components.len().to_string(),
148 | value: Box::new(Expr::JSXElement(Box::new(JSXElement {
149 | opening: el,
150 | closing: None,
151 | children: vec![],
152 | span: DUMMY_SP,
153 | }))),
154 | });
155 | }
156 |
157 | fn push_tag_closing(&mut self) {
158 | if let Some(index) = self.components_stack.pop() {
159 | self.push_msg(&format!("{index}>"));
160 | } else {
161 | // todo JSX tags mismatch. write tests for tags mismatch, swc should not crash in that case
162 | }
163 | }
164 |
165 | fn push_exp(&mut self, mut exp: Box) -> String {
166 | exp = expand_ts_as_expr(exp);
167 |
168 | match exp.as_ref() {
169 | Expr::Ident(ident) => {
170 | self.values.push(ValueWithPlaceholder {
171 | placeholder: ident.sym.to_string().clone(),
172 | value: exp.clone(),
173 | });
174 |
175 | ident.sym.to_string()
176 | }
177 | Expr::Object(object) => {
178 | if let Some(PropOrSpread::Prop(prop)) = object.props.first() {
179 | // {foo}
180 | if let Some(short) = prop.as_shorthand() {
181 | self.values_indexed.push(ValueWithPlaceholder {
182 | placeholder: short.sym.to_string(),
183 | value: Box::new(Expr::Ident(Ident {
184 | span: DUMMY_SP,
185 | sym: short.sym.clone(),
186 | ctxt: SyntaxContext::empty(),
187 | optional: false,
188 | })),
189 | });
190 |
191 | return short.sym.to_string();
192 | }
193 | // {foo: bar}
194 | if let Prop::KeyValue(kv) = prop.as_ref() {
195 | if let PropName::Ident(ident) = &kv.key {
196 | self.values_indexed.push(ValueWithPlaceholder {
197 | placeholder: ident.sym.to_string(),
198 | value: kv.value.clone(),
199 | });
200 |
201 | return ident.sym.to_string();
202 | }
203 | }
204 | }
205 |
206 | // fallback for {...spread} or {}
207 | let index = self.values_indexed.len().to_string();
208 |
209 | self.values_indexed.push(ValueWithPlaceholder {
210 | placeholder: index.clone(),
211 | value: exp.clone(),
212 | });
213 |
214 | index
215 | }
216 | _ => {
217 | let index = self.values_indexed.len().to_string();
218 |
219 | self.values_indexed.push(ValueWithPlaceholder {
220 | placeholder: index.clone(),
221 | value: exp.clone(),
222 | });
223 |
224 | index
225 | }
226 | }
227 | }
228 |
229 | fn push_icu(&mut self, icu: IcuChoice) {
230 | let value_placeholder = self.push_exp(icu.value);
231 | let method = icu.format;
232 | self.push_msg(&format!("{{{value_placeholder}, {method},"));
233 |
234 | for choice in icu.cases {
235 | match choice {
236 | // produce offset:{number}
237 | CaseOrOffset::Offset(val) => {
238 | self.push_msg(&format!(" offset:{val}"));
239 | }
240 | CaseOrOffset::Case(choice) => {
241 | let key = choice.key;
242 |
243 | self.push_msg(&format!(" {key} {{"));
244 | self.from_tokens(choice.tokens);
245 | self.push_msg("}");
246 | }
247 | }
248 | }
249 |
250 | self.push_msg("}");
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/src/generate_id.rs:
--------------------------------------------------------------------------------
1 | use data_encoding::BASE64;
2 | use sha2::{Digest, Sha256};
3 |
4 | const UNIT_SEPARATOR: &char = &'\u{001F}';
5 |
6 | pub fn generate_message_id(message: &str, context: &str) -> String {
7 | let mut hasher = Sha256::new();
8 | hasher.update(format!("{message}{UNIT_SEPARATOR}{context}"));
9 |
10 | let result = hasher.finalize();
11 | return BASE64.encode(result.as_ref())[0..6].into();
12 | }
13 |
14 | #[cfg(test)]
15 | mod tests {
16 | use super::*;
17 |
18 | #[test]
19 | fn test_generate_message_id() {
20 | assert_eq!(generate_message_id("my message", ""), "vQhkQx")
21 | }
22 |
23 | #[test]
24 | fn test_generate_message_id_with_context() {
25 | assert_eq!(
26 | generate_message_id("my message", "custom context"),
27 | "gGUeZH"
28 | )
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/js_macro_folder.rs:
--------------------------------------------------------------------------------
1 | use crate::ast_utils::*;
2 | use crate::builder::MessageBuilder;
3 | use crate::generate_id::generate_message_id;
4 | use crate::macro_utils::*;
5 | use crate::tokens::MsgToken;
6 | use swc_core::common::SyntaxContext;
7 | use swc_core::{
8 | common::DUMMY_SP,
9 | ecma::{
10 | ast::*,
11 | utils::ExprFactory,
12 | visit::{Fold, FoldWith},
13 | },
14 | };
15 |
16 | pub struct JsMacroFolder<'a> {
17 | pub ctx: &'a mut MacroCtx,
18 | }
19 |
20 | impl<'a> JsMacroFolder<'a> {
21 | pub fn new(ctx: &'a mut MacroCtx) -> JsMacroFolder<'a> {
22 | JsMacroFolder { ctx }
23 | }
24 |
25 | fn create_message_descriptor_from_tokens(&mut self, tokens: Vec) -> Expr {
26 | let parsed = MessageBuilder::parse(tokens);
27 |
28 | let mut props: Vec = vec![create_key_value_prop(
29 | "id",
30 | generate_message_id(&parsed.message_str, "").into(),
31 | )];
32 |
33 | if !self.ctx.options.strip_non_essential_fields {
34 | props.push(create_key_value_prop("message", parsed.message));
35 | }
36 |
37 | if let Some(v) = parsed.values {
38 | props.push(create_key_value_prop("values", v))
39 | }
40 |
41 | let message_descriptor = Expr::Object(ObjectLit {
42 | span: DUMMY_SP,
43 | props,
44 | });
45 |
46 | return message_descriptor;
47 | }
48 |
49 | fn create_i18n_fn_call_from_tokens(
50 | &mut self,
51 | callee_obj: Option>,
52 | tokens: Vec,
53 | ) -> CallExpr {
54 | let message_descriptor = Box::new(self.create_message_descriptor_from_tokens(tokens));
55 | return self.create_i18n_fn_call(callee_obj, vec![message_descriptor.as_arg()]);
56 | }
57 |
58 | fn create_i18n_fn_call(
59 | &mut self,
60 | callee_obj: Option>,
61 | args: Vec,
62 | ) -> CallExpr {
63 | CallExpr {
64 | span: DUMMY_SP,
65 | callee: Expr::Member(MemberExpr {
66 | span: DUMMY_SP,
67 | obj: callee_obj.unwrap_or_else(|| {
68 | self.ctx.should_add_18n_import = true;
69 |
70 | return Box::new(self.ctx.runtime_idents.i18n.clone().into());
71 | }),
72 | prop: MemberProp::Ident(IdentName::new("_".into(), DUMMY_SP)),
73 | })
74 | .as_callee(),
75 | args,
76 | type_args: None,
77 | ctxt: SyntaxContext::empty(),
78 | }
79 | }
80 |
81 | // take {message: "", id: "", ...} object literal, process message and return updated props
82 | fn update_msg_descriptor_props(&self, expr: Box) -> Box {
83 | if let Expr::Object(obj) = *expr {
84 | let id_prop = get_object_prop(&obj.props, "id");
85 |
86 | let context_val = get_object_prop(&obj.props, "context")
87 | .and_then(|prop| get_expr_as_string(&prop.value));
88 |
89 | let message_prop = get_object_prop(&obj.props, "message");
90 |
91 | let mut new_props: Vec = vec![];
92 |
93 | if let Some(prop) = id_prop {
94 | if let Some(value) = get_expr_as_string(&prop.value) {
95 | new_props.push(create_key_value_prop("id", value.into()));
96 | }
97 | }
98 |
99 | if let Some(prop) = message_prop {
100 | let tokens = self
101 | .ctx
102 | .try_tokenize_expr(&prop.value)
103 | .unwrap_or_else(|| Vec::new());
104 |
105 | let parsed = MessageBuilder::parse(tokens);
106 |
107 | if !id_prop.is_some() {
108 | new_props.push(create_key_value_prop(
109 | "id",
110 | generate_message_id(
111 | &parsed.message_str,
112 | &(context_val.unwrap_or_default()),
113 | )
114 | .into(),
115 | ))
116 | }
117 |
118 | if !self.ctx.options.strip_non_essential_fields {
119 | new_props.push(create_key_value_prop("message", parsed.message));
120 | }
121 |
122 | if let Some(v) = parsed.values {
123 | new_props.push(create_key_value_prop("values", v))
124 | }
125 | }
126 |
127 | return Box::new(Expr::Object(ObjectLit {
128 | span: DUMMY_SP,
129 | props: new_props,
130 | }));
131 | }
132 |
133 | expr
134 | }
135 | }
136 |
137 | impl<'a> Fold for JsMacroFolder<'a> {
138 | fn fold_expr(&mut self, expr: Expr) -> Expr {
139 | // t`Message`
140 | if let Expr::TaggedTpl(tagged_tpl) = &expr {
141 | let (is_t, callee) = self.ctx.is_lingui_t_call_expr(&tagged_tpl.tag);
142 |
143 | if is_t {
144 | return Expr::Call(self.create_i18n_fn_call_from_tokens(
145 | callee,
146 | self.ctx.tokenize_tpl(&tagged_tpl.tpl),
147 | ));
148 | }
149 | }
150 |
151 | // defineMessage`Message`
152 | if let Expr::TaggedTpl(tagged_tpl) = &expr {
153 | if let Expr::Ident(ident) = tagged_tpl.tag.as_ref() {
154 | if self.ctx.is_define_message_ident(&ident) {
155 | let tokens = self.ctx.tokenize_tpl(&tagged_tpl.tpl);
156 | return self.create_message_descriptor_from_tokens(tokens);
157 | }
158 | }
159 | }
160 |
161 | // defineMessage({message: "Message"})
162 | if let Expr::Call(call) = &expr {
163 | if let Some(_) = match_callee_name(&call, |n| self.ctx.is_define_message_ident(n)) {
164 | if call.args.len() == 1 {
165 | let descriptor = self.update_msg_descriptor_props(
166 | call.args.clone().into_iter().next().unwrap().expr,
167 | );
168 |
169 | return *descriptor;
170 | }
171 | }
172 | }
173 |
174 | expr.fold_children_with(self)
175 | }
176 |
177 | fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr {
178 | // t({}) / t(i18n)({})
179 | if let Callee::Expr(callee) = &expr.callee {
180 | let (is_t, callee) = self.ctx.is_lingui_t_call_expr(callee);
181 |
182 | if is_t && expr.args.len() == 1 {
183 | let descriptor =
184 | self.update_msg_descriptor_props(expr.args.into_iter().next().unwrap().expr);
185 |
186 | return self.create_i18n_fn_call(callee, vec![descriptor.as_arg()]);
187 | }
188 | }
189 |
190 | // plural / selectOrdinal / select
191 | if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(&expr) {
192 | return self.create_i18n_fn_call_from_tokens(None, tokens);
193 | }
194 |
195 | expr.fold_children_with(self)
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/jsx_visitor.rs:
--------------------------------------------------------------------------------
1 | use crate::ast_utils::{get_jsx_attr, get_jsx_attr_value_as_string};
2 | use crate::macro_utils::MacroCtx;
3 | use crate::tokens::{CaseOrOffset, ChoiceCase, IcuChoice, MsgToken, TagOpening};
4 | use once_cell::sync::Lazy;
5 | use regex::Regex;
6 | use swc_core::common::DUMMY_SP;
7 | use swc_core::ecma::ast::*;
8 | use swc_core::ecma::atoms::Atom;
9 | use swc_core::ecma::visit::{Visit, VisitWith};
10 | use swc_core::plugin::errors::HANDLER;
11 |
12 | pub struct TransJSXVisitor<'a> {
13 | pub tokens: Vec,
14 | ctx: &'a MacroCtx,
15 | }
16 |
17 | impl<'a> TransJSXVisitor<'a> {
18 | pub fn new(ctx: &'a MacroCtx) -> TransJSXVisitor<'a> {
19 | TransJSXVisitor {
20 | tokens: Vec::new(),
21 | ctx,
22 | }
23 | }
24 | }
25 |
26 | static PLURAL_OPTIONS_WHITELIST: Lazy =
27 | Lazy::new(|| Regex::new(r"(_[\d\w]+|zero|one|two|few|many|other)").unwrap());
28 | static NUM_OPTION: Lazy = Lazy::new(|| Regex::new(r"_(\d+)").unwrap());
29 | static WORD_OPTION: Lazy = Lazy::new(|| Regex::new(r"_(\w+)").unwrap());
30 |
31 | // const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/
32 | // const jsx2icuExactChoice = (value: string) => value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1")
33 |
34 | static TRIM_START: Lazy = Lazy::new(|| Regex::new(r"^[ ]+").unwrap());
35 | static TRIM_END: Lazy = Lazy::new(|| Regex::new(r"[ ]+$").unwrap());
36 |
37 | // taken from babel repo -> packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
38 | fn clean_jsx_element_literal_child(value: &str) -> String {
39 | let lines: Vec<&str> = value.split('\n').collect();
40 | let mut last_non_empty_line = 0;
41 |
42 | let re_non_space = Regex::new(r"[^\t ]").unwrap();
43 |
44 | for (i, line) in lines.iter().enumerate() {
45 | if re_non_space.is_match(line) {
46 | last_non_empty_line = i;
47 | }
48 | }
49 |
50 | let mut result = String::new();
51 |
52 | for (i, line) in lines.iter().enumerate() {
53 | let is_first_line = i == 0;
54 | let is_last_line = i == lines.len() - 1;
55 | let is_last_non_empty_line = i == last_non_empty_line;
56 |
57 | // replace rendered whitespace tabs with spaces
58 | let mut trimmed_line = line.replace("\t", " ");
59 |
60 | // trim whitespace touching a newline
61 | if !is_first_line {
62 | trimmed_line = TRIM_START.replace(&trimmed_line, "").to_string();
63 | }
64 |
65 | // trim whitespace touching an endline
66 | if !is_last_line {
67 | trimmed_line = TRIM_END.replace(&trimmed_line, "").to_string();
68 | }
69 |
70 | if !trimmed_line.is_empty() {
71 | if !is_last_non_empty_line {
72 | trimmed_line.push(' ');
73 | }
74 |
75 | result.push_str(&trimmed_line);
76 | }
77 | }
78 |
79 | result
80 | }
81 |
82 | fn is_allowed_plural_option(key: &str) -> Option {
83 | if PLURAL_OPTIONS_WHITELIST.is_match(key) {
84 | let key = NUM_OPTION.replace(key, "=$1");
85 | let key = WORD_OPTION.replace(&key, "$1");
86 |
87 | return Some(key.to_string().into());
88 | }
89 |
90 | None
91 | }
92 |
93 | impl<'a> TransJSXVisitor<'a> {
94 | //
95 | fn visit_icu_macro(&mut self, el: &JSXOpeningElement, icu_format: &str) -> Vec {
96 | let mut choices: Vec = Vec::new();
97 |
98 | for attr in &el.attrs {
99 | if let JSXAttrOrSpread::JSXAttr(attr) = attr {
100 | if let Some(attr_value) = &attr.value {
101 | if let JSXAttrName::Ident(ident) = &attr.name {
102 | if &ident.sym == "offset" && icu_format != "select" {
103 | if let Some(value) = get_jsx_attr_value_as_string(attr_value) {
104 | choices.push(CaseOrOffset::Offset(value.to_string()))
105 | } else {
106 | // todo: panic offset might be only a number, other forms are not supported
107 | }
108 | } else if let Some(key) = is_allowed_plural_option(&ident.sym) {
109 | let mut tokens: Vec = Vec::new();
110 |
111 | match attr_value {
112 | // some="# books"
113 | JSXAttrValue::Lit(Lit::Str(str)) => {
114 | let string: String = str.value.clone().to_string();
115 | tokens.push(MsgToken::String(string));
116 | }
117 |
118 | JSXAttrValue::JSXExprContainer(JSXExprContainer {
119 | expr: JSXExpr::Expr(exp),
120 | ..
121 | }) => {
122 | match exp.as_ref() {
123 | // some={"# books"}
124 | Expr::Lit(Lit::Str(str)) => tokens
125 | .push(MsgToken::String(str.value.clone().to_string())),
126 | // some={`# books ${name}`}
127 | Expr::Tpl(tpl) => {
128 | tokens.extend(self.ctx.tokenize_tpl(tpl));
129 | }
130 | // some={``}
131 | Expr::JSXElement(exp) => {
132 | let mut visitor = TransJSXVisitor::new(&self.ctx);
133 | exp.visit_children_with(&mut visitor);
134 |
135 | tokens.extend(visitor.tokens)
136 | }
137 |
138 | _ => tokens.push(MsgToken::Expression(exp.clone())),
139 | }
140 | }
141 |
142 | _ => {
143 | // todo unsupported syntax
144 | }
145 | }
146 |
147 | choices.push(CaseOrOffset::Case(ChoiceCase { tokens, key }))
148 | }
149 | }
150 | }
151 | } else {
152 | HANDLER.with(|h| {
153 | h.struct_span_warn(el.span, "Unsupported Syntax")
154 | .note("The spread expression could not be analyzed at compile time. Consider to use static values.")
155 | .emit()
156 | });
157 | }
158 | }
159 |
160 | return choices;
161 | }
162 | }
163 |
164 | impl<'a> Visit for TransJSXVisitor<'a> {
165 | fn visit_jsx_opening_element(&mut self, el: &JSXOpeningElement) {
166 | if let JSXElementName::Ident(ident) = &el.name {
167 | if self.ctx.is_lingui_ident("Trans", &ident) {
168 | el.visit_children_with(self);
169 | return;
170 | }
171 |
172 | if self.ctx.is_lingui_jsx_choice_cmp(&ident) {
173 | let value = match get_jsx_attr(&el, "value").and_then(|attr| attr.value.as_ref()) {
174 | Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
175 | expr: JSXExpr::Expr(exp),
176 | ..
177 | })) => exp.clone(),
178 | _ => Box::new(Expr::Lit(Lit::Null(Null { span: DUMMY_SP }))),
179 | };
180 |
181 | let icu_method = self
182 | .ctx
183 | .get_ident_export_name(ident)
184 | .unwrap()
185 | .to_lowercase();
186 | let choices = self.visit_icu_macro(el, &icu_method);
187 |
188 | self.tokens.push(MsgToken::IcuChoice(IcuChoice {
189 | cases: choices,
190 | format: icu_method.into(),
191 | value,
192 | }));
193 |
194 | return;
195 | }
196 | }
197 |
198 | self.tokens.push(MsgToken::TagOpening(TagOpening {
199 | self_closing: el.self_closing,
200 | el: JSXOpeningElement {
201 | self_closing: true,
202 | name: el.name.clone(),
203 | attrs: el.attrs.clone(),
204 | span: el.span,
205 | type_args: el.type_args.clone(),
206 | },
207 | }));
208 | }
209 |
210 | fn visit_jsx_closing_element(&mut self, _el: &JSXClosingElement) {
211 | self.tokens.push(MsgToken::TagClosing);
212 | }
213 |
214 | fn visit_jsx_text(&mut self, el: &JSXText) {
215 | self.tokens
216 | .push(MsgToken::String(clean_jsx_element_literal_child(
217 | &el.value.to_string(),
218 | )));
219 | }
220 |
221 | fn visit_jsx_expr_container(&mut self, cont: &JSXExprContainer) {
222 | if let JSXExpr::Expr(exp) = &cont.expr {
223 | match exp.as_ref() {
224 | Expr::Lit(Lit::Str(str)) => {
225 | self.tokens.push(MsgToken::String(str.value.to_string()));
226 | }
227 |
228 | // todo write tests and validate
229 | // support calls to js macro inside JSX, but not to t``
230 | Expr::Call(call) => {
231 | if let Some(tokens) = self.ctx.try_tokenize_call_expr_as_choice_cmp(call) {
232 | self.tokens.extend(tokens);
233 | } else if let Some(placeholder) =
234 | self.ctx.try_tokenize_call_expr_as_placeholder_call(call)
235 | {
236 | self.tokens.push(placeholder);
237 | } else {
238 | self.tokens.push(MsgToken::Expression(exp.clone()));
239 | }
240 | }
241 |
242 | Expr::JSXElement(jsx) => {
243 | jsx.visit_children_with(self);
244 | }
245 |
246 | Expr::Tpl(tpl) => {
247 | self.tokens.extend(self.ctx.tokenize_tpl(tpl));
248 | }
249 | _ => {
250 | self.tokens.push(MsgToken::Expression(exp.clone()));
251 | }
252 | }
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 | use swc_core::common::{SyntaxContext, DUMMY_SP};
3 |
4 | use swc_core::ecma::utils::private_ident;
5 | use swc_core::plugin::errors::HANDLER;
6 | use swc_core::{
7 | ecma::{
8 | ast::*,
9 | utils::quote_ident,
10 | visit::{Fold, FoldWith, VisitWith},
11 | },
12 | plugin::{
13 | metadata::TransformPluginMetadataContextKind, plugin_transform,
14 | proxies::TransformPluginProgramMetadata,
15 | },
16 | };
17 |
18 | mod ast_utils;
19 | mod builder;
20 | mod generate_id;
21 | mod js_macro_folder;
22 | mod jsx_visitor;
23 | mod macro_utils;
24 | mod options;
25 | mod tests;
26 | mod tokens;
27 |
28 | use crate::generate_id::*;
29 | use crate::macro_utils::*;
30 | use crate::options::*;
31 | use ast_utils::*;
32 | use builder::*;
33 | use js_macro_folder::JsMacroFolder;
34 | use jsx_visitor::TransJSXVisitor;
35 |
36 | pub struct IdentReplacer {
37 | from: Id,
38 | to: Ident,
39 | }
40 | impl IdentReplacer {
41 | // pub fn new(from: Id, to: Ident) -> {}
42 | }
43 | impl Fold for IdentReplacer {
44 | fn fold_ident(&mut self, n: Ident) -> Ident {
45 | if n.to_id() == self.from {
46 | return self.to.clone();
47 | }
48 |
49 | n
50 | }
51 | }
52 |
53 | #[derive(Default)]
54 | pub struct LinguiMacroFolder {
55 | has_lingui_macro_imports: bool,
56 | ctx: MacroCtx,
57 | }
58 |
59 | impl LinguiMacroFolder {
60 | pub fn new(options: LinguiOptions) -> LinguiMacroFolder {
61 | LinguiMacroFolder {
62 | has_lingui_macro_imports: false,
63 | ctx: MacroCtx::new(options),
64 | }
65 | }
66 |
67 | // Message
68 | //
69 | fn transform_jsx_macro(&mut self, el: JSXElement, is_trans_el: bool) -> JSXElement {
70 | let mut trans_visitor = TransJSXVisitor::new(&self.ctx);
71 |
72 | if is_trans_el {
73 | el.children.visit_children_with(&mut trans_visitor);
74 | } else {
75 | el.visit_children_with(&mut trans_visitor);
76 | }
77 |
78 | let parsed = MessageBuilder::parse(trans_visitor.tokens);
79 | let id_attr = get_jsx_attr(&el.opening, "id");
80 |
81 | let context_attr_val = get_jsx_attr(&el.opening, "context")
82 | .and_then(|attr| attr.value.as_ref())
83 | .and_then(|value| get_jsx_attr_value_as_string(value));
84 |
85 | let mut attrs = vec![create_jsx_attribute("message".into(), parsed.message)];
86 |
87 | if !id_attr.is_some() {
88 | attrs.push(create_jsx_attribute(
89 | "id",
90 | generate_message_id(&parsed.message_str, &context_attr_val.unwrap_or_default())
91 | .into(),
92 | ));
93 | }
94 |
95 | if let Some(exp) = parsed.values {
96 | attrs.push(create_jsx_attribute("values", exp));
97 | }
98 |
99 | if let Some(exp) = parsed.components {
100 | attrs.push(create_jsx_attribute("components", exp));
101 | }
102 |
103 | attrs.extend(pick_jsx_attrs(
104 | el.opening.attrs,
105 | HashSet::from(["id", "component", "render", "i18n"]),
106 | ));
107 |
108 | if self.ctx.options.strip_non_essential_fields {
109 | attrs = pick_jsx_attrs(
110 | attrs,
111 | HashSet::from(["id", "component", "render", "i18n", "values", "components"]),
112 | )
113 | }
114 |
115 | self.ctx.should_add_trans_import = true;
116 |
117 | return JSXElement {
118 | span: el.span,
119 | children: vec![],
120 | closing: None,
121 | opening: JSXOpeningElement {
122 | self_closing: true,
123 | span: el.opening.span,
124 | name: JSXElementName::Ident(self.ctx.runtime_idents.trans.clone().into()),
125 | type_args: None,
126 | attrs,
127 | },
128 | };
129 | }
130 |
131 | pub fn handle_use_lingui(&mut self, n: BlockStmt) -> BlockStmt {
132 | let mut ctx = self.ctx.clone();
133 |
134 | let mut ident_replacer: Option = None;
135 |
136 | let stmts: Vec = n
137 | .stmts
138 | .into_iter()
139 | .map(|stmt| {
140 | return match stmt {
141 | Stmt::Decl(Decl::Var(var_decl)) => {
142 | let decl = *var_decl;
143 |
144 | let underscore_ident = private_ident!("$__");
145 | let decls: Vec = decl.decls.into_iter().map(|declarator| {
146 | if let Some(init) = &declarator.init {
147 | let expr = init.as_ref();
148 |
149 | if let Expr::Call(call) = &expr {
150 | if match_callee_name(call, |n| {
151 | self.ctx.is_lingui_ident("useLingui", n)
152 | })
153 | .is_some()
154 | {
155 | self.ctx.should_add_uselingui_import = true;
156 |
157 | if let Pat::Object(obj_pat) = declarator.name {
158 | let mut new_props: Vec =
159 | obj_pat.props.into_iter().map(|prop| {
160 | return get_local_ident_from_object_pat_prop(&prop, "t")
161 | .and_then(|ident| {
162 | ctx.register_reference(
163 | &"t".into(),
164 | &ident.to_id(),
165 | );
166 |
167 | let new_i18n_ident = private_ident!("$__i18n");
168 |
169 | ident_replacer = Some(IdentReplacer {
170 | from: ident.to_id(),
171 | to: underscore_ident.clone(),
172 | });
173 |
174 | ctx.runtime_idents.i18n = new_i18n_ident.clone().into();
175 |
176 | return Some(ObjectPatProp::KeyValue(
177 | KeyValuePatProp {
178 | value: Box::new(Pat::Ident(new_i18n_ident.into())),
179 | key: PropName::Ident(quote_ident!("i18n")),
180 | },
181 | ))
182 | })
183 | .unwrap_or(prop);
184 | }).collect();
185 |
186 | new_props.push(ObjectPatProp::KeyValue(
187 | KeyValuePatProp {
188 | value: Box::new(Pat::Ident(underscore_ident.clone().into())),
189 | key: PropName::Ident(quote_ident!("_")),
190 | },
191 | ));
192 |
193 | return VarDeclarator {
194 | init: Some(Box::new(Expr::Call(CallExpr {
195 | callee: Callee::Expr(Box::new(Expr::Ident(ctx.runtime_idents.use_lingui.clone().into()))),
196 | ..call.clone()
197 | }))),
198 |
199 | definite: true,
200 | span: declarator.span,
201 | name: Pat::Object(ObjectPat {
202 | optional: false,
203 | type_ann: None,
204 | span: DUMMY_SP,
205 | props: new_props
206 |
207 | }),
208 | }
209 | } else {
210 | HANDLER.with(|h| {
211 | h.struct_span_warn(decl.span, "Unsupported Syntax")
212 | .note(
213 | r#"You have to destructure `t` when using the `useLingui` macro, i.e:
214 | const { t } = useLingui()
215 | or
216 | const { t: _ } = useLingui()"#)
217 | .emit()
218 | });
219 | }
220 | }
221 | }
222 | }
223 |
224 | return declarator;
225 | }).collect();
226 |
227 | return Stmt::Decl(Decl::Var(Box::new(VarDecl {
228 | span: decl.span,
229 | decls,
230 | declare: false,
231 | kind: decl.kind,
232 | ctxt: SyntaxContext::empty()
233 | })))
234 | }
235 | _ => stmt,
236 | };
237 | })
238 | .collect();
239 |
240 | let mut block = BlockStmt {
241 | span: n.span,
242 | stmts,
243 | ctxt: SyntaxContext::empty(),
244 | };
245 |
246 | // use lingui matched above
247 | if ident_replacer.is_some() {
248 | block = block
249 | .fold_children_with(&mut JsMacroFolder::new(&mut ctx))
250 | // replace other
251 | .fold_children_with(&mut ident_replacer.unwrap());
252 | }
253 |
254 | return block.fold_children_with(self);
255 | }
256 | }
257 |
258 | impl<'a> Fold for LinguiMacroFolder {
259 | fn fold_module_items(&mut self, mut n: Vec) -> Vec {
260 | let (i18n_source, i18n_export) = self.ctx.options.runtime_modules.i18n.clone();
261 | let (trans_source, trans_export) = self.ctx.options.runtime_modules.trans.clone();
262 | let (use_lingui_source, use_lingui_export) =
263 | self.ctx.options.runtime_modules.use_lingui.clone();
264 |
265 | let mut insert_index: usize = 0;
266 | let mut index = 0;
267 |
268 | n.retain(|m| {
269 | if let ModuleItem::ModuleDecl(ModuleDecl::Import(imp)) = m {
270 | // drop macro imports
271 | if &imp.src.value == "@lingui/macro"
272 | || &imp.src.value == "@lingui/core/macro"
273 | || &imp.src.value == "@lingui/react/macro"
274 | {
275 | self.has_lingui_macro_imports = true;
276 | self.ctx.register_macro_import(imp);
277 | insert_index = index;
278 | return false;
279 | }
280 | }
281 |
282 | index += 1;
283 | true
284 | });
285 |
286 | n = n.fold_children_with(self);
287 |
288 | if self.ctx.should_add_18n_import {
289 | n.insert(
290 | insert_index,
291 | create_import(
292 | i18n_source.into(),
293 | quote_ident!(i18n_export[..]),
294 | self.ctx.runtime_idents.i18n.clone().into(),
295 | ),
296 | );
297 | }
298 |
299 | if self.ctx.should_add_trans_import {
300 | n.insert(
301 | insert_index,
302 | create_import(
303 | trans_source.into(),
304 | quote_ident!(trans_export[..]),
305 | self.ctx.runtime_idents.trans.clone(),
306 | ),
307 | );
308 | }
309 |
310 | if self.ctx.should_add_uselingui_import {
311 | n.insert(
312 | insert_index,
313 | create_import(
314 | use_lingui_source.into(),
315 | quote_ident!(use_lingui_export[..]),
316 | self.ctx.runtime_idents.use_lingui.clone(),
317 | ),
318 | );
319 | }
320 |
321 | n
322 | }
323 | fn fold_arrow_expr(&mut self, n: ArrowExpr) -> ArrowExpr {
324 | // If no package that we care about is imported, skip the following
325 | // transformation logic.
326 | if !self.has_lingui_macro_imports {
327 | return n;
328 | }
329 |
330 | let mut func = n;
331 |
332 | if func.body.is_block_stmt() {
333 | let block = func.body.block_stmt().unwrap();
334 |
335 | func = ArrowExpr {
336 | body: Box::new(BlockStmtOrExpr::BlockStmt(self.handle_use_lingui(block))),
337 | ..func
338 | }
339 | }
340 |
341 | func.fold_children_with(self)
342 | }
343 |
344 | fn fold_function(&mut self, n: Function) -> Function {
345 | // If no package that we care about is imported, skip the following
346 | // transformation logic.
347 | if !self.has_lingui_macro_imports {
348 | return n;
349 | }
350 |
351 | let mut func = n;
352 |
353 | if let Some(body) = func.body {
354 | func = Function {
355 | body: Some(self.handle_use_lingui(body)),
356 | ..func
357 | };
358 | }
359 |
360 | func.fold_children_with(self)
361 | }
362 |
363 | fn fold_expr(&mut self, expr: Expr) -> Expr {
364 | // If no package that we care about is imported, skip the following
365 | // transformation logic.
366 | if !self.has_lingui_macro_imports {
367 | return expr;
368 | }
369 |
370 | if let Expr::Arrow(arrow_expr) = expr {
371 | return Expr::Arrow(self.fold_arrow_expr(arrow_expr));
372 | }
373 |
374 | let mut folder = JsMacroFolder::new(&mut self.ctx);
375 |
376 | folder.fold_expr(expr).fold_children_with(self)
377 | }
378 |
379 | fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr {
380 | // If no package that we care about is imported, skip the following
381 | // transformation logic.
382 | if !self.has_lingui_macro_imports {
383 | return expr;
384 | }
385 |
386 | let mut folder = JsMacroFolder::new(&mut self.ctx);
387 |
388 | folder.fold_call_expr(expr).fold_children_with(self)
389 | }
390 |
391 | fn fold_jsx_element(&mut self, mut el: JSXElement) -> JSXElement {
392 | // If no package that we care about is imported, skip the following
393 | // transformation logic.
394 | if !self.has_lingui_macro_imports {
395 | return el;
396 | }
397 |
398 | // apply JS Macro transformations to jsx elements
399 | // before they will be extracted as message components
400 | el = el.fold_with(&mut JsMacroFolder::new(&mut self.ctx));
401 |
402 | if let JSXElementName::Ident(ident) = &el.opening.name {
403 | if self.ctx.is_lingui_ident("Trans", &ident) {
404 | return self.transform_jsx_macro(el, true);
405 | }
406 |
407 | if self.ctx.is_lingui_jsx_choice_cmp(&ident) {
408 | return self.transform_jsx_macro(el, false);
409 | }
410 | }
411 |
412 | el.fold_children_with(self)
413 | }
414 | }
415 |
416 | #[plugin_transform]
417 | pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program {
418 | let config = serde_json::from_str::(
419 | &metadata
420 | .get_transform_plugin_config()
421 | .expect("failed to get plugin config for lingui-plugin"),
422 | )
423 | .expect("invalid config for lingui-plugin");
424 |
425 | let config = config.to_options(
426 | &metadata
427 | .get_context(&TransformPluginMetadataContextKind::Env)
428 | .unwrap_or_default(),
429 | );
430 |
431 | program.fold_with(&mut LinguiMacroFolder::new(config))
432 | }
433 |
--------------------------------------------------------------------------------
/src/macro_utils.rs:
--------------------------------------------------------------------------------
1 | use crate::ast_utils::*;
2 | use crate::tokens::*;
3 | use crate::LinguiOptions;
4 | use std::collections::{HashMap, HashSet};
5 | use swc_core::ecma::utils::quote_ident;
6 | use swc_core::ecma::{ast::*, atoms::Atom};
7 |
8 | const LINGUI_T: &str = &"t";
9 |
10 | #[derive(Default, Clone)]
11 | pub struct MacroCtx {
12 | // export name -> local name
13 | symbol_to_id_map: HashMap>,
14 | // local name -> export name
15 | id_to_symbol_map: HashMap,
16 |
17 | pub should_add_18n_import: bool,
18 | pub should_add_trans_import: bool,
19 | pub should_add_uselingui_import: bool,
20 |
21 | pub options: LinguiOptions,
22 | pub runtime_idents: RuntimeIdents,
23 | }
24 |
25 | #[derive(Clone)]
26 | pub struct RuntimeIdents {
27 | pub i18n: Ident,
28 | pub trans: IdentName,
29 | pub use_lingui: IdentName,
30 | }
31 |
32 | impl Default for RuntimeIdents {
33 | fn default() -> RuntimeIdents {
34 | RuntimeIdents {
35 | i18n: quote_ident!("$_i18n").into(),
36 | trans: quote_ident!("Trans_"),
37 | use_lingui: quote_ident!("$_useLingui"),
38 | }
39 | }
40 | }
41 |
42 | impl MacroCtx {
43 | pub fn new(options: LinguiOptions) -> MacroCtx {
44 | MacroCtx {
45 | options,
46 | ..Default::default()
47 | }
48 | }
49 |
50 | /// is given ident exported from @lingui/macro? and one of choice functions?
51 | fn is_lingui_fn_choice_cmp(&self, ident: &Ident) -> bool {
52 | // self.symbol_to_id_map.
53 | self.is_lingui_ident("plural", ident)
54 | || self.is_lingui_ident("select", ident)
55 | || self.is_lingui_ident("selectOrdinal", ident)
56 | }
57 |
58 | pub fn is_lingui_placeholder_expr(&self, ident: &Ident) -> bool {
59 | self.is_lingui_ident("ph", &ident)
60 | }
61 |
62 | /// is given ident exported from @lingui/macro?
63 | pub fn is_lingui_ident(&self, name: &str, ident: &Ident) -> bool {
64 | self.symbol_to_id_map
65 | .get(&name.into())
66 | .and_then(|refs| refs.get(&ident.to_id()))
67 | .is_some()
68 | }
69 |
70 | pub fn is_define_message_ident(&self, ident: &Ident) -> bool {
71 | return self.is_lingui_ident("defineMessage", &ident)
72 | || self.is_lingui_ident("msg", &ident);
73 | }
74 |
75 | /// given import {plural as i18nPlural} from "@lingui/macro";
76 | /// get_ident_export_name("i18nPlural") would return `plural`
77 | pub fn get_ident_export_name(&self, ident: &Ident) -> Option<&Atom> {
78 | if let Some(name) = self.id_to_symbol_map.get(&ident.to_id()) {
79 | return Some(name);
80 | }
81 |
82 | None
83 | }
84 |
85 | pub fn is_lingui_jsx_choice_cmp(&self, ident: &Ident) -> bool {
86 | self.is_lingui_ident("Plural", ident)
87 | || self.is_lingui_ident("Select", ident)
88 | || self.is_lingui_ident("SelectOrdinal", ident)
89 | }
90 |
91 | pub fn register_reference(&mut self, symbol: &Atom, id: &Id) {
92 | self.symbol_to_id_map
93 | .entry(symbol.clone())
94 | .or_default()
95 | .insert(id.clone());
96 |
97 | self.id_to_symbol_map.insert(id.clone(), symbol.clone());
98 | }
99 | pub fn register_macro_import(&mut self, imp: &ImportDecl) {
100 | for spec in &imp.specifiers {
101 | if let ImportSpecifier::Named(spec) = spec {
102 | if let Some(ModuleExportName::Ident(ident)) = &spec.imported {
103 | self.register_reference(&ident.sym, &spec.local.to_id());
104 | } else {
105 | self.register_reference(&spec.local.sym, &spec.local.to_id());
106 | }
107 | }
108 | }
109 | }
110 |
111 | /// Take a callee expression and detect is it a lingui t`` macro call
112 | /// Returns a callee object depending whether custom i18n instance was passed or not
113 | pub fn is_lingui_t_call_expr(&self, callee_expr: &Box) -> (bool, Option>) {
114 | match callee_expr.as_ref() {
115 | // t(i18n)...
116 | Expr::Call(call)
117 | if matches!(
118 | match_callee_name(call, |n| self.is_lingui_ident(LINGUI_T, n)),
119 | Some(_)
120 | ) =>
121 | {
122 | if let Some(v) = call.args.get(0) {
123 | (true, Some(v.expr.clone()))
124 | } else {
125 | (false, None)
126 | }
127 | }
128 | // t..
129 | Expr::Ident(ident) if self.is_lingui_ident(LINGUI_T, &ident) => (true, None),
130 | _ => (false, None),
131 | }
132 | }
133 |
134 | /// Receive TemplateLiteral with variables and return MsgTokens
135 | pub fn tokenize_tpl(&self, tpl: &Tpl) -> Vec {
136 | let mut tokens: Vec = Vec::with_capacity(tpl.quasis.len());
137 |
138 | for (i, tpl_element) in tpl.quasis.iter().enumerate() {
139 | tokens.push(MsgToken::String(
140 | tpl_element
141 | .cooked
142 | .as_ref()
143 | .unwrap_or(&tpl_element.raw)
144 | .to_string(),
145 | ));
146 |
147 | if let Some(exp) = tpl.exprs.get(i) {
148 | if let Expr::Call(call) = exp.as_ref() {
149 | if let Some(call_tokens) = self.try_tokenize_call_expr_as_choice_cmp(call) {
150 | tokens.extend(call_tokens);
151 | continue;
152 | }
153 | if let Some(placeholder) = self.try_tokenize_call_expr_as_placeholder_call(call)
154 | {
155 | tokens.push(placeholder);
156 | continue;
157 | }
158 | }
159 |
160 | tokens.push(MsgToken::Expression(exp.clone()));
161 | }
162 | }
163 |
164 | tokens
165 | }
166 |
167 | /// Try to tokenize call expression as ICU Choice macro
168 | /// Return None if this call is not related to macros or is not parsable
169 | pub fn try_tokenize_call_expr_as_choice_cmp(&self, expr: &CallExpr) -> Option> {
170 | if let Some(ident) = match_callee_name(&expr, |name| self.is_lingui_fn_choice_cmp(name)) {
171 | if expr.args.len() != 2 {
172 | // malformed plural call, exit
173 | return None;
174 | }
175 |
176 | // ICU value
177 | let arg = expr.args.get(0).unwrap();
178 | let icu_value = arg.expr.clone();
179 |
180 | // ICU Choice Cases
181 | let arg = expr.args.get(1).unwrap();
182 | if let Expr::Object(object) = &arg.expr.as_ref() {
183 | let format = self.get_ident_export_name(ident).unwrap().to_lowercase();
184 | let cases = self.get_choice_cases_from_obj(&object.props, &format);
185 |
186 | return Some(vec![MsgToken::IcuChoice(IcuChoice {
187 | format: format.into(),
188 | value: icu_value,
189 | cases,
190 | })]);
191 | } else {
192 | // todo passed not an ObjectLiteral,
193 | // we should panic here or just skip this call
194 | }
195 | }
196 |
197 | return None;
198 | }
199 |
200 | pub fn try_tokenize_call_expr_as_placeholder_call(&self, expr: &CallExpr) -> Option {
201 | if expr.callee.as_expr().is_some_and(|c| {
202 | c.as_ident()
203 | .map_or(false, |i| self.is_lingui_placeholder_expr(i))
204 | }) {
205 | if let Some(first) = expr.args.first() {
206 | return Some(MsgToken::Expression(first.expr.clone()));
207 | }
208 | }
209 |
210 | return None;
211 | }
212 |
213 | pub fn try_tokenize_expr(&self, expr: &Box) -> Option> {
214 | match expr.as_ref() {
215 | // String Literal: "has # friend"
216 | Expr::Lit(Lit::Str(str)) => Some(vec![MsgToken::String(str.clone().value.to_string())]),
217 | // Template Literal: `${name} has # friend`
218 | Expr::Tpl(tpl) => Some(self.tokenize_tpl(tpl)),
219 |
220 | // ParenthesisExpression: ("has # friend")
221 | Expr::Paren(ParenExpr { expr, .. }) => self.try_tokenize_expr(expr),
222 |
223 | // Call Expression: {one: plural(numArticles, {...})}
224 | Expr::Call(expr) => self.try_tokenize_call_expr_as_choice_cmp(expr),
225 | _ => None,
226 | }
227 | }
228 |
229 | /// Take KeyValueProp and return Key as string if parsable
230 | /// If key is numeric, return an exact match syntax `={number}`
231 | pub fn get_js_choice_case_key(&self, prop: &KeyValueProp) -> Option {
232 | match &prop.key {
233 | // {one: ""}
234 | PropName::Ident(IdentName { sym, .. })
235 | // {"one": ""}
236 | | PropName::Str(Str { value: sym, .. }) => {
237 | Some(sym.clone())
238 | }
239 | // {0: ""} -> `={number}`
240 | PropName::Num(Number { value, .. }) => {
241 | Some(format!("={value}").into())
242 | }
243 | _ => {
244 | None
245 | }
246 | }
247 | }
248 |
249 | /// receive ObjectLiteral {few: "..", many: "..", other: ".."} and create tokens
250 | /// If messages passed as TemplateLiterals with variables, it extracts variables
251 | pub fn get_choice_cases_from_obj(
252 | &self,
253 | props: &Vec,
254 | icu_format: &str,
255 | ) -> Vec {
256 | // todo: there might be more props then real choices. Id for example
257 | let mut choices: Vec = Vec::with_capacity(props.len());
258 |
259 | for prop_or_spread in props {
260 | if let PropOrSpread::Prop(prop) = prop_or_spread {
261 | if let Prop::KeyValue(prop) = prop.as_ref() {
262 | if let Some(key) = self.get_js_choice_case_key(prop) {
263 | if &key == "offset" && icu_format != "select" {
264 | if let Expr::Lit(Lit::Num(Number { value, .. })) = prop.value.as_ref() {
265 | choices.push(CaseOrOffset::Offset(value.to_string()))
266 | } else {
267 | // todo: panic offset might be only a number, other forms is not supported
268 | }
269 | } else {
270 | let tokens = self
271 | .try_tokenize_expr(&prop.value)
272 | .unwrap_or(vec![MsgToken::Expression(prop.value.clone())]);
273 |
274 | choices.push(CaseOrOffset::Case(ChoiceCase { tokens, key }));
275 | }
276 | }
277 | } else {
278 | // todo: panic here we could not parse anything else then KeyValue pair
279 | }
280 | } else {
281 | // todo: panic here, we could not parse spread
282 | }
283 | }
284 |
285 | choices
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/src/options.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Deserialize, Debug, PartialEq)]
4 | #[serde(rename_all = "camelCase")]
5 | pub struct LinguiJsOptions {
6 | runtime_modules: Option,
7 | #[serde(default)]
8 | strip_non_essential_fields: Option,
9 | }
10 |
11 | #[derive(Deserialize, Debug, PartialEq)]
12 | struct RuntimeModulesConfig(String, #[serde(default)] Option);
13 |
14 | #[derive(Deserialize, Debug, PartialEq)]
15 | #[serde(rename_all = "camelCase")]
16 | pub struct RuntimeModulesConfigMap {
17 | i18n: Option,
18 | trans: Option,
19 | use_lingui: Option,
20 | }
21 |
22 | #[derive(Debug, Clone)]
23 | pub struct RuntimeModulesConfigMapNormalized {
24 | pub i18n: (String, String),
25 | pub trans: (String, String),
26 | pub use_lingui: (String, String),
27 | }
28 |
29 | impl LinguiJsOptions {
30 | pub fn to_options(self, env_name: &str) -> LinguiOptions {
31 | LinguiOptions {
32 | strip_non_essential_fields: self
33 | .strip_non_essential_fields
34 | .unwrap_or_else(|| matches!(env_name, "production")),
35 | runtime_modules: RuntimeModulesConfigMapNormalized {
36 | i18n: (
37 | self.runtime_modules
38 | .as_ref()
39 | .and_then(|o| o.i18n.as_ref())
40 | .and_then(|o| Some(o.0.clone()))
41 | .unwrap_or("@lingui/core".into()),
42 | self.runtime_modules
43 | .as_ref()
44 | .and_then(|o| o.i18n.as_ref())
45 | .and_then(|o| o.1.clone())
46 | .unwrap_or("i18n".into()),
47 | ),
48 | trans: (
49 | self.runtime_modules
50 | .as_ref()
51 | .and_then(|o| o.trans.as_ref())
52 | .and_then(|o| Some(o.0.clone()))
53 | .unwrap_or("@lingui/react".into()),
54 | self.runtime_modules
55 | .as_ref()
56 | .and_then(|o| o.trans.as_ref())
57 | .and_then(|o| o.1.clone())
58 | .unwrap_or("Trans".into()),
59 | ),
60 | use_lingui: (
61 | self.runtime_modules
62 | .as_ref()
63 | .and_then(|o| o.use_lingui.as_ref())
64 | .and_then(|o| Some(o.0.clone()))
65 | .unwrap_or("@lingui/react".into()),
66 | self.runtime_modules
67 | .as_ref()
68 | .and_then(|o| o.use_lingui.as_ref())
69 | .and_then(|o| o.1.clone())
70 | .unwrap_or("useLingui".into()),
71 | ),
72 | },
73 | }
74 | }
75 | }
76 |
77 | #[derive(Debug, Clone)]
78 | pub struct LinguiOptions {
79 | pub strip_non_essential_fields: bool,
80 | pub runtime_modules: RuntimeModulesConfigMapNormalized,
81 | }
82 |
83 | impl Default for LinguiOptions {
84 | fn default() -> LinguiOptions {
85 | LinguiOptions {
86 | strip_non_essential_fields: false,
87 | runtime_modules: RuntimeModulesConfigMapNormalized {
88 | i18n: ("@lingui/core".into(), "i18n".into()),
89 | trans: ("@lingui/react".into(), "Trans".into()),
90 | use_lingui: ("@lingui/react".into(), "useLingui".into()),
91 | },
92 | }
93 | }
94 | }
95 |
96 | #[cfg(test)]
97 | mod lib_tests {
98 | use super::*;
99 |
100 | #[test]
101 | fn test_config() {
102 | let config = serde_json::from_str::(
103 | r#"{
104 | "runtimeModules": {
105 | "i18n": ["my-core", "myI18n"],
106 | "trans": ["my-react", "myTrans"],
107 | "useLingui": ["my-react", "myUseLingui"]
108 | }
109 | }"#,
110 | )
111 | .expect("invalid config for lingui-plugin");
112 |
113 | assert_eq!(
114 | config,
115 | LinguiJsOptions {
116 | runtime_modules: Some(RuntimeModulesConfigMap {
117 | i18n: Some(RuntimeModulesConfig(
118 | "my-core".into(),
119 | Some("myI18n".into())
120 | )),
121 | trans: Some(RuntimeModulesConfig(
122 | "my-react".into(),
123 | Some("myTrans".into())
124 | )),
125 | use_lingui: Some(RuntimeModulesConfig(
126 | "my-react".into(),
127 | Some("myUseLingui".into())
128 | )),
129 | }),
130 | strip_non_essential_fields: None,
131 | }
132 | )
133 | }
134 |
135 | #[test]
136 | fn test_config_optional() {
137 | let config = serde_json::from_str::(
138 | r#"{
139 | "runtimeModules": {
140 | "i18n": ["@lingui/core"]
141 | }
142 | }"#,
143 | )
144 | .expect("invalid config for lingui-plugin");
145 |
146 | assert_eq!(
147 | config,
148 | LinguiJsOptions {
149 | runtime_modules: Some(RuntimeModulesConfigMap {
150 | i18n: Some(RuntimeModulesConfig("@lingui/core".into(), None)),
151 | trans: None,
152 | use_lingui: None,
153 | }),
154 | strip_non_essential_fields: None,
155 | }
156 | )
157 | }
158 |
159 | #[test]
160 | fn test_strip_non_essential_fields_config() {
161 | let config = serde_json::from_str::(
162 | r#"{
163 | "stripNonEssentialFields": true,
164 | "runtimeModules": {}
165 | }"#,
166 | )
167 | .unwrap();
168 |
169 | let options = config.to_options("development");
170 | assert!(options.strip_non_essential_fields);
171 |
172 | let config = serde_json::from_str::(
173 | r#"{
174 | "stripNonEssentialFields": false,
175 | "runtimeModules": {}
176 | }"#,
177 | )
178 | .unwrap();
179 |
180 | let options = config.to_options("production");
181 | assert!(!options.strip_non_essential_fields);
182 | }
183 |
184 | #[test]
185 | fn test_strip_non_essential_fields_default() {
186 | let config = serde_json::from_str::(
187 | r#"{
188 | "runtimeModules": {}
189 | }"#,
190 | )
191 | .unwrap();
192 |
193 | let options = config.to_options("development");
194 | assert!(!options.strip_non_essential_fields);
195 |
196 | let config = serde_json::from_str::(
197 | r#"{
198 | "runtimeModules": {}
199 | }"#,
200 | )
201 | .unwrap();
202 |
203 | let options = config.to_options("production");
204 | assert!(options.strip_non_essential_fields);
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/tests/common/mod.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! to {
3 | ($name:ident, $from:expr, $to:expr) => {
4 | swc_core::ecma::transforms::testing::test_inline!(
5 | swc_core::ecma::parser::Syntax::Typescript(swc_core::ecma::parser::TsSyntax {
6 | tsx: true,
7 | ..Default::default()
8 | }),
9 | |_| {
10 | (
11 | swc_core::ecma::transforms::base::resolver(
12 | swc_core::common::Mark::new(),
13 | swc_core::common::Mark::new(),
14 | true,
15 | ),
16 | swc_core::ecma::visit::fold_pass($crate::LinguiMacroFolder::default()),
17 | )
18 | },
19 | $name,
20 | $from,
21 | $to
22 | );
23 | };
24 |
25 | (production, $name:ident, $from:expr, $to:expr) => {
26 | swc_core::ecma::transforms::testing::test_inline!(
27 | swc_core::ecma::parser::Syntax::Typescript(swc_core::ecma::parser::TsSyntax {
28 | tsx: true,
29 | ..Default::default()
30 | }),
31 | |_| {
32 | (
33 | swc_core::ecma::transforms::base::resolver(
34 | swc_core::common::Mark::new(),
35 | swc_core::common::Mark::new(),
36 | true,
37 | ),
38 | swc_core::ecma::visit::fold_pass($crate::LinguiMacroFolder::new(
39 | $crate::LinguiOptions {
40 | strip_non_essential_fields: true,
41 | ..Default::default()
42 | },
43 | )),
44 | )
45 | },
46 | $name,
47 | $from,
48 | $to
49 | );
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/src/tests/imports.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | should_add_not_clashing_imports,
5 | r#"
6 | import { t } from "@lingui/core/macro";
7 | import { Plural } from "@lingui/react/macro";
8 | import { i18n } from "@lingui/core";
9 | import { Trans } from "@lingui/react";
10 |
11 | t`Test`;
12 | ;
13 | Untouched
14 | "#,
15 | r#"
16 | import { Trans as Trans_ } from "@lingui/react";
17 | import { i18n as $_i18n } from "@lingui/core";
18 | import { i18n } from "@lingui/core";
19 | import { Trans } from "@lingui/react";
20 |
21 | $_i18n._({
22 | id: "NnH3pK",
23 | message: "Test"
24 | });
25 |
26 | ;
31 | Untouched
32 | "#
33 | );
34 |
35 | to!(
36 | jsx_should_process_only_elements_imported_from_macro,
37 | r#"
38 | import { Plural } from "@lingui/react/macro";
39 | import { Select } from "./my-select-cmp";
40 |
41 | ;
46 |
47 | ;
48 | "#,
49 | r#"
50 | import { Trans as Trans_ } from "@lingui/react";
51 | import { Select } from "./my-select-cmp";
52 |
53 | ;
58 |
59 | ;
60 | "#
61 | );
62 |
63 | to!(
64 | jsx_should_process_only_elements_imported_from_macro2,
65 | r#"
66 | import { Trans } from "@lingui/react";
67 | import { Plural } from "@lingui/react/macro";
68 |
69 | ;
74 |
75 | ;Should be untouched
76 | "#,
77 | r#"
78 | import { Trans } from "@lingui/react";
79 | import { Trans as Trans_ } from "@lingui/react";
80 |
81 | ;
86 | ;Should be untouched
87 | "#
88 | );
89 |
90 | to!(
91 | js_should_process_only_elements_imported_from_macro,
92 | r#"
93 | import { plural } from "@lingui/core/macro";
94 | import { t } from "./custom-t";
95 |
96 | t`Don't touch me!`
97 | plural(value, {one: "...", other: "..."})
98 | "#,
99 | r#"
100 | import { i18n as $_i18n } from "@lingui/core";
101 | import { t } from "./custom-t";
102 |
103 | t`Don't touch me!`
104 | $_i18n._({
105 | id: "kwTAtG",
106 | message: "{value, plural, one {...} other {...}}",
107 | values: {
108 | value: value
109 | }
110 | });
111 |
112 | "#
113 | );
114 |
115 | to!(
116 | js_should_process_only_elements_imported_from_macro2,
117 | r#"
118 | import { t } from "@lingui/core/macro";
119 | import { plural } from "./custom-plural";
120 |
121 | t`Hello World!`;
122 | plural(value, {one: "...", other: "..."});
123 | "#,
124 | r#"
125 | import { i18n as $_i18n } from "@lingui/core";
126 | import { plural } from "./custom-plural";
127 |
128 | $_i18n._({
129 | id: "0IkKj6",
130 | message: "Hello World!"
131 | });
132 |
133 | plural(value, {one: "...", other: "..."});
134 | "#
135 | );
136 |
137 | to!(
138 | js_should_support_renamed_imports,
139 | r#"
140 | import { t as i18nT, plural as i18nPlural } from "@lingui/core/macro";
141 |
142 | i18nT`Hello World!`;
143 | i18nPlural(value, {one: "...", other: "..."});
144 | "#,
145 | r#"
146 | import { i18n as $_i18n } from "@lingui/core";
147 | $_i18n._({
148 | id: "0IkKj6",
149 | message: "Hello World!"
150 | });
151 | $_i18n._({
152 | id: "kwTAtG",
153 | message: "{value, plural, one {...} other {...}}",
154 | values: {
155 | value: value
156 | }
157 | });
158 | "#
159 | );
160 | to!(
161 | jsx_should_support_renamed_imports,
162 | r#"
163 | import { Trans as I18nTrans, Plural as I18nPlural } from "@lingui/react/macro";
164 |
165 | ;
170 |
171 | ;Hello!
172 | "#,
173 | r#"
174 | import { Trans as Trans_ } from "@lingui/react";
175 |
176 | ;
179 |
180 | ;;
181 | "#
182 | );
183 | to!(
184 | // https://github.com/lingui/swc-plugin/issues/21
185 | should_add_imports_after_directive_prologues,
186 | r#"
187 | "use client";
188 | import { t } from "@lingui/core/macro"
189 | import foo from "bar"
190 | t`Text`
191 | "#,
192 | r#"
193 | "use client";
194 | import { i18n as $_i18n } from "@lingui/core";
195 | import foo from "bar";
196 | $_i18n._({
197 | id: "xeiujy",
198 | message: "Text"
199 | });
200 | "#
201 | );
202 |
--------------------------------------------------------------------------------
/src/tests/js_define_message.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | should_transform_define_message,
5 | r#"
6 | import { defineMessage, msg } from '@lingui/macro';
7 | const message1 = defineMessage({
8 | comment: "Description",
9 | message: "Message"
10 | })
11 | const message2 = msg({
12 | comment: "Description",
13 | message: "Message"
14 | })
15 | "#,
16 | r#"
17 | const message1 = {
18 | id: "xDAtGP",
19 | message: "Message"
20 | };
21 | const message2 = {
22 | id: "xDAtGP",
23 | message: "Message"
24 | };
25 | "#
26 | );
27 |
28 | to!(
29 | define_message_should_support_template_literal,
30 | r#"
31 | import { defineMessage, msg } from '@lingui/macro';
32 | const message1 = defineMessage`Message`;
33 | const message2 = msg`Message`
34 | "#,
35 | r#"
36 | const message1 = {
37 | id: "xDAtGP",
38 | message: "Message"
39 | };
40 | const message2 = {
41 | id: "xDAtGP",
42 | message: "Message"
43 | };
44 | "#
45 | );
46 |
47 | to!(
48 | should_preserve_custom_id,
49 | r#"
50 | import { defineMessage, plural, arg } from '@lingui/macro';
51 | const message = defineMessage({
52 | comment: "Description",
53 | id: "custom.id",
54 | message: "Message",
55 | })
56 | "#,
57 | r#"
58 | const message = {
59 | id: "custom.id",
60 | message: "Message"
61 | }
62 | "#
63 | );
64 |
65 | to!(
66 | should_expand_values,
67 | r#"
68 | import { defineMessage, plural, arg } from '@lingui/macro';
69 | const message = defineMessage({
70 | message: `Hello ${name}`
71 | })
72 | "#,
73 | r#"
74 | const message = {
75 | id: "OVaF9k",
76 | message: "Hello {name}",
77 | values: {
78 | name: name
79 | }
80 | };
81 | "#
82 | );
83 |
84 | to!(
85 | should_expand_macros,
86 | r#"
87 | import { defineMessage, plural, arg } from '@lingui/macro';
88 | const message = defineMessage({
89 | comment: "Description",
90 | message: plural(count, { one: "book", other: "books" })
91 | })
92 | "#,
93 | r#"
94 | const message = {
95 | id: "AJdPPy",
96 | message: "{count, plural, one {book} other {books}}",
97 | values: {
98 | count: count
99 | }
100 | };
101 | "#
102 | );
103 |
104 | to!(
105 | production,
106 | should_kept_only_essential_props,
107 | r#"
108 | import { defineMessage } from '@lingui/macro'
109 | const message1 = defineMessage`Message`;
110 | const message2 = defineMessage({
111 | message: `Hello ${name}`,
112 | id: 'msgId',
113 | comment: 'description for translators',
114 | context: 'My Context',
115 | })
116 | "#,
117 | r#"
118 | const message1 = {
119 | id: "xDAtGP",
120 | };
121 |
122 | const message2 = {
123 | id: "msgId",
124 | values: {
125 | name: name,
126 | },
127 | };
128 | "#
129 | );
130 |
--------------------------------------------------------------------------------
/src/tests/js_icu.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | js_icu_macro,
5 | r#"
6 | import { plural, select, selectOrdinal } from "@lingui/core/macro";
7 | const messagePlural = plural(count, {
8 | one: '# Book',
9 | other: '# Books'
10 | })
11 | const messageSelect = select(gender, {
12 | male: 'he',
13 | female: 'she',
14 | other: 'they'
15 | })
16 | const messageSelectOrdinal = selectOrdinal(count, {
17 | one: '#st',
18 | two: '#nd',
19 | few: '#rd',
20 | other: '#th',
21 | })
22 | "#,
23 | r#"
24 | import { i18n as $_i18n } from "@lingui/core";
25 | const messagePlural = $_i18n._({
26 | id: "V/M0Vc",
27 | message: "{count, plural, one {# Book} other {# Books}}",
28 | values: {
29 | count: count
30 | }
31 | });
32 | const messageSelect = $_i18n._({
33 | id: "VRptzI",
34 | message: "{gender, select, male {he} female {she} other {they}}",
35 | values: {
36 | gender: gender
37 | }
38 | });
39 | const messageSelectOrdinal = $_i18n._({
40 | id: "Q9Q8Bj",
41 | message: "{count, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
42 | values: {
43 | count: count
44 | }
45 | });
46 | "#
47 | );
48 |
49 | to!(
50 | js_icu_diffrent_object_literal_syntax,
51 | r#"
52 | import { plural } from "@lingui/core/macro";
53 |
54 | const messagePlural = plural(count, {
55 | one: '# Book',
56 | "other": '# Books',
57 | few: ('# Books'),
58 | })
59 | "#,
60 | r#"
61 | import { i18n as $_i18n } from "@lingui/core";
62 |
63 | const messagePlural = $_i18n._({
64 | id: "2y/Fr5",
65 | message: "{count, plural, one {# Book} other {# Books} few {# Books}}",
66 | values: {
67 | count: count
68 | }
69 | });
70 | "#
71 | );
72 |
73 | to!(
74 | js_choices_may_contain_expressions,
75 | r#"
76 | import { plural, select, selectOrdinal } from "@lingui/core/macro";
77 | const messagePlural = plural(count, {
78 | one: foo.bar,
79 | other: variable
80 | })
81 | const messageSelect = select(gender, {
82 | male: 'he',
83 | female: variable,
84 | third: fn(),
85 | other: foo.bar
86 | })
87 | "#,
88 | r#"
89 | import { i18n as $_i18n } from "@lingui/core";
90 | const messagePlural = $_i18n._({
91 | id: "l6reUi",
92 | message: "{count, plural, one {{0}} other {{variable}}}",
93 | values: {
94 | count: count,
95 | variable: variable,
96 | 0: foo.bar
97 | }
98 | });
99 | const messageSelect = $_i18n._({
100 | id: "M4Fisk",
101 | message: "{gender, select, male {he} female {{variable}} third {{0}} other {{1}}}",
102 | values: {
103 | gender: gender,
104 | variable: variable,
105 | 0: fn(),
106 | 1: foo.bar
107 | }
108 | });
109 | "#
110 | );
111 |
112 | to!(
113 | js_should_not_touch_non_lungui_fns,
114 | r#"
115 | import { plural } from "@lingui/core/macro";
116 | const messagePlural = customName(count, {
117 | one: '# Book',
118 | other: '# Books'
119 | })
120 | "#,
121 | r#"
122 | const messagePlural = customName(count, {
123 | one: '# Book',
124 | other: '# Books'
125 | })
126 | "#
127 | );
128 |
129 | to!(
130 | js_plural_with_placeholders,
131 | r#"
132 | import { plural } from "@lingui/core/macro";
133 |
134 | const message = plural(count, {
135 | one: `${name} has # friend`,
136 | other: `${name} has # friends`
137 | })
138 | "#,
139 | r#"
140 | import { i18n as $_i18n } from "@lingui/core";
141 | const message = $_i18n._({
142 | id: "CvuUwE",
143 | message: "{count, plural, one {{name} has # friend} other {{name} has # friends}}",
144 | values: {
145 | count: count,
146 | name: name
147 | }
148 | });
149 | "#
150 | );
151 |
152 | to!(
153 | js_dedup_values_in_icu,
154 | r#"
155 | import { plural } from "@lingui/core/macro";
156 |
157 | const message = plural(count, {
158 | one: `${name} has ${count} friend`,
159 | other: `${name} has {count} friends`
160 | })
161 | "#,
162 | r#"
163 | import { i18n as $_i18n } from "@lingui/core";
164 |
165 | const message = $_i18n._({
166 | id: "tK7kAV",
167 | message: "{count, plural, one {{name} has {count} friend} other {{name} has {count} friends}}",
168 | values: {
169 | count: count,
170 | name: name
171 | }
172 | });
173 | "#
174 | );
175 |
176 | to!(
177 | js_icu_nested_in_t,
178 | r#"
179 | import { t, selectOrdinal } from '@lingui/macro'
180 |
181 | t`This is my ${selectOrdinal(count, {
182 | one: "st",
183 | two: "nd",
184 | other: "rd"
185 | })} cat`
186 | "#,
187 | r#"
188 | import { i18n as $_i18n } from "@lingui/core";
189 |
190 | $_i18n._({
191 | id: "LF3Ndn",
192 | message: "This is my {count, selectordinal, one {st} two {nd} other {rd}} cat",
193 | values: {
194 | count: count
195 | }
196 | });
197 | "#
198 | );
199 |
200 | to!(
201 | js_icu_nested_in_choices,
202 | r#"
203 | import { plural } from "@lingui/core/macro"
204 | const message = plural(numBooks, {
205 | one: plural(numArticles, {
206 | one: `1 book and 1 article`,
207 | other: `1 book and ${numArticles} articles`,
208 | }),
209 | other: plural(numArticles, {
210 | one: `${numBooks} books and 1 article`,
211 | other: `${numBooks} books and ${numArticles} articles`,
212 | }),
213 | })
214 | "#,
215 | r#"
216 | import { i18n as $_i18n } from "@lingui/core"
217 | const message = $_i18n._({
218 | id: "AA3wsz",
219 | message: "{numBooks, plural, one {{numArticles, plural, one {1 book and 1 article} other {1 book and {numArticles} articles}}} other {{numArticles, plural, one {{numBooks} books and 1 article} other {{numBooks} books and {numArticles} articles}}}}",
220 | values: {
221 | numBooks: numBooks,
222 | numArticles: numArticles
223 | }
224 | });
225 | "#
226 | );
227 |
228 | to!(
229 | js_plural_with_offset_and_exact_matches,
230 | r#"
231 | import { plural } from '@lingui/macro'
232 | plural(users.length, {
233 | offset: 1,
234 | 0: "No books",
235 | 1: "1 book",
236 | other: "\# books"
237 | });
238 | "#,
239 | r#"
240 | import { i18n as $_i18n } from "@lingui/core";
241 | $_i18n._({
242 | id: "CF5t+7",
243 | message: "{0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}",
244 | values: {
245 | 0: users.length
246 | }
247 | });
248 | "#
249 | );
250 |
251 | to!(
252 | js_should_not_treat_offset_in_select,
253 | r#"
254 | import { select } from '@lingui/macro'
255 | select(value, {
256 | offset: "..",
257 | any: "..",
258 | other: "..",
259 | });
260 | "#,
261 | r#"
262 | import { i18n as $_i18n } from "@lingui/core";
263 | $_i18n._({
264 | id: "QHtFym",
265 | message: "{value, select, offset {..} any {..} other {..}}",
266 | values: {
267 | value: value
268 | }
269 | });
270 | "#
271 | );
272 |
--------------------------------------------------------------------------------
/src/tests/js_t.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | js_should_not_touch_code_if_no_macro_import,
5 | // input
6 | r#"
7 | t`Refresh inbox`;
8 | "#,
9 | // output after transform
10 | r#"
11 | t`Refresh inbox`;
12 | "#
13 | );
14 |
15 | to!(
16 | js_should_not_touch_not_related_tagget_tpls,
17 | // input
18 | r#"
19 | import { t } from "@lingui/core/macro";
20 |
21 | b`Refresh inbox`;
22 | b(i18n)`Refresh inbox`;
23 | "#,
24 | // output after transform
25 | r#"
26 | b`Refresh inbox`;
27 | b(i18n)`Refresh inbox`;
28 | "#
29 | );
30 |
31 | to!(
32 | js_should_work_with_legacy_import,
33 | // input
34 | r#"
35 | import { t } from "@lingui/macro";
36 |
37 | t`Refresh inbox`;
38 | "#,
39 | // output after transform
40 | r#"
41 | import { i18n as $_i18n } from "@lingui/core";
42 | $_i18n._({
43 | id: "EsCV2T",
44 | message: "Refresh inbox"
45 | });
46 |
47 | "#
48 | );
49 |
50 | to!(
51 | js_substitution_in_tpl_literal,
52 | // input
53 | r#"
54 | import { t } from "@lingui/core/macro";
55 |
56 | t`Refresh inbox`
57 | t`Refresh ${foo} inbox ${bar}`
58 | t`Refresh ${foo.bar} inbox ${bar}`
59 | t`Refresh ${expr()}`
60 | "#,
61 | // output after transform
62 | r#"
63 | import { i18n as $_i18n } from "@lingui/core";
64 | $_i18n._({
65 | id: "EsCV2T",
66 | message: "Refresh inbox"
67 | });
68 | $_i18n._({
69 | id: "JPS+Xq",
70 | message: "Refresh {foo} inbox {bar}",
71 | values: {
72 | foo: foo,
73 | bar: bar
74 | }
75 | });
76 | $_i18n._({
77 | id: "xplbye",
78 | message: "Refresh {0} inbox {bar}",
79 | values: {
80 | bar: bar,
81 | 0: foo.bar
82 | }
83 | });
84 | $_i18n._({
85 | id: "+NCjg/",
86 | message: "Refresh {0}",
87 | values: {
88 | 0: expr()
89 | }
90 | });
91 | "#
92 | );
93 |
94 | to!(
95 | js_dedup_values_in_tpl_literal,
96 | // input
97 | r#"
98 | import { t } from "@lingui/core/macro";
99 | t`Refresh ${foo} inbox ${foo}`
100 | "#,
101 | // output after transform
102 | r#"
103 | import { i18n as $_i18n } from "@lingui/core";
104 | $_i18n._({
105 | id: "YZhODz",
106 | message: "Refresh {foo} inbox {foo}",
107 | values: {
108 | foo: foo
109 | }
110 | });
111 |
112 | "#
113 | );
114 |
115 | to!(
116 | js_explicit_labels_in_tpl_literal,
117 | // input
118 | r#"
119 | import { t } from "@lingui/core/macro";
120 |
121 | t`Refresh ${{foo}} inbox`
122 | t`Refresh ${{foo: foo.bar}} inbox`
123 | t`Refresh ${{foo: expr()}} inbox`
124 | t`Refresh ${{foo: bar, baz: qux}} inbox`
125 | t`Refresh ${{}} inbox`
126 | t`Refresh ${{...spread}} inbox`
127 | "#,
128 | // output after transform
129 | r#"
130 | import { i18n as $_i18n } from "@lingui/core";
131 | $_i18n._({
132 | id: "rtxU8c",
133 | message: "Refresh {foo} inbox",
134 | values: {
135 | foo: foo
136 | }
137 | });
138 | $_i18n._({
139 | id: "rtxU8c",
140 | message: "Refresh {foo} inbox",
141 | values: {
142 | foo: foo.bar
143 | }
144 | });
145 | $_i18n._({
146 | id: "rtxU8c",
147 | message: "Refresh {foo} inbox",
148 | values: {
149 | foo: expr()
150 | }
151 | });
152 | $_i18n._({
153 | id: "rtxU8c",
154 | message: "Refresh {foo} inbox",
155 | values: {
156 | foo: bar
157 | }
158 | });
159 | $_i18n._({
160 | id: "AmeQ8b",
161 | message: "Refresh {0} inbox",
162 | values: {
163 | 0: {}
164 | }
165 | });
166 | $_i18n._({
167 | id: "AmeQ8b",
168 | message: "Refresh {0} inbox",
169 | values: {
170 | 0: {
171 | ...spread
172 | }
173 | }
174 | });
175 | "#
176 | );
177 |
178 | to!(
179 | js_ph_labels_in_tpl_literal,
180 | // input
181 | r#"
182 | import { t, ph } from "@lingui/core/macro";
183 |
184 | t`Refresh ${ph({foo})} inbox`
185 | t`Refresh ${ph({foo: foo.bar})} inbox`
186 | t`Refresh ${ph({foo: expr()})} inbox`
187 | t`Refresh ${ph({foo: bar, baz: qux})} inbox`
188 | t`Refresh ${ph({})} inbox`
189 | t`Refresh ${ph({...spread})} inbox`
190 | "#,
191 | // output after transform
192 | r#"
193 | import { i18n as $_i18n } from "@lingui/core";
194 | $_i18n._({
195 | id: "rtxU8c",
196 | message: "Refresh {foo} inbox",
197 | values: {
198 | foo: foo
199 | }
200 | });
201 | $_i18n._({
202 | id: "rtxU8c",
203 | message: "Refresh {foo} inbox",
204 | values: {
205 | foo: foo.bar
206 | }
207 | });
208 | $_i18n._({
209 | id: "rtxU8c",
210 | message: "Refresh {foo} inbox",
211 | values: {
212 | foo: expr()
213 | }
214 | });
215 | $_i18n._({
216 | id: "rtxU8c",
217 | message: "Refresh {foo} inbox",
218 | values: {
219 | foo: bar
220 | }
221 | });
222 | $_i18n._({
223 | id: "AmeQ8b",
224 | message: "Refresh {0} inbox",
225 | values: {
226 | 0: {}
227 | }
228 | });
229 | $_i18n._({
230 | id: "AmeQ8b",
231 | message: "Refresh {0} inbox",
232 | values: {
233 | 0: {
234 | ...spread
235 | }
236 | }
237 | });
238 | "#
239 | );
240 |
241 | to!(
242 | js_choice_labels_in_tpl_literal,
243 | // input
244 | r##"
245 | import { t, ph, plural, select, selectOrdinal } from "@lingui/core/macro";
246 |
247 | t`We have ${plural({count: getDevelopersCount()}, {one: "# developer", other: "# developers"})}`
248 | t`${select(gender, {male: "he", female: "she", other: "they"})}`
249 | t`${selectOrdinal(count, {one: "#st", two: "#nd", few: "#rd", other: "#th"})}`
250 | "##,
251 | // output after transform
252 | r#"
253 | import { i18n as $_i18n } from "@lingui/core";
254 | $_i18n._({
255 | id: "+7z66M",
256 | message: "We have {count, plural, one {# developer} other {# developers}}",
257 | values: {
258 | count: getDevelopersCount()
259 | }
260 | });
261 | $_i18n._({
262 | id: "VRptzI",
263 | message: "{gender, select, male {he} female {she} other {they}}",
264 | values: {
265 | gender: gender
266 | }
267 | });
268 | $_i18n._({
269 | id: "Q9Q8Bj",
270 | message: "{count, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
271 | values: {
272 | count: count
273 | }
274 | });
275 | "#
276 | );
277 |
278 | to!(
279 | js_custom_i18n_passed,
280 | // input
281 | r#"
282 | import { t } from "@lingui/core/macro";
283 | import { custom_i18n } from "./i18n";
284 |
285 | t(custom_i18n)`Refresh inbox`
286 | t(custom_i18n)`Refresh ${foo} inbox ${bar}`
287 | t(custom_i18n)`Refresh ${foo.bar} inbox ${bar}`
288 | t(custom_i18n)`Refresh ${expr()}`
289 | t(custom.i18n)`Refresh ${expr()}`
290 | "#,
291 | // output after transform
292 | r#"
293 | import { custom_i18n } from "./i18n";
294 | custom_i18n._({
295 | id: "EsCV2T",
296 | message: "Refresh inbox"
297 | });
298 | custom_i18n._({
299 | id: "JPS+Xq",
300 | message: "Refresh {foo} inbox {bar}",
301 | values: {
302 | foo: foo,
303 | bar: bar
304 | }
305 | });
306 | custom_i18n._({
307 | id: "xplbye",
308 | message: "Refresh {0} inbox {bar}",
309 | values: {
310 | bar: bar,
311 | 0: foo.bar
312 | }
313 | });
314 | custom_i18n._({
315 | id: "+NCjg/",
316 | message: "Refresh {0}",
317 | values: {
318 | 0: expr()
319 | }
320 | });
321 | custom.i18n._({
322 | id: "+NCjg/",
323 | message: "Refresh {0}",
324 | values: {
325 | 0: expr()
326 | }
327 | });
328 | "#
329 | );
330 |
331 | to!(
332 | js_newlines_are_preserved,
333 | r#"
334 | import { t } from '@lingui/core/macro';
335 | t`Multiline
336 | string`;
337 | "#,
338 | r#"
339 | import { i18n as $_i18n } from "@lingui/core";
340 | $_i18n._({
341 | id: "amQF7O",
342 | message: "Multiline\n string"
343 | });
344 | "#
345 | );
346 |
347 | to!(
348 | js_continuation_character,
349 | r#"
350 | import { t } from '@lingui/core/macro';
351 | t`Multiline\
352 | string`;
353 | "#,
354 | r#"
355 | import { i18n as $_i18n } from "@lingui/core";
356 | $_i18n._({
357 | id: "d1nA7b",
358 | message: "Multiline string"
359 | });
360 | "#
361 | );
362 | to!(
363 | unicode_characters_interpreted,
364 | r#"
365 | import { t } from '@lingui/core/macro';
366 | t`Message \u0020`;
367 | t`Bienvenue\xA0!`
368 | "#,
369 | r#"
370 | import { i18n as $_i18n } from "@lingui/core";
371 | $_i18n._({
372 | id: "dZXeyN",
373 | message: "Message "
374 | });
375 | $_i18n._({
376 | id: "9K3RGd",
377 | message: "Bienvenue !"
378 | });
379 | "#
380 | );
381 | to!(
382 | js_support_message_descriptor_in_t_fn,
383 | r#"
384 | import { t } from '@lingui/core/macro'
385 | const msg = t({ message: `Hello ${name}`, id: 'msgId', comment: 'description for translators' })
386 | "#,
387 | r#"
388 | import { i18n as $_i18n } from "@lingui/core";
389 | const msg = $_i18n._({
390 | id: "msgId",
391 | message: "Hello {name}",
392 | values: {
393 | name: name,
394 | },
395 | });
396 | "#
397 | );
398 |
399 | to!(
400 | js_t_fn_wrapped_in_call_expr,
401 | r#"
402 | import { t } from '@lingui/core/macro'
403 | const msg = message.error(t({message: "dasd"}))
404 | "#,
405 | r#"
406 | import { i18n as $_i18n } from "@lingui/core";
407 | const msg = message.error(
408 | $_i18n._(
409 | {
410 | id: "9ZMZjU",
411 | message: "dasd",
412 | }
413 | )
414 | );
415 | "#
416 | );
417 |
418 | to!(
419 | production,
420 | js_should_kept_only_essential_props,
421 | r#"
422 | import { t } from '@lingui/core/macro'
423 | const msg1 = t`Message`
424 | const msg2 = t({
425 | message: `Hello ${name}`,
426 | id: 'msgId',
427 | comment: 'description for translators',
428 | context: 'My Context',
429 | })
430 | "#,
431 | r#"
432 | import { i18n as $_i18n } from "@lingui/core";
433 | const msg1 = $_i18n._({
434 | id: "xDAtGP"
435 | });
436 |
437 | const msg2 = $_i18n._({
438 | id: "msgId",
439 | values: {
440 | name: name,
441 | },
442 | });
443 | "#
444 | );
445 |
446 | to!(
447 | js_support_template_strings_in_t_macro_message_with_custom_i18n_instance,
448 | r#"
449 | import { t } from '@lingui/core/macro'
450 | import { i18n_custom } from './lingui'
451 | const msg = t(i18n_custom)({ message: `Hello ${name}` })
452 | "#,
453 | r#"
454 | import { i18n_custom } from './lingui';
455 | const msg = i18n_custom._({
456 | id: "OVaF9k",
457 | message: "Hello {name}",
458 | values: {
459 | name: name,
460 | },
461 | });
462 | "#
463 | );
464 |
465 | to!(
466 | support_id_and_comment_in_t_macro_as_call_expression,
467 | r#"
468 | import { t, plural } from '@lingui/core/macro'
469 | const msg = t({ id: 'msgId', comment: 'description for translators', message: plural(val, { one: '...', other: '...' }) })
470 | "#,
471 | r#"
472 | import { i18n as $_i18n } from "@lingui/core";
473 | const msg = $_i18n._({
474 | id: "msgId",
475 | message: "{val, plural, one {...} other {...}}",
476 | values: {
477 | val: val,
478 | },
479 | });
480 | "#
481 | );
482 |
483 | to!(
484 | support_id_in_template_literal,
485 | r#"
486 | import { t } from '@lingui/core/macro'
487 | const msg = t({ id: `msgId` })
488 | "#,
489 | r#"
490 | import { i18n as $_i18n } from "@lingui/core";
491 | const msg = $_i18n._({
492 | id: "msgId"
493 | });
494 | "#
495 | );
496 |
497 | to!(
498 | should_generate_diffrent_id_when_context_provided,
499 | r#"
500 | import { t } from '@lingui/core/macro'
501 | t({ message: 'Ola' })
502 | t({ message: 'Ola', context: "My Context"})
503 | t({ message: 'Ola', context: `My Context`})
504 | "#,
505 | r#"
506 | import { i18n as $_i18n } from "@lingui/core";
507 | $_i18n._({
508 | id: "l1LkPs",
509 | message: "Ola"
510 | });
511 | $_i18n._({
512 | id: "7hFP9A",
513 | message: "Ola"
514 | });
515 | $_i18n._({
516 | id: "7hFP9A",
517 | message: "Ola"
518 | });
519 | "#
520 | );
521 |
--------------------------------------------------------------------------------
/src/tests/jsx.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | jsx_simple_jsx,
5 | r#"
6 | import { Trans } from "@lingui/react/macro";
7 | const exp1 = Refresh inbox;
8 | const exp2 = Refresh inbox;
9 | const exp3 = Refresh inbox
;
10 | "#,
11 | r#"
12 | import { Trans as Trans_ } from "@lingui/react";
13 | const exp1 = Refresh inbox;
14 | const exp2 = ;
15 | const exp3 =
;
16 | "#
17 | );
18 |
19 | to!(
20 | jsx_should_suppor_legacy_import,
21 | r#"
22 | import { Trans } from "@lingui/macro";
23 | const exp2 = Refresh inbox;
24 | "#,
25 | r#"
26 | import { Trans as Trans_ } from "@lingui/react";
27 | const exp2 = ;
28 | "#
29 | );
30 |
31 | to!(
32 | jsx_with_custom_id,
33 | r#"
34 | import { Trans } from "@lingui/react/macro";
35 | const exp2 = Refresh inbox;
36 | "#,
37 | r#"
38 | import { Trans as Trans_ } from "@lingui/react";
39 | const exp2 =
40 | "#
41 | );
42 |
43 | to!(
44 | jsx_with_context,
45 | r#"
46 | import { Trans } from "@lingui/react/macro";
47 | const exp1 = Refresh inbox;
48 | const exp2 = Refresh inbox;
49 | "#,
50 | r#"
51 | import { Trans as Trans_ } from "@lingui/react";
52 | const exp1 = ;
53 | const exp2 = ;
54 | "#
55 | );
56 |
57 | to!(
58 | jsx_preserve_reserved_attrs,
59 | r#"
60 | import { Trans } from "@lingui/react/macro";
61 | const exp2 = {p.translation}
} render={(v) => v}>Refresh inbox;
62 | "#,
63 | r#"
64 | import { Trans as Trans_ } from "@lingui/react";
65 | const exp2 = {p.translation}
} render={(v) => v} />
66 | "#
67 | );
68 |
69 | to!(
70 | jsx_expressions_are_converted_to_positional_arguments,
71 | r#"
72 | import { Trans } from "@lingui/react/macro";
73 |
74 | Property {props.name},
75 | function {random()},
76 | array {array[index]},
77 | constant {42},
78 | object {new Date()},
79 | everything {props.messages[index].value()}
80 | ;
81 | "#,
82 | r#"
83 | import { Trans as Trans_ } from "@lingui/react";
84 | ;
95 | "#
96 | );
97 |
98 | to!(
99 | jsx_components_interpolation,
100 | r#"
101 | import { Trans } from "@lingui/react/macro";
102 |
103 | Hello World!
104 |
105 | My name is {" "}
106 | {name}
107 |
108 |
109 | "#,
110 | r#"
111 | import { Trans as Trans_ } from "@lingui/react";
112 | World!0><1/><2>My name is <3> <4>{name}4>3>2>"}
114 | id={"k9gsHO"}
115 | values={{
116 | name: name,
117 | }} components={{
118 | 0: ,
119 | 1:
,
120 | 2: ,
121 | 3: ,
122 | 4:
123 | }} />;
124 | "#
125 | );
126 |
127 | to!(
128 | jsx_values_dedup,
129 | r#"
130 | import { Trans } from "@lingui/react/macro";
131 |
132 | Hello {foo} and {foo}{" "}
133 | {bar}
134 |
135 | "#,
136 | r#"
137 | import { Trans as Trans_ } from "@lingui/react";
138 | ;
143 | "#
144 | );
145 |
146 | to!(
147 | jsx_explicit_labels_with_as_statement,
148 | r#"
149 | import { Trans } from "@lingui/react/macro";
150 | Refresh {{foo} as unknown as string} inbox;
151 | "#,
152 | r#"
153 | import { Trans as Trans_ } from "@lingui/react";
154 | ;
158 | "#
159 | );
160 |
161 | to!(
162 | jsx_explicit_labels,
163 | r#"
164 | import { Trans } from "@lingui/react/macro";
165 |
166 | Refresh {{foo}} inbox;
167 | Refresh {{foo: foo.bar}} inbox;
168 | Refresh {{foo: expr()}} inbox;
169 | Refresh {{foo: bar, baz: qux}} inbox;
170 | Refresh {{}} inbox;
171 | Refresh {{...spread}} inbox;
172 | "#,
173 | r#"
174 | import { Trans as Trans_ } from "@lingui/react";
175 | ;
179 | ;
183 | ;
187 | ;
191 | ;
195 | ;
201 | "#
202 | );
203 |
204 | to!(
205 | jsx_ph_labels,
206 | r#"
207 | import { Trans, ph } from "@lingui/react/macro";
208 |
209 | Refresh {ph({foo})} inbox;
210 | Refresh {ph({foo: foo.bar})} inbox;
211 | Refresh {ph({foo: expr()})} inbox;
212 | Refresh {ph({foo: bar, baz: qux})} inbox;
213 | Refresh {ph({})} inbox;
214 | Refresh {ph({...spread})} inbox;
215 | "#,
216 | r#"
217 | import { Trans as Trans_ } from "@lingui/react";
218 | ;
222 | ;
226 | ;
230 | ;
234 | ;
238 | ;
244 | "#
245 | );
246 |
247 | to!(
248 | jsx_nested_labels,
249 | r#"
250 | import { Trans, ph } from "@lingui/react/macro";
251 |
252 | Refresh {{foo}} inbox;
253 | Refresh {ph({foo})} inbox;
254 | "#,
255 | r#"
256 | import { Trans as Trans_ } from "@lingui/react";
257 | {foo}0> inbox"} id={"USNn2Q"}
258 | values={{
259 | foo: foo,
260 | }}
261 | components={{
262 | 0: ,
263 | }}/>;
264 | {foo}0> inbox"} id={"USNn2Q"}
265 | values={{
266 | foo: foo,
267 | }}
268 | components={{
269 | 0: ,
270 | }}/>;
271 | "#
272 | );
273 |
274 | to!(
275 | jsx_template_literal_in_children,
276 | r#"
277 | import { Trans } from "@lingui/react/macro";
278 | {`Hello ${foo} and ${bar}`}
279 | "#,
280 | r#"
281 | import { Trans as Trans_ } from "@lingui/react";
282 | ;
286 | "#
287 | );
288 |
289 | to!(
290 | quoted_jsx_attributes_are_handled,
291 | r#"
292 | import { Trans } from "@lingui/react/macro";
293 | Speak "friend"!;
294 | Speak "friend"!;
295 | "#,
296 | r#"
297 | import { Trans as Trans_ } from "@lingui/react";
298 | ;
299 | ;
300 | "#
301 | );
302 |
303 | to!(
304 | html_attributes_are_handled,
305 | r#"
306 | import { Trans } from "@lingui/react/macro";
307 |
308 | This should work
309 | ;
310 | "#,
311 | r#"
312 | import { Trans as Trans_ } from "@lingui/react";
313 | This should work 0>"} id={"K/1Xpr"}
314 | components={{
315 | 0: ,
316 | }}
317 | />;
318 | "#
319 | );
320 |
321 | to!(
322 | use_decoded_html_entities,
323 | r#"
324 | import { Trans } from "@lingui/react/macro";
325 | &
326 | "#,
327 | r#"
328 | import { Trans as Trans_ } from "@lingui/react";
329 | ;
330 | "#
331 | );
332 |
333 | to!(
334 | elements_inside_expression_container,
335 | r#"
336 | import { Trans } from "@lingui/react/macro";
337 | {Component inside expression container};
338 | "#,
339 | r#"
340 | import { Trans as Trans_ } from "@lingui/react";
341 | Component inside expression container0>"}
343 | id={"1cZQQW"}
344 | components={{
345 | 0:
346 | }} />;
347 | "#
348 | );
349 |
350 | to!(
351 | elements_without_children,
352 | r#"
353 | import { Trans } from "@lingui/react/macro";
354 | {
};
355 | "#,
356 | r#"
357 | import { Trans as Trans_ } from "@lingui/react";
358 | "} id={"SCJtqt"} components={{
359 | 0:
360 | }} />;
361 | "#
362 | );
363 |
364 | // it's better to throw panic here
365 | // to!(
366 | // jsx_spread_child_is_noop,
367 | // r#"
368 | // import { Trans } from "@lingui/react/macro";
369 | // {...spread}
370 | // "#,
371 | // r#"
372 | // import { Trans as Trans_ } from "@lingui/react";
373 | // {...spread}
374 | // "#
375 | // );
376 |
377 | to!(
378 | strip_whitespace_around_arguments,
379 | r#"
380 | import { Trans } from "@lingui/react/macro";
381 |
382 | Strip whitespace around arguments: '
383 | {name}
384 | '
385 |
386 | "#,
387 | r#"
388 | import { Trans as Trans_ } from "@lingui/react";
389 | ;
392 | "#
393 | );
394 |
395 | to!(
396 | strip_whitespace_around_tags_but_keep_forced_spaces,
397 | r#"
398 | import { Trans } from "@lingui/react/macro";
399 |
400 | Strip whitespace around tags, but keep{" "}
401 | forced spaces
402 | !
403 |
404 | "#,
405 | r#"
406 | import { Trans as Trans_ } from "@lingui/react";
407 | forced spaces0>!"} id={"Ud4KOf"} components={{
408 | 0:
409 | }} />;
410 | "#
411 | );
412 |
413 | to!(
414 | keep_multiple_forced_newlines,
415 | r#"
416 | import { Trans } from "@lingui/react/macro";
417 |
418 | Keep multiple{"\n"}
419 | forced{"\n"}
420 | newlines!
421 |
422 | "#,
423 | r#"
424 | import { Trans as Trans_ } from "@lingui/react";
425 | ;
426 | "#
427 | );
428 |
429 | to!(
430 | use_js_macro_in_jsx_attrs,
431 | r#"
432 | import { t } from '@lingui/core/macro';
433 | import { Trans } from '@lingui/react/macro';
434 | Read more
435 | "#,
436 | r#"
437 | import { Trans as Trans_ } from "@lingui/react";
438 | import { i18n as $_i18n } from "@lingui/core";
439 | more0>"} id={"QZyANg"} components={{
440 | 0:
447 | }}/>;
448 | "#
449 | );
450 |
451 | to!(
452 | use_js_plural_in_jsx_attrs,
453 | r#"
454 | import { plural } from '@lingui/core/macro';
455 | About
459 | "#,
460 | r#"
461 | import { i18n as $_i18n } from "@lingui/core";
462 | About;
469 |
470 | "#
471 | );
472 |
473 | to!(
474 | ignore_jsx_empty_expression,
475 | r#"
476 | import { Trans } from "@lingui/react/macro";
477 | Hello {/* and I cannot stress this enough */} World;
478 | "#,
479 | r#"
480 | import { Trans as Trans_ } from "@lingui/react";
481 | ;
482 | "#
483 | );
484 |
485 | to!(
486 | production,
487 | production_only_essential_props_are_kept,
488 | r#"
489 | import { Trans } from "@lingui/react/macro";
490 | Hello {name}
496 | "#,
497 | r#"
498 | import { Trans as Trans_ } from "@lingui/react";
499 | }}
502 | id="msg.hello"
503 | render="render"
504 | i18n="i18n"
505 | />;
506 | "#
507 | );
508 | to!(
509 | strip_whitespaces_in_jsxtext_but_keep_in_jsx_expression_containers,
510 | r#"
511 | import { Trans } from "@lingui/react/macro";
512 |
513 | {"Wonderful framework "}
514 | Next.js
515 | {" say hi. And "}
516 | Next.js
517 | {" say hi."}
518 |
519 | "#,
520 | r#"
521 | import { Trans as Trans_ } from "@lingui/react";
522 |
523 | Next.js0> say hi. And <1>Next.js1> say hi."
526 | }
527 | id={"3YVd0H"}
528 | components={{
529 | 0: ,
530 | 1: ,
531 | }}
532 | />;
533 | "#
534 | );
535 | to!(
536 | non_breaking_whitespace_handling_2226,
537 | r#"
538 | import { Trans } from "@lingui/react/macro";
539 |
540 | hello
541 |
542 | world
543 | ;
544 |
545 | "#,
546 | r#"
547 | import { Trans as Trans_ } from "@lingui/react";
548 | hello0> <1>world1>"} id={"kJt6bJ"} components={{
549 | 0: ,
550 | 1:
551 | }}/>;
552 | "#
553 | );
554 | // {
555 | // name: "production - import_type_doesn't_interference_on_normal_import",
556 | // production: true,
557 | // useTypescriptPreset: true,
558 | // input: `
559 | // import { withI18nProps } from '@lingui/react'
560 | // import { Trans } from "@lingui/react/macro";
561 | // Hello World
562 | // `,
563 | // expected: `
564 | // import { withI18nProps, Trans } from "@lingui/react";
565 | // ;
566 | // `,
567 | // },
568 |
--------------------------------------------------------------------------------
/src/tests/jsx_icu.rs:
--------------------------------------------------------------------------------
1 | use crate::to;
2 |
3 | to!(
4 | jsx_icu,
5 | r#"
6 | import { Plural } from "@lingui/react/macro";
7 |
8 | const ex1 =
13 |
14 | const ex2 =
19 | "#,
20 | r#"
21 | import { Trans as Trans_ } from "@lingui/react";
22 |
23 | const ex1 = ;
26 | const ex2 =
;
29 |
30 | "#
31 | );
32 |
33 | to!(
34 | jsx_icu_explicit_id,
35 | r#"
36 | import { Plural } from "@lingui/react/macro";
37 |
38 |
44 | "#,
45 | r#"
46 | import { Trans as Trans_ } from "@lingui/react";
47 |
48 |
53 | "#
54 | );
55 |
56 | to!(
57 | jsx_plural_preserve_reserved_attrs,
58 | r#"
59 | import { Plural } from "@lingui/react/macro";
60 |
61 | v}
65 | value={count}
66 | one="..."
67 | other="..."
68 | />
69 | "#,
70 | r#"
71 | import { Trans as Trans_ } from "@lingui/react";
72 |
73 | v}
78 | />
79 | "#
80 | );
81 |
82 | to!(
83 | jsx_icu_nested,
84 | r#"
85 | import { Plural, Trans } from "@lingui/react/macro";
86 |
87 |
88 | You have{" "}
89 |
94 |
95 | "#,
96 | r#"
97 | import { Trans as Trans_ } from "@lingui/react";
98 |
99 |
104 | "#
105 | );
106 |
107 | to!(
108 | jsx_trans_inside_plural,
109 | r#"
110 | import { Trans, Plural } from '@lingui/macro';
111 |
115 | # slot added
116 |
117 | }
118 | other={
119 |
120 | # slots added
121 |
122 | }
123 | />;
124 | "#,
125 | r#"
126 | import { Trans as Trans_ } from "@lingui/react";
127 | #0> slot added} other {<1>#1> slots added}}"} id={"X8eyr1"}
128 | values={{
129 | count: count
130 | }} components={{
131 | 0: ,
132 | 1:
133 | }} />;
134 |
135 | "#
136 | );
137 |
138 | to!(
139 | jsx_multivelel_nesting,
140 | r#"
141 | import { Trans, Plural } from '@lingui/macro';
142 |
143 |
147 |
151 | second level one
152 |
153 | }
154 | other={
155 |
156 | second level other
157 |
158 | }
159 | />
160 |
161 | # slot added
162 |
163 | }
164 | other={
165 |
166 | # slots added
167 |
168 | }
169 | />;
170 | "#,
171 | r#"
172 | import { Trans as Trans_ } from "@lingui/react";
173 | #0> slot added} other {<1>#1> slots added}}"}
175 | id={"bDgQmM"}
176 | values={{
177 | count: count,
178 | count2: count2
179 | }} components={{
180 | 0: ,
181 | 1:
182 | }} />;
183 | "#
184 | );
185 |
186 | to!(
187 | jsx_plural_with_offset_and_exact_matches,
188 | r#"
189 | import { Plural } from "@lingui/react/macro";
190 |
191 | A lot of them}
196 | />;
197 | "#,
198 | r#"
199 | import { Trans as Trans_ } from "@lingui/react";
200 | A lot of them0>}}"}
201 | id={"ZFknU1"}
202 | values={{
203 | count: count
204 | }} components={{
205 | 0:
206 | }} />;
207 | "#
208 | );
209 |
210 | to!(
211 | jsx_icu_with_template_literal,
212 | r#"
213 | import { Plural } from "@lingui/react/macro";
214 |
215 | ;
220 | "#,
221 | r#"
222 | import { Trans as Trans_ } from "@lingui/react";
223 | ;
230 | "#
231 | );
232 |
233 | to!(
234 | jsx_select_simple,
235 | r#"
236 | import { Select } from '@lingui/macro';
237 |