├── .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 | [![npm](https://img.shields.io/npm/v/@lingui/swc-plugin?logo=npm&cacheSeconds=1800)](https://www.npmjs.com/package/@lingui/swc-plugin) 8 | [![npm](https://img.shields.io/npm/dt/@lingui/swc-plugin?cacheSeconds=500)](https://www.npmjs.com/package/@lingui/swc-plugin) 9 | [![CI](https://github.com/lingui/swc-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/lingui/swc-plugin/actions/workflows/ci.yml) 10 | [![GitHub contributors](https://img.shields.io/github/contributors/lingui/swc-plugin?cacheSeconds=1000)](https://github.com/lingui/swc-plugin/graphs/contributors) 11 | [![GitHub](https://img.shields.io/github/license/lingui/swc-plugin)](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!("")); 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 | // Should be untouched 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!<1/><2>My name is <3> <4>{name}"} 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} inbox"} id={"USNn2Q"} 258 | values={{ 259 | foo: foo, 260 | }} 261 | components={{ 262 | 0: , 263 | }}/>; 264 | {foo} 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  "} 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 container"} 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 spaces!"} 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 | more"} 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.js say hi. And <1>Next.js 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 | hello <1>world"} 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 | # slot added} other {<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 | # slot added} other {<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 them}}"} 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 | Other} 264 | />; 265 | "#, 266 | r#" 267 | import { Trans as Trans_ } from "@lingui/react"; 268 | Other}}"} id={"/7RSeH"} values={{ 269 | count: count, 270 | variable: variable, 271 | 0: foo.bar 272 | }} components={{ 273 | 0: 274 | }} />; 275 | "# 276 | ); 277 | 278 | to!( 279 | jsx_select_with_reserved_attrs, 280 | r#" 281 | import { Select } from '@lingui/macro'; 282 | Other} 313 | // />; 314 | // "#, 315 | // 316 | // r#" 317 | // import { Trans as Trans_ } from "@lingui/react"; 318 | // Other}}"} values={{ 319 | // count: count 320 | // }} components={{ 321 | // 0: 322 | // }} 323 | // />; 324 | // "# 325 | // ); 326 | 327 | to!( 328 | jsx_select_ordinal_with_offset_and_exact_matches, 329 | r#" 330 | import { SelectOrdinal } from "@lingui/react/macro"; 331 | 332 | ; 339 | "#, 340 | r#" 341 | import { Trans as Trans_ } from "@lingui/react"; 342 | ; 345 | "# 346 | ); 347 | 348 | to!( 349 | production, 350 | production_only_essential_props_are_kept, 351 | r#" 352 | import { Plural } from '@lingui/macro'; 353 | 354 | A lot of them} 364 | /> 365 | "#, 366 | r#" 367 | import { Trans as Trans_ } from "@lingui/react"; 368 | }} 371 | id="custom.id" 372 | render="render" 373 | i18n="i18n" />; 374 | "# 375 | ); 376 | 377 | to!( 378 | multiple_new_lines_with_nbsp_endind, 379 | r#" 380 | import { Trans } from "@lingui/react/macro"; 381 | 382 | Line ending in non-breaking space.  383 | text in element 384 | ; 385 | "#, 386 | r#" 387 | import { Trans as Trans_ } from "@lingui/react"; 388 | text in element"} id={"CJuEhi"} components={{ 389 | 0: 390 | }}/>; 391 | "# 392 | ); 393 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod imports; 3 | mod js_define_message; 4 | mod js_icu; 5 | mod js_t; 6 | mod jsx; 7 | mod jsx_icu; 8 | mod runtime_config; 9 | mod use_lingui; 10 | -------------------------------------------------------------------------------- /src/tests/runtime_config.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use crate::{LinguiOptions, RuntimeModulesConfigMapNormalized}; 3 | 4 | macro_rules! to { 5 | ($name:ident, $options:expr, $from:expr, $to:expr) => { 6 | swc_core::ecma::transforms::testing::test_inline!( 7 | swc_core::ecma::parser::Syntax::Typescript(swc_core::ecma::parser::TsSyntax { 8 | tsx: true, 9 | ..Default::default() 10 | }), 11 | |_| { swc_core::ecma::visit::fold_pass($crate::LinguiMacroFolder::new($options)) }, 12 | $name, 13 | $from, 14 | $to 15 | ); 16 | }; 17 | } 18 | 19 | to!( 20 | should_use_provided_runtime_modules, 21 | LinguiOptions { 22 | runtime_modules: RuntimeModulesConfigMapNormalized { 23 | i18n: ("./custom-core".into(), "customI18n".into()), 24 | trans: ("./custom-react".into(), "CustomTrans".into()), 25 | use_lingui: ("./custom-react".into(), "useLingui2".into()) 26 | }, 27 | ..Default::default() 28 | }, 29 | r#" 30 | import { t } from "@lingui/core/macro"; 31 | import { Trans } from "@lingui/react/macro"; 32 | 33 | t`Refresh inbox`; 34 | const exp2 = Refresh inbox; 35 | "#, 36 | r#" 37 | import { CustomTrans as Trans_ } from "./custom-react"; 38 | import { customI18n as $_i18n } from "./custom-core"; 39 | $_i18n._({ 40 | id: "EsCV2T", 41 | message: "Refresh inbox" 42 | }); 43 | const exp2 = ; 44 | "# 45 | ); 46 | -------------------------------------------------------------------------------- /src/tests/use_lingui.rs: -------------------------------------------------------------------------------- 1 | use crate::to; 2 | 3 | to!( 4 | js_use_lingui_hook, 5 | // input 6 | r#" 7 | import { useLingui } from "@lingui/react/macro"; 8 | 9 | const bla1 = () => { 10 | console.log() 11 | } 12 | 13 | function bla() { 14 | const { t, i18n } = useLingui(); 15 | t`Refresh inbox`; 16 | } 17 | "#, 18 | // output after transform 19 | r#" 20 | import { useLingui as $_useLingui } from "@lingui/react"; 21 | 22 | const bla1 = ()=>{ 23 | console.log(); 24 | }; 25 | 26 | function bla() { 27 | const { i18n: $__i18n, i18n, _: $__ } = $_useLingui(); 28 | 29 | $__i18n._({ 30 | id: "EsCV2T", 31 | message: "Refresh inbox" 32 | }); 33 | } 34 | "# 35 | ); 36 | 37 | to!( 38 | support_renamed_destructuring, 39 | // input 40 | r#" 41 | import { useLingui } from '@lingui/react/macro'; 42 | 43 | function MyComponent() { 44 | const { t: _ } = useLingui(); 45 | const a = _`Text`; 46 | } 47 | "#, 48 | // output after transform 49 | r#" 50 | import { useLingui as $_useLingui } from "@lingui/react"; 51 | function MyComponent() { 52 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 53 | const a = $__i18n._({ 54 | id: "xeiujy", 55 | message: "Text" 56 | }); 57 | } 58 | "# 59 | ); 60 | 61 | to!( 62 | should_process_macro_with_matching_name_in_correct_scopes, 63 | // input 64 | r#" 65 | import { useLingui } from '@lingui/react/macro'; 66 | 67 | function MyComponent() { 68 | const { t } = useLingui(); 69 | const a = t`Text`; 70 | 71 | { 72 | // here is child scope with own "t" binding, shouldn't be processed 73 | const t = () => {}; 74 | t`Text`; 75 | } 76 | { 77 | // here is child scope which should be processed, since 't' relates to outer scope 78 | t`Text`; 79 | } 80 | } 81 | "#, 82 | // output after transform 83 | r#" 84 | import { useLingui as $_useLingui } from "@lingui/react"; 85 | function MyComponent() { 86 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 87 | const a = $__i18n._({ 88 | id: "xeiujy", 89 | message: "Text" 90 | }); 91 | { 92 | const t = ()=>{}; 93 | t`Text`; 94 | } 95 | { 96 | $__i18n._({ 97 | id: "xeiujy", 98 | message: "Text" 99 | }); 100 | } 101 | } 102 | "# 103 | ); 104 | 105 | to!( 106 | support_nested_macro, 107 | // input 108 | r#" 109 | import { useLingui } from '@lingui/react/macro'; 110 | import { plural } from '@lingui/core/macro'; 111 | 112 | function MyComponent() { 113 | const { t } = useLingui(); 114 | const a = t`Text ${plural(users.length, { 115 | offset: 1, 116 | 0: "No books", 117 | 1: "1 book", 118 | other: "\# books" 119 | })}`; 120 | } 121 | "#, 122 | // output after transform 123 | r#" 124 | import { useLingui as $_useLingui } from "@lingui/react"; 125 | function MyComponent() { 126 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 127 | const a = $__i18n._({ 128 | id: "hJRCh6", 129 | message: "Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", 130 | values: { 131 | 0: users.length 132 | } 133 | }); 134 | } 135 | "# 136 | ); 137 | 138 | to!( 139 | support_nested_macro_when_in_arrow_function_issue_2095, 140 | // input 141 | r#" 142 | import { plural } from '@lingui/core/macro' 143 | import { useLingui } from '@lingui/react/macro' 144 | 145 | const MyComponent = () => { 146 | const { t } = useLingui(); 147 | const a = t`Text ${plural(users.length, { 148 | offset: 1, 149 | 0: "No books", 150 | 1: "1 book", 151 | other: "\# books" 152 | })}`; 153 | } 154 | "#, 155 | // output after transform 156 | r#" 157 | import { useLingui as $_useLingui } from "@lingui/react"; 158 | const MyComponent = () => { 159 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 160 | const a = $__i18n._({ 161 | id: "hJRCh6", 162 | message: "Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}", 163 | values: { 164 | 0: users.length 165 | } 166 | }); 167 | } 168 | "# 169 | ); 170 | 171 | to!( 172 | support_passing_t_variable_as_dependency, 173 | // input 174 | r#" 175 | import { useLingui } from '@lingui/react/macro'; 176 | 177 | function MyComponent() { 178 | const { t } = useLingui(); 179 | const a = useMemo(() => t`Text`, [t]); 180 | } 181 | "#, 182 | // output after transform 183 | r#" 184 | import { useLingui as $_useLingui } from "@lingui/react"; 185 | function MyComponent() { 186 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 187 | const a = useMemo(()=>$__i18n._({ 188 | id: "xeiujy", 189 | message: "Text" 190 | }), [ 191 | $__ 192 | ]); 193 | } 194 | "# 195 | ); 196 | 197 | to!( 198 | work_when_t_is_not_used, 199 | // input 200 | r#" 201 | import { useLingui } from '@lingui/react/macro'; 202 | 203 | function MyComponent() { 204 | const { i18n } = useLingui(); 205 | console.log(i18n); 206 | } 207 | "#, 208 | // output after transform 209 | r#" 210 | import { useLingui as $_useLingui } from "@lingui/react"; 211 | function MyComponent() { 212 | const { i18n, _: $__ } = $_useLingui(); 213 | console.log(i18n); 214 | } 215 | "# 216 | ); 217 | 218 | to!( 219 | work_with_existing_use_lingui_statement, 220 | // input 221 | r#" 222 | import { useLingui as useLinguiMacro } from '@lingui/react/macro'; 223 | import { useLingui } from '@lingui/react'; 224 | 225 | function MyComponent() { 226 | const { _ } = useLingui(); 227 | 228 | console.log(_); 229 | const { t } = useLinguiMacro(); 230 | const a = t`Text`; 231 | } 232 | "#, 233 | // output after transform 234 | r#" 235 | import { useLingui as $_useLingui } from "@lingui/react"; 236 | import { useLingui } from '@lingui/react'; 237 | function MyComponent() { 238 | const { _ } = useLingui(); 239 | console.log(_); 240 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 241 | const a = $__i18n._({ 242 | id: "xeiujy", 243 | message: "Text" 244 | }); 245 | } 246 | "# 247 | ); 248 | 249 | to!( 250 | work_with_multiple_react_components, 251 | // input 252 | r#" 253 | import { useLingui } from '@lingui/react/macro'; 254 | 255 | function MyComponent() { 256 | const { t } = useLingui(); 257 | const a = t`Text`; 258 | } 259 | 260 | function MyComponent2() { 261 | const { t } = useLingui(); 262 | const b = t`Text`; 263 | } 264 | "#, 265 | // output after transform 266 | r#" 267 | import { useLingui as $_useLingui } from "@lingui/react"; 268 | function MyComponent() { 269 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 270 | const a = $__i18n._({ 271 | id: "xeiujy", 272 | message: "Text" 273 | }); 274 | } 275 | function MyComponent2() { 276 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 277 | const b = $__i18n._({ 278 | id: "xeiujy", 279 | message: "Text" 280 | }); 281 | } 282 | "# 283 | ); 284 | 285 | to!( 286 | work_with_components_defined_as_arrow_function, 287 | // input 288 | r#" 289 | import { useLingui } from '@lingui/react/macro'; 290 | 291 | const MyComponent = () => { 292 | const { t } = useLingui(); 293 | const a = t`Text`; 294 | } 295 | "#, 296 | // output after transform 297 | r#" 298 | import { useLingui as $_useLingui } from "@lingui/react"; 299 | const MyComponent = ()=>{ 300 | const { i18n: $__i18n, _: $__ } = $_useLingui(); 301 | const a = $__i18n._({ 302 | id: "xeiujy", 303 | message: "Text" 304 | }); 305 | }; 306 | "# 307 | ); 308 | -------------------------------------------------------------------------------- /src/tokens.rs: -------------------------------------------------------------------------------- 1 | use swc_core::ecma::ast::{Expr, JSXOpeningElement}; 2 | use swc_core::ecma::atoms::Atom; 3 | 4 | pub enum MsgToken { 5 | String(String), 6 | Expression(Box), 7 | TagOpening(TagOpening), 8 | TagClosing, 9 | IcuChoice(IcuChoice), 10 | } 11 | 12 | pub struct TagOpening { 13 | pub self_closing: bool, 14 | pub el: JSXOpeningElement, 15 | } 16 | 17 | pub struct IcuChoice { 18 | pub value: Box, 19 | /// plural | select | selectOrdinal 20 | pub format: Atom, 21 | pub cases: Vec, 22 | } 23 | 24 | pub enum CaseOrOffset { 25 | Case(ChoiceCase), 26 | Offset(String), 27 | } 28 | pub struct ChoiceCase { 29 | pub key: Atom, 30 | pub tokens: Vec, 31 | } 32 | 33 | // #[cfg(test)] 34 | // mod tests { 35 | // use super::{*}; 36 | // 37 | // #[test] 38 | // fn test_normalize_whitespaces() { 39 | // 40 | // } 41 | // } 42 | --------------------------------------------------------------------------------