Initial commit

This commit is contained in:
Kirill Zhumarin
2025-01-19 20:17:41 +00:00
commit b4cb7b5ca2
128 changed files with 28558 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
**/dist/*
**/node_modules/*
**/fixtures/*
**/locale/*
website/*
examples/*
README.md
**/npm/*
/packages/*/build/**
/packages/solid/babel.config.js
+26
View File
@@ -0,0 +1,26 @@
{
"root": true,
"env": {
"jest/globals": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"extends": [],
"plugins": ["promise", "jest", "@typescript-eslint", "import"],
"settings": {
"import/resolver": {
"typescript": true,
"node": true
}
},
"rules": {
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"import/no-extraneous-dependencies": ["error", {"includeTypes": true}]
}
}
+24
View File
@@ -0,0 +1,24 @@
node_modules/
.yalc/
yalc.lock
dist
npm-debug.log
yarn-error.log
coverage/
results/
junit.xml
.DS_Store
.vscode
.idea
*.iml
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+14
View File
@@ -0,0 +1,14 @@
**/.next/*
**/dist/*
**/test/fixtures/**
**/test/**/actual.js
**/test/**/actual/**
**/test/**/expected.js
**/locale/*
**/fixtures/*
**/expected/*
coverage/
website/
.github
examples
/.nx/workspace-data
+3
View File
@@ -0,0 +1,3 @@
{
"semi": false
}
+8
View File
@@ -0,0 +1,8 @@
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
unsafeHttpWhitelist:
- 0.0.0.0
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: 16,
},
modules: "commonjs",
},
],
"@babel/preset-typescript",
"@babel/preset-react",
],
}
+17
View File
@@ -0,0 +1,17 @@
const sourceConfig = require("./jest.config")
/**
* @type {import('jest').Config}
*/
module.exports = {
...sourceConfig,
projects: sourceConfig.projects.map((project) => ({
...project,
// Redirect imports to the compiled bundles.
// This is to test compiled code instead of source (applies to code under test and also its deps).
moduleNameMapper: {},
})),
// Exclude the build output from transforms
transformIgnorePatterns: ["/node_modules/", "<rootDir>/packages/*/build/"],
}
+63
View File
@@ -0,0 +1,63 @@
const { pathsToModuleNameMapper } = require("ts-jest")
const tsConfig = require("./tsconfig.json")
const tsConfigPathMapping = pathsToModuleNameMapper(
tsConfig.compilerOptions.paths,
{
prefix: "<rootDir>/",
}
)
const testMatch = ["**/?(*.)test.(js|ts|tsx)", "**/test/index.(js|ts|tsx)"]
/**
* @type {import('jest').Config}
*/
module.exports = {
roots: ["<rootDir>/packages/"],
collectCoverageFrom: [
"**/*.{ts,tsx}",
"!**/*.d.ts",
"!**/*.test-d.{ts,tsx}",
"!**/node_modules/**",
"!**/build/**",
"!**/fixtures/**",
],
coverageDirectory: "<rootDir>/coverage/",
coveragePathIgnorePatterns: [
"node_modules",
"scripts",
"test",
".*.json$",
".*.js.snap$",
],
coverageReporters: ["lcov", "text"],
globalSetup: "./scripts/jest/setupTimezone.js",
projects: [
{
displayName: "web",
testEnvironment: "jsdom",
testMatch,
moduleNameMapper: tsConfigPathMapping,
roots: ["<rootDir>/packages/solid"],
transform: {
"\\.[jt]sx?$": ["babel-jest", { configFile: "./packages/solid/babel.config.js" }]
},
},
{
displayName: "node",
testEnvironment: "jest-environment-node-single-context",
testMatch,
moduleNameMapper: tsConfigPathMapping,
snapshotSerializers: [
require.resolve("./scripts/jest/stripAnsiSerializer.js"),
],
setupFilesAfterEnv: [require.resolve("./scripts/jest/env.js")],
roots: [
"<rootDir>/packages/babel-plugin-extract-messages",
"<rootDir>/packages/babel-plugin-lingui-macro",
"<rootDir>/packages/vite-plugin"
],
},
],
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @type {import("jest").Config}
*/
module.exports = {
displayName: "tsd",
testMatch: ["**/__typetests__/*.test-d.{ts,tsx}"],
runner: "jest-runner-tsd",
roots: ["<rootDir>/packages/"],
}
+89
View File
@@ -0,0 +1,89 @@
{
"name": "js-lingui-solid-workspaces",
"private": true,
"version": "5.0.0",
"license": "MIT",
"scripts": {
"test": "jest",
"test:ci": "jest --ci --runInBand",
"test:ci:coverage": "yarn test:ci --coverage",
"test:e2e": "yarn workspaces foreach -A -p run test:e2e",
"test:tsd": "jest -c jest.config.types.js",
"test:all": "yarn test && yarn test:e2e && yarn test:tsd",
"lint:types": "tsc",
"lint:eslint": "eslint ./packages",
"lint:all": "yarn lint:eslint && yarn lint:types",
"prettier": "prettier --write '**/*.{ts,tsx,js,jsx}'",
"prettier:check": "prettier --check '**/*.{ts,tsx,js,jsx}'",
"verdaccio:release": "node -r @swc-node/register ./scripts/verdaccio-release.ts",
"release:build": "yarn workspaces foreach --topological-dev -A -v run build",
"release:test": "yarn release:build && yarn test:all",
"version": "yarn install --no-immutable && git stage yarn.lock",
"version:next": "lerna version --exact --force-publish --no-private --preid next --create-release github --conventional-commits --conventional-prerelease --yes",
"version:latest": "lerna version --exact --force-publish --no-private --create-release github --conventional-commits --yes",
"release:latest": "lerna publish from-package --dist-tag latest --yes",
"release:next": "lerna publish from-package --canary --preid next --pre-dist-tag next --yes",
"build:docs": "cd website && yarn install && yarn build",
"size-limit": "size-limit",
"stub:all": "yarn workspaces foreach -A -p run stub",
"build": "yarn workspaces foreach -A -p run build",
"prepare": "husky install"
},
"engines": {
"node": ">=20.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@eslint/js": "^9.17.0",
"@size-limit/preset-small-lib": "^11.1.6",
"@swc/core": "^1.3.26",
"@tsd/typescript": "^4.9.5",
"@types/jest": "^29.4.0",
"@types/mock-fs": "^4.13.1",
"@types/node": "20.14.8",
"babel-jest": "^29.7.0",
"chalk": "^4.1.0",
"cross-env": "^7.0.2",
"eslint": "^9.17.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.2",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node-single-context": "^29.4.0",
"jest-runner-tsd": "^4.0.0",
"lerna": "^8.1.9",
"lint-staged": "^13.1.0",
"memory-fs": "^0.5.0",
"minimist": "^1.2.5",
"mock-fs": "^5.2.0",
"npm-cli-login": "^0.1.1",
"ora": "^5.1.0",
"prettier": "2.8.3",
"rimraf": "^3.0.2",
"semver": "^7.3.2",
"size-limit": "^11.1.6",
"strip-ansi": "^6.0.1",
"swc-node": "^1.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.1"
},
"workspaces": [
"packages/*",
"test/*"
],
"repository": {
"type": "git",
"url": "https://github.com/Azq2/js-lingui-solid.git"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"prettier --write --ignore-unknown",
"eslint --fix"
]
},
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728"
}
@@ -0,0 +1,319 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.2](https://github.com/lingui/js-lingui/compare/v5.1.1...v5.1.2) (2024-12-16)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [5.1.1](https://github.com/lingui/js-lingui/compare/v5.1.0...v5.1.1) (2024-12-16)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [5.1.0](https://github.com/lingui/js-lingui/compare/v5.0.0...v5.1.0) (2024-12-06)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.14.1](https://github.com/lingui/js-lingui/compare/v4.14.0...v4.14.1) (2024-11-28)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.14.0](https://github.com/lingui/js-lingui/compare/v4.13.0...v4.14.0) (2024-11-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.13.0](https://github.com/lingui/js-lingui/compare/v4.12.0...v4.13.0) (2024-10-15)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.12.0](https://github.com/lingui/js-lingui/compare/v4.11.4...v4.12.0) (2024-10-11)
### Features
- enable importAttributes and explicitResourceManagement for extractor ([#2009](https://github.com/lingui/js-lingui/issues/2009)) ([c20ce12](https://github.com/lingui/js-lingui/commit/c20ce12dbc3edaf476fd745df7e8f8b1390afe95))
## [4.11.4](https://github.com/lingui/js-lingui/compare/v4.11.3...v4.11.4) (2024-09-02)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.11.3](https://github.com/lingui/js-lingui/compare/v4.11.2...v4.11.3) (2024-08-09)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.11.2](https://github.com/lingui/js-lingui/compare/v4.11.1...v4.11.2) (2024-07-03)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.11.1](https://github.com/lingui/js-lingui/compare/v4.11.0...v4.11.1) (2024-05-30)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.11.0](https://github.com/lingui/js-lingui/compare/v4.10.1...v4.11.0) (2024-05-17)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.10.1](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.10.1) (2024-05-03)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.10.0](https://github.com/lingui/js-lingui/compare/v4.8.0...v4.10.0) (2024-04-12)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.9.0](https://github.com/lingui/js-lingui/compare/v4.8.0...v4.9.0) (2024-04-12)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.8.0](https://github.com/lingui/js-lingui/compare/v4.7.2...v4.8.0) (2024-04-03)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.7.2](https://github.com/lingui/js-lingui/compare/v4.7.1...v4.7.2) (2024-03-26)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.7.1](https://github.com/lingui/js-lingui/compare/v4.7.0...v4.7.1) (2024-02-20)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.7.0](https://github.com/lingui/js-lingui/compare/v4.6.0...v4.7.0) (2024-01-05)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.6.0](https://github.com/lingui/js-lingui/compare/v4.5.0...v4.6.0) (2023-12-01)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.5.0](https://github.com/lingui/js-lingui/compare/v4.4.2...v4.5.0) (2023-09-14)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.4.2](https://github.com/lingui/js-lingui/compare/v4.4.1...v4.4.2) (2023-08-31)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.4.1](https://github.com/lingui/js-lingui/compare/v4.4.0...v4.4.1) (2023-08-30)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.4.0](https://github.com/lingui/js-lingui/compare/v4.3.0...v4.4.0) (2023-08-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.3.0](https://github.com/lingui/js-lingui/compare/v4.2.1...v4.3.0) (2023-06-29)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.2.1](https://github.com/lingui/js-lingui/compare/v4.2.0...v4.2.1) (2023-06-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.2.0](https://github.com/lingui/js-lingui/compare/v4.1.2...v4.2.0) (2023-05-26)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.1.2](https://github.com/lingui/js-lingui/compare/v4.1.1...v4.1.2) (2023-05-17)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [4.1.1](https://github.com/lingui/js-lingui/compare/v4.1.0...v4.1.1) (2023-05-17)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [4.1.0](https://github.com/lingui/js-lingui/compare/v4.0.0...v4.1.0) (2023-05-15)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.17.2](https://github.com/lingui/js-lingui/compare/v3.17.1...v3.17.2) (2023-02-24)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.17.1](https://github.com/lingui/js-lingui/compare/v3.17.0...v3.17.1) (2023-02-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.17.0](https://github.com/lingui/js-lingui/compare/v3.16.1...v3.17.0) (2023-02-01)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.16.1](https://github.com/lingui/js-lingui/compare/v3.16.0...v3.16.1) (2023-01-24)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.16.0](https://github.com/lingui/js-lingui/compare/v3.15.0...v3.16.0) (2023-01-18)
### Features
- allow extract to work with i18n.\_ calls not created from macro ([#1309](https://github.com/lingui/js-lingui/issues/1309)) ([90be171](https://github.com/lingui/js-lingui/commit/90be1719becc4710c910ea16928b7ce41ef9ab19))
# [3.15.0](https://github.com/lingui/js-lingui/compare/v3.14.0...v3.15.0) (2022-11-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.14.0](https://github.com/lingui/js-lingui/compare/v3.13.3...v3.14.0) (2022-06-22)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.13.3](https://github.com/lingui/js-lingui/compare/v3.13.2...v3.13.3) (2022-04-24)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.13.2](https://github.com/lingui/js-lingui/compare/v3.13.1...v3.13.2) (2022-01-24)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.13.1](https://github.com/lingui/js-lingui/compare/v3.13.0...v3.13.1) (2022-01-21)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.13.0](https://github.com/lingui/js-lingui/compare/v3.12.1...v3.13.0) (2021-11-26)
### Features
- add the ability to extract concatenated comments ([#1152](https://github.com/lingui/js-lingui/issues/1152)) ([0e553cf](https://github.com/lingui/js-lingui/commit/0e553cf14f5f6dce87839abed76fd21f351a2eae))
- msgctxt support ([#1094](https://github.com/lingui/js-lingui/issues/1094)) ([8ee42cb](https://github.com/lingui/js-lingui/commit/8ee42cbfe26bc6d055748dcf2713ab8ade7ec827))
## [3.12.1](https://github.com/lingui/js-lingui/compare/v3.12.0...v3.12.1) (2021-09-28)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.12.0](https://github.com/lingui/js-lingui/compare/v3.11.1...v3.12.0) (2021-09-28)
### Bug Fixes
- **#1137:** configPath is passed through babel-plugin-extract-messages ([#1140](https://github.com/lingui/js-lingui/issues/1140)) ([8921156](https://github.com/lingui/js-lingui/commit/89211567632733cf9955cafc9c92bd87c6154852)), closes [#1137](https://github.com/lingui/js-lingui/issues/1137)
## [3.11.1](https://github.com/lingui/js-lingui/compare/v3.11.0...v3.11.1) (2021-09-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.11.0](https://github.com/lingui/js-lingui/compare/v3.10.4...v3.11.0) (2021-09-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.10.4](https://github.com/lingui/js-lingui/compare/v3.10.3...v3.10.4) (2021-06-16)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.10.3](https://github.com/lingui/js-lingui/compare/v3.10.2...v3.10.3) (2021-06-14)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.10.2](https://github.com/lingui/js-lingui/compare/v3.10.1...v3.10.2) (2021-06-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.10.1](https://github.com/lingui/js-lingui/compare/v3.10.0...v3.10.1) (2021-06-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.10.0](https://github.com/lingui/js-lingui/compare/v3.9.0...v3.10.0) (2021-06-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.9.0](https://github.com/lingui/js-lingui/compare/v3.8.10...v3.9.0) (2021-05-18)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.10](https://github.com/lingui/js-lingui/compare/v3.8.9...v3.8.10) (2021-04-19)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.9](https://github.com/lingui/js-lingui/compare/v3.8.8...v3.8.9) (2021-04-09)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.8](https://github.com/lingui/js-lingui/compare/v3.8.7...v3.8.8) (2021-04-09)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.7](https://github.com/lingui/js-lingui/compare/v3.8.6...v3.8.7) (2021-04-09)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.6](https://github.com/lingui/js-lingui/compare/v3.8.5...v3.8.6) (2021-04-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.5](https://github.com/lingui/js-lingui/compare/v3.8.4...v3.8.5) (2021-04-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.4](https://github.com/lingui/js-lingui/compare/v3.8.3...v3.8.4) (2021-04-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.3](https://github.com/lingui/js-lingui/compare/v3.8.2...v3.8.3) (2021-04-05)
### Bug Fixes
- extract works with template string id's ([#1027](https://github.com/lingui/js-lingui/issues/1027)) ([a17d629](https://github.com/lingui/js-lingui/commit/a17d629d82395cd86cc080648ef2ebe2a9653225))
## [3.8.2](https://github.com/lingui/js-lingui/compare/v3.8.1...v3.8.2) (2021-03-31)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.8.1](https://github.com/lingui/js-lingui/compare/v3.8.0...v3.8.1) (2021-03-23)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.8.0](https://github.com/lingui/js-lingui/compare/v3.7.2...v3.8.0) (2021-03-23)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.7.2](https://github.com/lingui/js-lingui/compare/v3.7.1...v3.7.2) (2021-03-14)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.7.1](https://github.com/lingui/js-lingui/compare/v3.7.0...v3.7.1) (2021-03-07)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.7.0](https://github.com/lingui/js-lingui/compare/v3.6.0...v3.7.0) (2021-03-04)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.6.0](https://github.com/lingui/js-lingui/compare/v3.5.1...v3.6.0) (2021-02-23)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.5.1](https://github.com/lingui/js-lingui/compare/v3.5.0...v3.5.1) (2021-02-09)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.5.0](https://github.com/lingui/js-lingui/compare/v3.4.0...v3.5.0) (2021-02-02)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.4.0](https://github.com/lingui/js-lingui/compare/v3.3.0...v3.4.0) (2021-01-13)
### Bug Fixes
- prevent adding undefined msgid to messages ([#915](https://github.com/lingui/js-lingui/issues/915)) ([3afacec](https://github.com/lingui/js-lingui/commit/3afaceccb669b59ee2f5b42ee2e138646ccdb79d))
# [3.3.0](https://github.com/lingui/js-lingui/compare/v3.2.3...v3.3.0) (2020-12-08)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.2.3](https://github.com/lingui/js-lingui/compare/v3.2.2...v3.2.3) (2020-11-22)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
## [3.2.2](https://github.com/lingui/js-lingui/compare/v3.2.1...v3.2.2) (2020-11-20)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
# [3.2.0](https://github.com/lingui/js-lingui/compare/v3.1.0...v3.2.0) (2020-11-12)
### Bug Fixes
- t macro as function not extracting ([#846](https://github.com/lingui/js-lingui/issues/846)) ([d819bfc](https://github.com/lingui/js-lingui/commit/d819bfc74707a8766bfe1b1a3d43edce97f8f265))
### Features
- extract multiple comments per translation ID ([#854](https://github.com/lingui/js-lingui/issues/854)) ([c849c9c](https://github.com/lingui/js-lingui/commit/c849c9c024832aa7b07e5f837791e287c3aebe29))
# [3.1.0](https://github.com/lingui/js-lingui/compare/v3.0.3...v3.1.0) (2020-11-10)
**Note:** Version bump only for package @lingui-solid/babel-plugin-extract-messages
@@ -0,0 +1,54 @@
[![License][badge-license]][license]
[![Version][badge-version]][package]
[![Downloads][badge-downloads]][package]
# @lingui-solid/babel-plugin-extract-messages
> Babel plugin which extracts messages for translation from source files
`@lingui-solid/babel-plugin-extract-messages` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.
## Installation
```sh
npm install --save-dev @lingui-solid/babel-plugin-extract-messages
# yarn add --dev @lingui-solid/babel-plugin-extract-messages
```
## Usage
### Via `.babelrc` (Recommended)
**.babelrc**
```json
{
"plugins": ["@lingui-solid/babel-plugin-extract-messages"]
}
```
### Via CLI
```bash
babel --plugins @lingui-solid/babel-plugin-extract-messages script.js
```
### Via Node API
```js
require("@babel/core").transform("code", {
plugins: ["@lingui-solid/babel-plugin-extract-messages"]
})
```
## License
[MIT][license]
[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
[linguijs]: https://github.com/lingui/js-lingui
[documentation]: https://lingui.dev
[package]: https://www.npmjs.com/package/@lingui-solid/babel-plugin-extract-messages
[badge-downloads]: https://img.shields.io/npm/dw/@lingui-solid/babel-plugin-extract-messages.svg
[badge-version]: https://img.shields.io/npm/v/@lingui-solid/babel-plugin-extract-messages.svg
[badge-license]: https://img.shields.io/npm/l/@lingui-solid/babel-plugin-extract-messages.svg
@@ -0,0 +1,5 @@
import { defineBuildConfig } from "unbuild"
export default defineBuildConfig({
externals: ["@babel/core", "@babel/types", "@babel/traverse"],
})
@@ -0,0 +1,51 @@
{
"name": "@lingui-solid/babel-plugin-extract-messages",
"version": "5.1.2",
"description": "Babel plugin for collecting messages from source code for internationalization",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"author": {
"name": "Tomáš Ehrlich",
"email": "tomas.ehrlich@gmail.com"
},
"license": "MIT",
"keywords": [
"babel-plugin",
"i18n",
"internationalization",
"i10n",
"localization",
"i9n",
"translation",
"multilingual"
],
"scripts": {
"build": "rimraf ./dist && unbuild",
"stub": "unbuild --stub"
},
"files": [
"LICENSE",
"README.md",
"dist/"
],
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=20.0.0"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/traverse": "^7.20.12",
"@babel/types": "^7.20.7",
"@lingui-solid/babel-plugin-lingui-macro": "workspace:*",
"@lingui/jest-mocks": "workspace:*",
"rimraf": "^6.0.1",
"unbuild": "2.0.0"
}
}
@@ -0,0 +1,399 @@
import type * as BabelTypesNamespace from "@babel/types"
import {
Expression,
Identifier,
Node,
ObjectExpression,
ObjectProperty,
isObjectExpression,
} from "@babel/types"
import type { PluginObj, PluginPass, NodePath } from "@babel/core"
import type { Hub } from "@babel/traverse"
type BabelTypes = typeof BabelTypesNamespace
export type ExtractedMessage = {
id: string
message?: string
context?: string
origin?: Origin
comment?: string
placeholders?: Record<string, string>
}
export type ExtractPluginOpts = {
onMessageExtracted(msg: ExtractedMessage): void
}
type RawMessage = {
id?: string
message?: string
comment?: string
context?: string
placeholders?: Record<string, string>
}
export type Origin = [filename: string, line: number, column?: number]
function collectMessage(
path: NodePath<any>,
props: RawMessage,
ctx: PluginPass
) {
// prevent from adding undefined msgid
if (props.id === undefined) return
const node: Node = path.node
const line = node.loc ? node.loc.start.line : null
const column = node.loc ? node.loc.start.column : null
;(ctx.opts as ExtractPluginOpts).onMessageExtracted({
id: props.id,
message: props.message,
context: props.context,
comment: props.comment,
placeholders: props.placeholders || {},
origin: [ctx.file.opts.filename, line, column],
})
}
function getTextFromExpression(
t: BabelTypes,
exp: Expression,
hub: Hub,
emitErrorOnVariable = true
): string {
if (t.isStringLiteral(exp)) {
return exp.value
}
if (t.isBinaryExpression(exp)) {
return (
getTextFromExpression(
t,
exp.left as Expression,
hub,
emitErrorOnVariable
) +
getTextFromExpression(
t,
exp.right as Expression,
hub,
emitErrorOnVariable
)
)
}
if (t.isTemplateLiteral(exp)) {
if (exp?.quasis.length > 1) {
console.warn(
hub.buildError(
exp,
"Could not extract from template literal with expressions.",
SyntaxError
).message
)
return ""
}
return exp.quasis[0]?.value?.cooked
}
if (emitErrorOnVariable) {
console.warn(
hub.buildError(
exp,
"Only strings or template literals could be extracted.",
SyntaxError
).message
)
}
}
function getNodeSource(fileContents: string, node: Node) {
return fileContents.slice(node.start, node.end)
}
function valuesObjectExpressionToPlaceholdersRecord(
t: BabelTypes,
exp: ObjectExpression,
hub: Hub
) {
const props: Record<string, string> = {}
;(exp.properties as ObjectProperty[]).forEach(({ key, value }, i) => {
let name: string
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) {
name = key.value.toString()
} else if (t.isIdentifier(key)) {
name = key.name
} else {
console.warn(
hub.buildError(
exp,
`Could not extract values to placeholders. The key #${i} has unsupported syntax`,
SyntaxError
).message
)
}
if (name) {
props[name] = getNodeSource(hub.getCode(), value)
}
})
return props
}
function extractFromObjectExpression(
t: BabelTypes,
exp: ObjectExpression,
hub: Hub
) {
const props: RawMessage = {}
const textKeys = ["id", "message", "comment", "context"] as const
;(exp.properties as ObjectProperty[]).forEach(({ key, value }, i) => {
const name = (key as Identifier).name
if (name === "values" && isObjectExpression(value)) {
props.placeholders = valuesObjectExpressionToPlaceholdersRecord(
t,
value,
hub
)
} else if (textKeys.includes(name as any)) {
props[name as (typeof textKeys)[number]] = getTextFromExpression(
t,
value as Expression,
hub
)
}
})
return props
}
const I18N_OBJECT = "i18n"
function hasComment(node: Node, comment: string): boolean {
return (
node.leadingComments &&
node.leadingComments.some((comm) => comm.value.trim() === comment)
)
}
function hasIgnoreComment(node: Node): boolean {
return hasComment(node, "lingui-extract-ignore")
}
function hasI18nComment(node: Node): boolean {
return hasComment(node, "i18n")
}
export default function ({ types: t }: { types: BabelTypes }): PluginObj {
function isTransComponent(path: NodePath) {
return (
path.isJSXElement() &&
(
path
.get("openingElement")
.get("name")
.referencesImport("@lingui/react", "Trans") ||
path
.get("openingElement")
.get("name")
.referencesImport("@lingui-solid/solid", "Trans")
)
)
}
const isI18nMethod = (node: Node) =>
t.isMemberExpression(node) &&
(t.isIdentifier(node.object, { name: I18N_OBJECT }) ||
(t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, { name: I18N_OBJECT }))) &&
(t.isIdentifier(node.property, { name: "_" }) ||
t.isIdentifier(node.property, { name: "t" }))
const extractFromMessageDescriptor = (
path: NodePath<ObjectExpression>,
ctx: PluginPass
) => {
const props = extractFromObjectExpression(t, path.node, ctx.file.hub)
if (!props.id) {
console.warn(
path.buildCodeFrameError("Missing message ID, skipping.").message
)
return
}
collectMessage(path, props, ctx)
}
return {
visitor: {
// Extract translation from <Trans /> component.
JSXElement(path, ctx) {
const { node } = path
if (!isTransComponent(path)) return
const attrs = node.openingElement.attributes || []
if (
attrs.find(
(attr) =>
t.isJSXSpreadAttribute(attr) && hasI18nComment(attr.argument)
)
) {
return
}
const props = attrs.reduce<RawMessage>((acc, item) => {
if (t.isJSXSpreadAttribute(item)) {
return acc
}
const key = item.name.name
if (
key === "id" ||
key === "message" ||
key === "comment" ||
key === "context"
) {
if (t.isStringLiteral(item.value)) {
acc[key] = item.value.value
} else if (
t.isJSXExpressionContainer(item.value) &&
t.isStringLiteral(item.value.expression)
) {
acc[key] = item.value.expression.value
}
}
if (
key === "values" &&
t.isJSXExpressionContainer(item.value) &&
isObjectExpression(item.value.expression)
) {
acc.placeholders = valuesObjectExpressionToPlaceholdersRecord(
t,
item.value.expression,
ctx.file.hub
)
}
return acc
}, {})
if (!props.id) {
// <Trans id={message} /> is valid, don't raise warning
const idProp = attrs.filter(
(item) => t.isJSXAttribute(item) && item.name.name === "id"
)[0]
if (idProp === undefined || t.isLiteral(props.id as any)) {
console.warn(
path.buildCodeFrameError("Missing message ID, skipping.").message
)
}
return
}
collectMessage(path, props, ctx)
},
CallExpression(path, ctx) {
if ([path.node, path.parent].some((node) => hasIgnoreComment(node))) {
return
}
const firstArgument = path.get("arguments")[0]
// i18n._(...)
if (!isI18nMethod(path.node.callee)) {
return
}
// call with explicit annotation
// i18n._(/*i18n*/ {descriptor})
// skipping this as it is processed
// by ObjectExpression visitor
if (hasI18nComment(firstArgument.node)) {
return
}
if (firstArgument.isObjectExpression()) {
// i8n._({message, id, context})
extractFromMessageDescriptor(firstArgument, ctx)
return
} else {
// i18n._(id, variables, descriptor)
let props: RawMessage = {
id: getTextFromExpression(
t,
firstArgument.node as Expression,
ctx.file.hub,
false
),
}
if (!props.id) {
return
}
const secondArgument = path.node.arguments[1]
if (secondArgument && t.isObjectExpression(secondArgument)) {
props.placeholders = valuesObjectExpressionToPlaceholdersRecord(
t,
secondArgument,
ctx.file.hub
)
}
const msgDescArg = path.node.arguments[2]
if (t.isObjectExpression(msgDescArg)) {
props = {
...props,
...extractFromObjectExpression(t, msgDescArg, ctx.file.hub),
}
}
collectMessage(path, props, ctx)
}
},
StringLiteral(path, ctx) {
if (!hasI18nComment(path.node)) {
return
}
const props = {
id: path.node.value,
}
if (!props.id) {
console.warn(
path.buildCodeFrameError("Empty StringLiteral, skipping.").message
)
return
}
collectMessage(path, props, ctx)
},
// Extract message descriptors
ObjectExpression(path, ctx) {
if (!hasI18nComment(path.node)) return
extractFromMessageDescriptor(path, ctx)
},
},
}
}
@@ -0,0 +1,527 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@lingui-solid/babel-plugin-extract-messages CallExpression i18n._() should extract messages from i18n._ call expressions 1`] = `
[
{
comment: undefined,
context: undefined,
id: Message,
message: undefined,
origin: [
js-call-expression.js,
1,
],
placeholders: {},
},
{
comment: description,
context: undefined,
id: Description,
message: undefined,
origin: [
js-call-expression.js,
3,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: ID,
message: Message with id,
origin: [
js-call-expression.js,
5,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: Values {param},
message: undefined,
origin: [
js-call-expression.js,
7,
],
placeholders: {
param: param,
},
},
{
comment: undefined,
context: Context1,
id: Some id,
message: undefined,
origin: [
js-call-expression.js,
9,
],
placeholders: {},
},
{
comment: My comment,
context: undefined,
id: my.id,
message: My Id Message,
origin: [
js-call-expression.js,
12,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: Aliased Message,
message: undefined,
origin: [
js-call-expression.js,
19,
],
placeholders: {},
},
{
comment: My comment,
context: undefined,
id: my.id,
message: My Id Message,
origin: [
js-call-expression.js,
22,
],
placeholders: {},
},
]
`;
exports[`@lingui-solid/babel-plugin-extract-messages MessageDescriptor should extract messages from MessageDescriptors 1`] = `
[
{
comment: undefined,
context: undefined,
id: Message,
message: undefined,
origin: [
js-message-descriptor.js,
1,
],
placeholders: {},
},
{
comment: description,
context: undefined,
id: Description,
message: undefined,
origin: [
js-message-descriptor.js,
3,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: ID,
message: Message with id,
origin: [
js-message-descriptor.js,
5,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: Values {param} {0} {name} {value},
message: undefined,
origin: [
js-message-descriptor.js,
7,
],
placeholders: {
0: user.getName(),
name: "foo",
param: param,
value: user
? user.name
: null,
},
},
{
comment: undefined,
context: undefined,
id: Values {param} {0},
message: undefined,
origin: [
js-message-descriptor.js,
23,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: Some id,
message: undefined,
origin: [
js-message-descriptor.js,
25,
],
placeholders: {},
},
]
`;
exports[`@lingui-solid/babel-plugin-extract-messages should extract Plural messages from JSX files when there's no Trans tag (integration) 1`] = `
[
{
comment: undefined,
context: undefined,
id: esnaQO,
message: {count, plural, one {# book} other {# books}},
origin: [
jsx-without-trans.js,
2,
],
placeholders: {
count: count,
},
},
{
comment: undefined,
context: Some context,
id: 8qNz+K,
message: {count, plural, one {# book} other {# books}},
origin: [
jsx-without-trans.js,
3,
],
placeholders: {
count: count,
},
},
]
`;
exports[`@lingui-solid/babel-plugin-extract-messages should extract all messages from JS files (macros) 1`] = `
[
{
comment: undefined,
context: undefined,
id: xDAtGP,
message: Message,
origin: [
js-with-macros.js,
4,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: xDAtGP,
message: Message,
origin: [
js-with-macros.js,
6,
],
placeholders: {},
},
{
comment: description,
context: undefined,
id: Nu4oKW,
message: Description,
origin: [
js-with-macros.js,
8,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: ID,
message: Message with id,
origin: [
js-with-macros.js,
13,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: QCVtWw,
message: Values {param},
origin: [
js-with-macros.js,
18,
],
placeholders: {
param: param,
},
},
{
comment: undefined,
context: undefined,
id: ID Some,
message: Message with id some,
origin: [
js-with-macros.js,
20,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: Backtick,
message: undefined,
origin: [
js-with-macros.js,
25,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: Some ID,
message: undefined,
origin: [
js-with-macros.js,
29,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: Some other ID,
message: undefined,
origin: [
js-with-macros.js,
34,
],
placeholders: {},
},
{
comment: undefined,
context: Context2,
id: Some ID,
message: undefined,
origin: [
js-with-macros.js,
39,
],
placeholders: {},
},
{
comment: undefined,
context: Context2,
id: Some ID,
message: undefined,
origin: [
js-with-macros.js,
44,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: sD7MQ4,
message: TplLiteral,
origin: [
js-with-macros.js,
49,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: VO4BJY,
message: [useLingui]: TplLiteral,
origin: [
js-with-macros.js,
54,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: ZxxjOE,
message: [useLingui]: Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}},
origin: [
js-with-macros.js,
57,
],
placeholders: {
0: users.length,
},
},
]
`;
exports[`@lingui-solid/babel-plugin-extract-messages should extract all messages from JSX files (macros) 1`] = `
[
{
comment: undefined,
context: undefined,
id: d1Kdl3,
message: Hi, my name is {name},
origin: [
jsx-with-macros.js,
3,
],
placeholders: {
name: name,
},
},
{
comment: undefined,
context: Context1,
id: YikuIL,
message: Some message,
origin: [
jsx-with-macros.js,
4,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: LBCs5C,
message: Some other message,
origin: [
jsx-with-macros.js,
5,
],
placeholders: {},
},
{
comment: undefined,
context: Context2,
id: ru2rzr,
message: Some message,
origin: [
jsx-with-macros.js,
6,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: MHrjPM,
message: Title,
origin: [
jsx-with-macros.js,
7,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: esnaQO,
message: {count, plural, one {# book} other {# books}},
origin: [
jsx-with-macros.js,
9,
],
placeholders: {
count: count,
},
},
]
`;
exports[`@lingui-solid/babel-plugin-extract-messages should extract all messages from JSX files 1`] = `
[
{
comment: Description,
context: undefined,
id: msg.hello,
message: undefined,
origin: [
jsx-without-macros.js,
5,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: msg.context,
message: undefined,
origin: [
jsx-without-macros.js,
6,
],
placeholders: {},
},
{
comment: undefined,
context: Context1,
id: msg.notcontext,
message: undefined,
origin: [
jsx-without-macros.js,
7,
],
placeholders: {},
},
{
comment: undefined,
context: Context2,
id: msg.context,
message: undefined,
origin: [
jsx-without-macros.js,
8,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: msg.default,
message: Hello World,
origin: [
jsx-without-macros.js,
9,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: msg.default,
message: Hello World,
origin: [
jsx-without-macros.js,
10,
],
placeholders: {},
},
{
comment: undefined,
context: undefined,
id: Hi, my name is <0>{name}</0>,
message: undefined,
origin: [
jsx-without-macros.js,
11,
],
placeholders: {
count: count,
},
},
]
`;
@@ -0,0 +1,26 @@
const msg = i18n._("Message")
const withDescription = i18n._("Description", {}, { comment: "description" })
const withId = i18n._("ID", {}, { message: "Message with id" })
const withValues = i18n._("Values {param}", { param: param })
const withContext = i18n._("Some id", {}, { context: "Context1" })
// from message descriptor
i18n._({
id: "my.id",
message: "My Id Message",
comment: "My comment",
})
// support alias
i18n.t("Aliased Message")
// from message descriptor
i18n.t({
id: "my.id",
message: "My Id Message",
comment: "My comment",
})
@@ -0,0 +1,25 @@
const msg = /*i18n*/ { id: "Message" }
const withDescription = /*i18n*/ { id: "Description", comment: "description" }
const withId = /*i18n*/ { id: "ID", message: "Message with id" }
const withValues = /*i18n*/ {
id: "Values {param} {0} {name} {value}",
values: {
param: param,
0: user.getName(),
["name"]: "foo",
// prettier-ignore
value: user
? user.name
: null,
},
}
/**
* With values passed as variable
*/
const values = {}
const withValues2 = /*i18n*/ { id: "Values {param} {0}", values }
const withContext = /*i18n*/ { id: "Some id", context: "Context1" }
@@ -0,0 +1,63 @@
import { t, defineMessage, msg, plural } from "@lingui/core/macro"
import { useLingui } from "@lingui/react/macro"
t`Message`
const msg1 = t`Message`
const withDescription = defineMessage({
message: "Description",
comment: "description",
})
const withId = defineMessage({
id: "ID",
message: "Message with id",
})
const withValues = t`Values ${param}`
const withTId = t({
id: "ID Some",
message: "Message with id some",
})
const withTIdBacktick = t({
id: `Backtick`,
})
const tWithContextA = t({
id: "Some ID",
context: "Context1",
})
const tWithContextB = t({
id: "Some other ID",
context: "Context1",
})
const defineMessageWithContext = defineMessage({
id: "Some ID",
context: "Context2",
})
const defineMessageAlias = msg({
id: "Some ID",
context: "Context2",
})
const defineMessageAlias2 = msg`TplLiteral`
function MyComponent() {
const { t } = useLingui()
t`[useLingui]: TplLiteral`
// macro nesting
const a = t`[useLingui]: Text ${plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: "# books",
})}`
}
@@ -0,0 +1,13 @@
import { Trans } from "@lingui/react/macro"
import { t, plural } from "@lingui/core/macro"
;<Trans>Hi, my name is {name}</Trans>
;<Trans context="Context1">Some message</Trans>
;<Trans context="Context1">Some other message</Trans>
;<Trans context="Context2">Some message</Trans>
;<span title={t`Title`} />
;<span
title={plural(count, {
one: "# book",
other: "# books",
})}
/>
@@ -0,0 +1,12 @@
import { Trans } from "@lingui/react"
;<span id="ignore" />
;<Trans id={"msg.hello"} comment="Description" />
;<Trans id="msg.context" context="Context1" />
;<Trans id="msg.notcontext" context="Context1" />
;<Trans id="msg.context" context="Context2" />
;<Trans id="msg.default" message="Hello World" />
;<Trans id="msg.default" message="Hello World" />
;<Trans id="Hi, my name is <0>{name}</0>" values={{ count }} />
@@ -0,0 +1,3 @@
import { Plural } from "@lingui/react/macro"
;<Plural value={count} one="# book" other="# books" />
;<Plural value={count} one="# book" other="# books" context="Some context" />
@@ -0,0 +1,3 @@
module.exports = {
locales: ["en", "cs"]
}
@@ -0,0 +1,7 @@
import { Select } from "awesome-form-lib"
import { Trans } from "awesome-animation-lib"
;<Trans x={-50} y={20}>
Displaced element
</Trans>
;<Select value={50} onChange={() => {}} />
@@ -0,0 +1,401 @@
import path from "path"
import fs from "fs"
import { transform as babelTransform } from "@babel/core"
import plugin, { ExtractedMessage, ExtractPluginOpts } from "../src/index"
import { mockConsole } from "@lingui/jest-mocks"
import linguiMacroPlugin, {
type LinguiPluginOpts,
} from "@lingui-solid/babel-plugin-lingui-macro"
const transform = (filename: string) => {
const rootDir = path.join(__dirname, "fixtures")
const filePath = path.join(rootDir, filename)
const code = fs.readFileSync(filePath).toString()
return transformCode(code, filePath, rootDir)
}
const transformCode = (
code: string,
filename = "test-case.js",
rootDir = "."
) => {
process.env.LINGUI_CONFIG = path.join(
__dirname,
"fixtures",
"lingui.config.js"
)
const messages: ExtractedMessage[] = []
try {
const pluginOpts: ExtractPluginOpts = {
onMessageExtracted(msg: ExtractedMessage) {
const filename = path.relative(rootDir, msg.origin[0])
messages.push({
...msg,
origin: [filename, msg.origin[1]],
})
},
}
babelTransform(code, {
configFile: false,
filename,
plugins: [
"@babel/plugin-syntax-jsx",
[
linguiMacroPlugin,
{
extract: true,
} satisfies LinguiPluginOpts,
],
[plugin, pluginOpts],
],
})
} finally {
process.env.LINGUI_CONFIG = null
}
return messages
}
function expectNoConsole(cb: () => void) {
return mockConsole((console) => {
cb()
expect(console.warn).not.toBeCalled()
expect(console.error).not.toBeCalled()
})
}
describe("@lingui-solid/babel-plugin-extract-messages", function () {
it("should ignore files without lingui import", () => {
expectNoConsole(() => {
const messages = transform("without-lingui.js")
expect(messages.length).toBe(0)
})
})
it("should extract all messages from JSX files", () => {
expectNoConsole(() => {
const messages = transform("jsx-without-macros.js")
expect(messages).toMatchSnapshot()
})
})
describe("JSX", () => {
it("Should not rise warning when translation from variable", () => {
const code = `
import { Trans } from "@lingui/react";
<Trans id={message} />;
<Trans id={message.field} />;
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
})
})
it("Should not rise warning when `key` used with macro", () => {
const code = `
import { Trans } from "@lingui/react/macro";
<Trans context="Context2" key={1}>
Some message
</Trans>
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(1)
})
})
it("Should log error when no ID provided", () => {
const code = `
import { Trans } from "@lingui/react";
<Trans />;
<Trans message="Missing ID" />;
`
mockConsole((console) => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(`Missing message ID`)
)
})
})
})
describe("CallExpression i18n._()", () => {
it("should extract messages from i18n._ call expressions", () => {
expectNoConsole(() => {
const messages = transform("js-call-expression.js")
expect(messages).toMatchSnapshot()
})
})
it("should extract from member access expressions", () => {
const code = `
// member access
ctx.i18n._("Message")
// member access any depth
ctx.req.i18n._("Message")
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(2)
})
})
it("should not extract from random i18n members", () => {
const code = `
i18n.load("Message")
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
})
})
it("should not extract if disabled via annotation", () => {
const code = `
/* lingui-extract-ignore */
i18n._("Message")
/* lingui-extract-ignore */
ctx.i18n._("Message")
/* lingui-extract-ignore */
ctx.req.i18n._("Message")
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
})
})
it("Should not rise warning when translation from variable", () => {
const code = `
i18n._(message);
// member expression
i18n._(foo.bar);
// function call
i18n._(getMessage());
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
})
})
it("Should throw error when not a string provided as comment", () => {
const code = `const msg = i18n._('message.id', {}, {comment: variable})`
return mockConsole((console) => {
transformCode(code)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"Only strings or template literals could be extracted."
)
)
})
})
it("Should support extract id from TplLiteral and Concatenation", () => {
const code = `
const msg = i18n._(\`message.id\`);
const msg2 = i18n._("second" + '.' + "id")
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages[0]).toMatchObject({
id: "message.id",
})
expect(messages[1]).toMatchObject({
id: "second.id",
})
})
})
it("Should support string concatenation", () => {
const code = `const msg = i18n._('message.id', {}, {comment: "first " + "second " + "third"})`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages[0]).toMatchObject({
comment: "first second third",
})
})
})
it("Should not double extract from call generated by macro and dont rise warnings", () => {
const code = `import { i18n } from "@lingui/core";
const msg =
i18n._(/*i18n*/
{
id: "Hello {name}",
values: {
name: name,
},
});
`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages.length).toBe(1)
})
})
})
describe("StringLiteral", () => {
it("Should extract from marked StringLiteral", () => {
const code = `const t = /*i18n*/'Message'`
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages[0]).toMatchObject({
id: "Message",
})
})
})
it("Should log error when empty StringLiteral marked for extraction", () => {
const code = `const t = /*i18n*/''`
return mockConsole((console) => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(`Empty StringLiteral`)
)
})
})
})
describe("MessageDescriptor", () => {
it("should extract messages from MessageDescriptors", () => {
expectNoConsole(() => {
const messages = transform("js-message-descriptor.js")
expect(messages).toMatchSnapshot()
})
})
it("Should extract id from TemplateLiteral", () => {
const code = "const msg = /*i18n*/{id: `Message`}"
expectNoConsole(() => {
const messages = transformCode(code)
expect(messages[0]).toMatchObject({
id: "Message",
})
})
})
it("Should log error if TemplateLiteral in id has expressions", () => {
const code = "const msg = /*i18n*/{id: `Hello ${name}`}"
return mockConsole((console) => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(
`Could not extract from template literal with expressions.`
)
)
})
})
it("Should log error when no ID provided", () => {
const code = "const msg = /*i18n*/ {message: `Hello ${name}`}"
return mockConsole((console) => {
const messages = transformCode(code)
expect(messages.length).toBe(0)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(`Missing message ID`)
)
})
})
it("Should log error when not a string provided as ID", () => {
const code = "const msg = /*i18n*/ {id: id}"
return mockConsole((console) => {
transformCode(code)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"Only strings or template literals could be extracted."
)
)
})
})
it("Should log error when not a string provided as comment", () => {
const code = `const msg = /*i18n*/ {id: "msg.id", comment: variable}`
return mockConsole((console) => {
transformCode(code)
expect(console.error).not.toBeCalled()
expect(console.warn).toBeCalledWith(
expect.stringContaining(
"Only strings or template literals could be extracted."
)
)
})
})
it("Should support string concatenation in comment", () => {
const code = `const msg = /*i18n*/ {id: "msg.id", comment: "first " + "second " + "third"}`
return expectNoConsole(() => {
const messages = transformCode(code)
expect(messages[0]).toMatchObject({
comment: "first second third",
})
})
})
})
it("should extract all messages from JSX files (macros)", () => {
return expectNoConsole(() => {
const messages = transform("jsx-with-macros.js")
expect(messages).toMatchSnapshot()
})
})
it("should extract Plural messages from JSX files when there's no Trans tag (integration)", () => {
return expectNoConsole(() => {
const messages = transform("jsx-without-trans.js")
expect(messages).toMatchSnapshot()
})
})
it("should extract all messages from JS files (macros)", () => {
return expectNoConsole(() => {
const messages = transform("js-with-macros.js")
expect(messages).toMatchSnapshot()
})
})
})
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"strictNullChecks": false
}
}
@@ -0,0 +1,16 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.2](https://github.com/lingui/js-lingui/compare/v5.1.1...v5.1.2) (2024-12-16)
**Note:** Version bump only for package @lingui-solid/babel-plugin-lingui-macro
## [5.1.1](https://github.com/lingui/js-lingui/compare/v5.1.0...v5.1.1) (2024-12-16)
**Note:** Version bump only for package @lingui-solid/babel-plugin-lingui-macro
# [5.1.0](https://github.com/lingui/js-lingui/compare/v5.0.0...v5.1.0) (2024-12-06)
**Note:** Version bump only for package @lingui-solid/babel-plugin-lingui-macro
@@ -0,0 +1,40 @@
[![License][badge-license]][license]
[![Version][badge-version]][package]
[![Downloads][badge-downloads]][package]
# @lingui-solid/babel-plugin-lingui-macro
> Babel plugin that does actual transforms of Lingui's Macros
`@lingui-solid/babel-plugin-lingui-macro` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.
## Installation
```sh
npm install --save-dev @lingui-solid/babel-plugin-lingui-macro
# yarn add --dev @lingui-solid/babel-plugin-lingui-macro
```
## Usage
### Via `.babelrc`
**.babelrc**
```json
{
"plugins": ["@lingui-solid/babel-plugin-lingui-macro"]
}
```
## License
[MIT][license]
[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
[linguijs]: https://github.com/lingui/js-lingui
[documentation]: https://lingui.dev
[package]: https://www.npmjs.com/package/@lingui-solid/babel-plugin-lingui-macro
[badge-downloads]: https://img.shields.io/npm/dw/@lingui-solid/babel-plugin-lingui-macro.svg
[badge-version]: https://img.shields.io/npm/v/@lingui-solid/babel-plugin-lingui-macro.svg
[badge-license]: https://img.shields.io/npm/l/@lingui-solid/babel-plugin-lingui-macro.svg
@@ -0,0 +1,109 @@
{
"name": "@lingui-solid/babel-plugin-lingui-macro",
"version": "5.1.2",
"description": "Babel plugin for transforming Lingui Macros",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"contributors": [
{
"name": "Timofei Iatsenko",
"email": "timiatsenko@gmail.com"
}
],
"author": {
"name": "Tomáš Ehrlich",
"email": "tomas.ehrlich@gmail.com"
},
"publishConfig": {
"access": "public"
},
"license": "MIT",
"keywords": [
"babel-plugin",
"i18n",
"internationalization",
"i10n",
"localization",
"i9n",
"translation",
"multilingual"
],
"scripts": {
"build": "rimraf ./dist && unbuild",
"stub": "unbuild --stub"
},
"exports": {
".": {
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"./macro": {
"require": {
"types": "./dist/macro.d.cts",
"default": "./dist/macro.cjs"
},
"import": {
"types": "./dist/macro.d.mts",
"default": "./dist/macro.mjs"
}
},
"./ast": {
"require": {
"types": "./dist/ast.d.cts",
"default": "./dist/ast.cjs"
},
"import": {
"types": "./dist/ast.d.mts",
"default": "./dist/ast.mjs"
}
}
},
"files": [
"LICENSE",
"README.md",
"dist/"
],
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/core": "^7.20.12",
"@babel/runtime": "^7.20.13",
"@babel/types": "^7.20.7",
"@lingui/conf": "5.1.2",
"@lingui/core": "5.1.2",
"@lingui/message-utils": "5.1.2"
},
"peerDependencies": {
"babel-plugin-macros": "2 || 3"
},
"peerDependenciesMeta": {
"babel-plugin-macros": {
"optional": true
}
},
"devDependencies": {
"@babel/parser": "^7.20.15",
"@babel/traverse": "^7.20.12",
"@lingui/macro": "^5.1.2",
"@lingui/react": "^5.1.2",
"@types/babel-plugin-macros": "^2.8.5",
"prettier": "2.8.3",
"rimraf": "^6.0.1",
"unbuild": "2.0.0"
}
}
@@ -0,0 +1,19 @@
export * from "./icu"
export * from "./messageDescriptorUtils"
export { JsMacroName } from "./constants"
export {
isChoiceMethod,
isLinguiIdentifier,
isI18nMethod,
isDefineMessage,
tokenizeExpression,
tokenizeChoiceComponent,
tokenizeTemplateLiteral,
tokenizeNode,
processDescriptor,
createMacroJsContext,
type MacroJsContext,
tokenizeArg,
isArgDecorator,
} from "./macroJsAst"
@@ -0,0 +1,32 @@
export const EXTRACT_MARK = "i18n"
export const MACRO_LEGACY_PACKAGE = "@lingui/macro"
export const MACRO_CORE_PACKAGE = "@lingui/core/macro"
export const MACRO_REACT_PACKAGE = "@lingui/react/macro"
export const MACRO_SOLID_PACKAGE = "@lingui-solid/solid/macro"
export enum MsgDescriptorPropKey {
id = "id",
message = "message",
comment = "comment",
values = "values",
components = "components",
context = "context",
}
export enum JsMacroName {
t = "t",
plural = "plural",
select = "select",
selectOrdinal = "selectOrdinal",
msg = "msg",
defineMessage = "defineMessage",
arg = "arg",
useLingui = "useLingui",
}
export enum JsxMacroName {
Trans = "Trans",
Plural = "Plural",
Select = "Select",
SelectOrdinal = "SelectOrdinal",
}
@@ -0,0 +1,49 @@
import { ICUMessageFormat, Token } from "./icu"
import { Identifier } from "@babel/types"
describe("ICU MessageFormat", function () {
it("should collect text message", function () {
const messageFormat = new ICUMessageFormat()
const tokens: Token[] = [
{
type: "text",
value: "Hello World",
},
]
expect(messageFormat.fromTokens(tokens)).toEqual(
expect.objectContaining({
message: "Hello World",
values: {},
})
)
})
it("should collect text message with arguments", function () {
const messageFormat = new ICUMessageFormat()
const tokens: Token[] = [
{
type: "text",
value: "Hello ",
},
{
type: "arg",
name: "name",
value: {
type: "Identifier",
name: "Joe",
} as Identifier,
},
]
expect(messageFormat.fromTokens(tokens)).toEqual(
expect.objectContaining({
message: "Hello {name}",
values: {
name: {
type: "Identifier",
name: "Joe",
},
},
})
)
})
})
@@ -0,0 +1,151 @@
import { Expression, isJSXEmptyExpression, Node } from "@babel/types"
const metaOptions = ["id", "comment", "props"]
const escapedMetaOptionsRe = new RegExp(`^_(${metaOptions.join("|")})$`)
export type ParsedResult = {
message: string
values?: Record<string, Expression>
elements?: Record<string, any> // JSXElement or ElementNode in Vue
}
export type TextToken = {
type: "text"
value: string
}
export type ArgToken = {
type: "arg"
value: Expression
name?: string
raw?: boolean
/**
* plural
* select
* selectordinal
*/
format?: string
options?: {
offset: string
[icuChoice: string]: string | Tokens
}
}
export type ElementToken = {
type: "element"
value: any // JSXElement or ElementNode in Vue
name?: string | number
children?: Token[]
}
export type Tokens = Token | Token[]
export type Token = TextToken | ArgToken | ElementToken
export class ICUMessageFormat {
public fromTokens(tokens: Tokens): ParsedResult {
return (Array.isArray(tokens) ? tokens : [tokens])
.map((token) => this.processToken(token))
.filter(Boolean)
.reduce(
(props, message) => ({
...message,
message: props.message + message.message,
values: { ...props.values, ...message.values },
elements: { ...props.elements, ...message.elements },
}),
{
message: "",
values: {},
elements: {},
}
)
}
public processToken(token: Token): ParsedResult {
const jsxElements: ParsedResult["elements"] = {}
if (token.type === "text") {
return {
message: token.value,
}
} else if (token.type === "arg") {
if (
token.value !== undefined &&
isJSXEmptyExpression(token.value as Node)
) {
return null
}
const values =
token.value !== undefined ? { [token.name]: token.value } : {}
switch (token.format) {
case "plural":
case "select":
case "selectordinal":
const formatOptions = Object.keys(token.options)
.filter((key) => token.options[key] != null)
.map((key) => {
let value = token.options[key]
key = key.replace(escapedMetaOptionsRe, "$1")
if (key === "offset") {
// offset has special syntax `offset:number`
return `offset:${value}`
}
if (typeof value !== "string") {
// process tokens from nested formatters
const {
message,
values: childValues,
elements: childJsxElements,
} = this.fromTokens(value)
Object.assign(values, childValues)
Object.assign(jsxElements, childJsxElements)
value = message
}
return `${key} {${value}}`
})
.join(" ")
return {
message: `{${token.name}, ${token.format}, ${formatOptions}}`,
values,
elements: jsxElements,
}
default:
return {
message: token.raw ? `${token.name}` : `{${token.name}}`,
values,
}
}
} else if (token.type === "element") {
let message = ""
let elementValues: ParsedResult["values"] = {}
Object.assign(jsxElements, { [token.name]: token.value })
token.children.forEach((child) => {
const {
message: childMessage,
values: childValues,
elements: childJsxElements,
} = this.fromTokens(child)
message += childMessage
Object.assign(elementValues, childValues)
Object.assign(jsxElements, childJsxElements)
})
return {
message: token.children.length
? `<${token.name}>${message}</${token.name}>`
: `<${token.name}/>`,
values: elementValues,
elements: jsxElements,
}
}
throw new Error(`Unknown token type ${(token as any).type}`)
}
}
@@ -0,0 +1,277 @@
import type { PluginObj, Visitor, PluginPass, BabelFile } from "@babel/core"
import type * as babelTypes from "@babel/types"
import { Program, Identifier } from "@babel/types"
import { MacroJSX } from "./macroJsx"
import { NodePath } from "@babel/traverse"
import { MacroJs } from "./macroJs"
import {
MACRO_CORE_PACKAGE,
MACRO_REACT_PACKAGE,
MACRO_SOLID_PACKAGE,
MACRO_LEGACY_PACKAGE,
JsMacroName,
} from "./constants"
import {
type LinguiConfigNormalized,
getConfig as loadConfig,
} from "@lingui/conf"
let config: LinguiConfigNormalized
export type LinguiPluginOpts = {
// explicitly set by CLI when running extraction process
extract?: boolean
stripMessageField?: boolean
linguiConfig?: LinguiConfigNormalized
}
function getConfig(_config?: LinguiConfigNormalized) {
if (_config) {
config = _config
}
if (!config) {
config = loadConfig()
}
return config
}
function reportUnsupportedSyntax(path: NodePath, e: Error) {
const codeFrameError = path.buildCodeFrameError(
`Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros.
If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues
Error: ${e.message}`
)
// show stack trace where error originally happened
codeFrameError.stack = codeFrameError.message + "\n" + e.stack
throw codeFrameError
}
function shouldStripMessageProp(opts: LinguiPluginOpts) {
if (typeof opts.stripMessageField === "boolean") {
// if explicitly set in options, use it
return opts.stripMessageField
}
// default to strip message in production if no explicit option is set and not during extract
return process.env.NODE_ENV === "production" && !opts.extract
}
type LinguiSymbol = "Trans" | "useLingui" | "i18n"
const getIdentifierPath = ((path: NodePath, node: Identifier) => {
let foundPath: NodePath
path.traverse({
Identifier: (path) => {
if (path.node === node) {
foundPath = path
path.stop()
}
},
})
return foundPath
}) as any
export default function ({
types: t,
}: {
types: typeof babelTypes
}): PluginObj {
function addImport(state: PluginPass, name: LinguiSymbol) {
const path = state.get(
"macroImport"
) as NodePath<babelTypes.ImportDeclaration>
const config = state.get("linguiConfig") as LinguiConfigNormalized
if (!state.get("has_import_" + name)) {
state.set("has_import_" + name, true)
const [moduleSource, importName] = config.runtimeConfigModule[name]
const [newPath] = path.insertAfter(
t.importDeclaration(
[
t.importSpecifier(
getSymbolIdentifier(state, name),
t.identifier(importName)
),
],
t.stringLiteral(moduleSource)
)
)
path.parentPath.scope.registerDeclaration(newPath)
}
return path.parentPath.scope.getBinding(
getSymbolIdentifier(state, name).name
)
}
function getMacroImports(path: NodePath<Program>) {
return path.get("body").filter((statement) => {
return (
statement.isImportDeclaration() &&
[
MACRO_CORE_PACKAGE,
MACRO_REACT_PACKAGE,
MACRO_SOLID_PACKAGE,
MACRO_LEGACY_PACKAGE,
].includes(statement.get("source").node.value)
)
})
}
function getSymbolIdentifier(
state: PluginPass,
name: LinguiSymbol
): Identifier {
return state.get("linguiIdentifiers")[name]
}
function isLinguiIdentifier(
path: NodePath,
node: Identifier,
macro: JsMacroName
) {
let identPath = getIdentifierPath(path, node)
if (macro === JsMacroName.useLingui) {
if (
identPath.referencesImport(
MACRO_REACT_PACKAGE,
JsMacroName.useLingui
) ||
identPath.referencesImport(
MACRO_SOLID_PACKAGE,
JsMacroName.useLingui
) ||
identPath.referencesImport(MACRO_LEGACY_PACKAGE, JsMacroName.useLingui)
) {
return true
}
} else {
// useLingui might ask for identifiers which are not direct child of macro
identPath = identPath || getIdentifierPath(path.getFunctionParent(), node)
if (
identPath.referencesImport(MACRO_CORE_PACKAGE, macro) ||
identPath.referencesImport(MACRO_LEGACY_PACKAGE, macro)
) {
return true
}
}
return false
}
return {
name: "lingui-macro-plugin",
pre(file: BabelFile) {
file.hub
},
visitor: {
Program: {
enter(path, state) {
const macroImports = getMacroImports(path)
if (!macroImports.length) {
return
}
state.set("macroImport", macroImports[0])
state.set(
"linguiConfig",
getConfig((state.opts as LinguiPluginOpts).linguiConfig)
)
state.set("linguiIdentifiers", {
i18n: path.scope.generateUidIdentifier("i18n"),
Trans: path.scope.generateUidIdentifier("Trans"),
useLingui: path.scope.generateUidIdentifier("useLingui"),
})
path.traverse(
{
JSXElement(path, state) {
const macro = new MacroJSX(
{ types: t },
{
transImportName: getSymbolIdentifier(state, "Trans").name,
stripNonEssentialProps:
process.env.NODE_ENV == "production" &&
!(state.opts as LinguiPluginOpts).extract,
stripMessageProp: shouldStripMessageProp(
state.opts as LinguiPluginOpts
),
}
)
let newNode: false | babelTypes.Node
try {
newNode = macro.replacePath(path)
} catch (e) {
reportUnsupportedSyntax(path, e as Error)
}
if (newNode) {
const [newPath] = path.replaceWith(newNode)
addImport(state, "Trans").reference(newPath)
}
},
"CallExpression|TaggedTemplateExpression"(
path: NodePath<
| babelTypes.CallExpression
| babelTypes.TaggedTemplateExpression
>,
state: PluginPass
) {
const macro = new MacroJs({
stripNonEssentialProps:
process.env.NODE_ENV == "production" &&
!(state.opts as LinguiPluginOpts).extract,
stripMessageProp: shouldStripMessageProp(
state.opts as LinguiPluginOpts
),
i18nImportName: getSymbolIdentifier(state, "i18n").name,
useLinguiImportName: getSymbolIdentifier(state, "useLingui")
.name,
isLinguiIdentifier: (node: Identifier, macro) =>
isLinguiIdentifier(path, node, macro),
})
let newNode: false | babelTypes.Node
try {
newNode = macro.replacePath(path)
} catch (e) {
reportUnsupportedSyntax(path, e as Error)
}
if (newNode) {
const [newPath] = path.replaceWith(newNode)
if (macro.needsUseLinguiImport) {
addImport(state, "useLingui").reference(newPath)
}
if (macro.needsI18nImport) {
addImport(state, "i18n").reference(newPath)
}
}
},
} as Visitor<PluginPass>,
state
)
},
exit(path, state) {
const macroImports = getMacroImports(path)
macroImports.forEach((path) => path.remove())
},
},
} as Visitor<PluginPass>,
}
}
@@ -0,0 +1,57 @@
import { createMacro, MacroParams } from "babel-plugin-macros"
import { VisitNodeObject } from "@babel/traverse"
import { Program } from "@babel/types"
import linguiPlugin from "./index"
import { JsMacroName, JsxMacroName } from "./constants"
function macro({ state, babel, config }: MacroParams) {
if (!state.get("linguiProcessed")) {
state.opts = config
const plugin = linguiPlugin(babel)
const { enter, exit } = plugin.visitor.Program as VisitNodeObject<
any,
Program
>
enter(state.file.path, state)
state.file.path.traverse(plugin.visitor, state)
exit(state.file.path, state)
state.set("linguiProcessed", true)
}
return { keepImports: true }
}
;[
JsMacroName.defineMessage,
JsMacroName.msg,
JsMacroName.t,
JsMacroName.useLingui,
JsMacroName.plural,
JsMacroName.select,
JsMacroName.selectOrdinal,
JsxMacroName.Trans,
JsxMacroName.Plural,
JsxMacroName.Select,
JsxMacroName.SelectOrdinal,
].forEach((name) => {
Object.defineProperty(module.exports, name, {
get() {
throw new Error(
`The macro you imported from "@lingui/core/macro" or "@lingui/react/macro" is being executed outside the context of compilation with babel-plugin-macros. ` +
`This indicates that you don't have the babel plugin "babel-plugin-macros" configured correctly. ` +
`Please see the documentation for how to configure babel-plugin-macros properly: ` +
"https://github.com/kentcdodds/babel-plugin-macros/blob/main/other/docs/user.md"
)
},
})
})
export default createMacro(macro, {
configName: "lingui",
}) as { isBabelMacro: true }
@@ -0,0 +1,322 @@
import * as babelTypes from "@babel/types"
import * as t from "@babel/types"
import {
CallExpression,
Expression,
Identifier,
ObjectExpression,
ObjectProperty,
} from "@babel/types"
import { NodePath } from "@babel/traverse"
import { Tokens } from "./icu"
import { JsMacroName } from "./constants"
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
import {
isLinguiIdentifier,
isDefineMessage,
tokenizeTemplateLiteral,
tokenizeNode,
processDescriptor,
createMacroJsContext,
MacroJsContext,
} from "./macroJsAst"
export type MacroJsOpts = {
i18nImportName: string
useLinguiImportName: string
stripNonEssentialProps: boolean
stripMessageProp: boolean
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
}
export class MacroJs {
// Identifier of i18n object
i18nImportName: string
useLinguiImportName: string
needsUseLinguiImport = false
needsI18nImport = false
_ctx: MacroJsContext
constructor(opts: MacroJsOpts) {
this.i18nImportName = opts.i18nImportName
this.useLinguiImportName = opts.useLinguiImportName
this._ctx = createMacroJsContext(
opts.isLinguiIdentifier,
opts.stripNonEssentialProps,
opts.stripMessageProp
)
}
private replacePathWithMessage = (
path: NodePath,
tokens: Tokens,
linguiInstance?: babelTypes.Expression
) => {
return this.createI18nCall(
createMessageDescriptorFromTokens(
tokens,
path.node.loc,
this._ctx.stripNonEssentialProps,
this._ctx.stripMessageProp
),
linguiInstance
)
}
replacePath = (path: NodePath): false | babelTypes.Expression => {
const ctx = this._ctx
// defineMessage({ message: "Message", context: "My" }) -> {id: <hash + context>, message: "Message"}
if (
//
path.isCallExpression() &&
isDefineMessage(path.get("callee").node, ctx)
) {
return processDescriptor(
path.get("arguments")[0].node as ObjectExpression,
ctx
)
}
// defineMessage`Message` -> {id: <hash>, message: "Message"}
if (
path.isTaggedTemplateExpression() &&
isDefineMessage(path.get("tag").node, ctx)
) {
const tokens = tokenizeTemplateLiteral(path.get("quasi").node, ctx)
return createMessageDescriptorFromTokens(
tokens,
path.node.loc,
ctx.stripNonEssentialProps,
ctx.stripMessageProp
)
}
if (path.isTaggedTemplateExpression()) {
const tag = path.get("tag")
// t(i18nInstance)`Message` -> i18nInstance._(messageDescriptor)
if (
tag.isCallExpression() &&
tag.get("arguments")[0]?.isExpression() &&
isLinguiIdentifier(tag.get("callee").node, JsMacroName.t, ctx)
) {
// Use the first argument as i18n instance instead of the default i18n instance
const i18nInstance = tag.get("arguments")[0].node as Expression
const tokens = tokenizeNode(path.node, false, ctx)
return this.replacePathWithMessage(path, tokens, i18nInstance)
}
}
// t(i18nInstance)(messageDescriptor) -> i18nInstance._(messageDescriptor)
if (path.isCallExpression()) {
const callee = path.get("callee")
if (
callee.isCallExpression() &&
callee.get("arguments")[0]?.isExpression() &&
isLinguiIdentifier(callee.get("callee").node, JsMacroName.t, ctx)
) {
const i18nInstance = callee.node.arguments[0] as Expression
return this.replaceTAsFunction(
path.node as CallExpression,
ctx,
i18nInstance
)
}
}
// t({...})
if (
path.isCallExpression() &&
isLinguiIdentifier(path.get("callee").node, JsMacroName.t, ctx)
) {
this.needsI18nImport = true
return this.replaceTAsFunction(path.node, ctx)
}
// { t } = useLingui()
if (
path.isCallExpression() &&
isLinguiIdentifier(path.get("callee").node, JsMacroName.useLingui, ctx)
) {
this.needsUseLinguiImport = true
return this.processUseLingui(path, ctx)
}
const tokens = tokenizeNode(path.node, true, ctx)
if (tokens) {
this.needsI18nImport = true
return this.replacePathWithMessage(path, tokens)
}
return false
}
/**
* macro `t` is called with MessageDescriptor, after that
* we create a new node to append it to i18n._
*/
private replaceTAsFunction = (
node: CallExpression,
ctx: MacroJsContext,
linguiInstance?: babelTypes.Expression
): babelTypes.CallExpression => {
let arg: Expression = node.arguments[0] as Expression
if (t.isObjectExpression(arg)) {
arg = processDescriptor(arg, ctx)
}
return this.createI18nCall(arg, linguiInstance)
}
/**
* Receives reference to `useLingui()` call
*
* Finds every usage of { t } destructured from the call
* and process each reference as usual `t` macro.
*
* const { t } = useLingui()
* t`Message`
*
* ↓ ↓ ↓ ↓ ↓ ↓
*
* const { _: _t } = useLingui()
* _t({id: <hash>, message: "Message"})
*/
processUseLingui(path: NodePath<CallExpression>, ctx: MacroJsContext) {
/*
* path is CallExpression eq:
* useLingui()
*
* path.parentPath should be a VariableDeclarator eq:
* const { t } = useLingui()
*/
if (!path.parentPath.isVariableDeclarator()) {
throw new Error(
`\`useLingui\` macro must be used in variable declaration.
Example:
const { t } = useLingui()
`
)
}
// looking for `t` property in left side assigment
// in the declarator `const { t } = useLingui()`
const varDec = path.parentPath.node
if (!t.isObjectPattern(varDec.id)) {
// Enforce destructuring `t` from `useLingui` macro to prevent misuse
throw new Error(
`You have to destructure \`t\` when using the \`useLingui\` macro, i.e:
const { t } = useLingui()
or
const { t: _ } = useLingui()
`
)
}
const _property = t.isObjectPattern(varDec.id)
? varDec.id.properties.find(
(
property
): property is ObjectProperty & {
value: Identifier
key: Identifier
} =>
t.isObjectProperty(property) &&
t.isIdentifier(property.key) &&
t.isIdentifier(property.value) &&
property.key.name == "t"
)
: null
const newNode = t.callExpression(t.identifier(this.useLinguiImportName), [])
if (!_property) {
return newNode
}
const uniqTIdentifier = path.scope.generateUidIdentifier("t")
path.scope
.getBinding(_property.value.name)
?.referencePaths.forEach((refPath) => {
// reference usually points to Identifier,
// parent would be an Expression with this identifier which we are interesting in
const currentPath = refPath.parentPath
// { t } = useLingui()
// t`Hello!`
if (currentPath.isTaggedTemplateExpression()) {
const tokens = tokenizeTemplateLiteral(currentPath.node, ctx)
const descriptor = createMessageDescriptorFromTokens(
tokens,
currentPath.node.loc,
ctx.stripNonEssentialProps,
ctx.stripMessageProp
)
const callExpr = t.callExpression(
t.identifier(uniqTIdentifier.name),
[descriptor]
)
return currentPath.replaceWith(callExpr)
}
// { t } = useLingui()
// t(messageDescriptor)
if (
currentPath.isCallExpression() &&
currentPath.get("arguments")[0]?.isObjectExpression()
) {
let descriptor = processDescriptor(
(currentPath.get("arguments")[0] as NodePath<ObjectExpression>)
.node,
ctx
)
const callExpr = t.callExpression(
t.identifier(uniqTIdentifier.name),
[descriptor]
)
return currentPath.replaceWith(callExpr)
}
// for rest of cases just rename identifier for run-time counterpart
refPath.replaceWith(t.identifier(uniqTIdentifier.name))
})
// assign uniq identifier for runtime `_`
// { t } = useLingui() -> { _ : _t } = useLingui()
_property.key.name = "_"
_property.value.name = uniqTIdentifier.name
return t.callExpression(t.identifier(this.useLinguiImportName), [])
}
private createI18nCall(
messageDescriptor: Expression | undefined,
linguiInstance?: Expression
) {
return t.callExpression(
t.memberExpression(
linguiInstance ?? t.identifier(this.i18nImportName),
t.identifier("_")
),
messageDescriptor ? [messageDescriptor] : []
)
}
}
@@ -0,0 +1,376 @@
import { type CallExpression, type Expression } from "@babel/types"
import {
tokenizeTemplateLiteral,
tokenizeChoiceComponent,
createMacroJsContext,
} from "./macroJsAst"
import type { NodePath } from "@babel/traverse"
import { transformSync } from "@babel/core"
import { JsMacroName } from "./constants"
const parseExpression = (expression: string) => {
let path: NodePath<Expression>
const importExp = `import {t, plural, select, selectOrdinal} from "@lingui/core/macro"; \n`
transformSync(importExp + expression, {
filename: "unit-test.js",
configFile: false,
presets: [],
plugins: [
"@babel/plugin-syntax-jsx",
{
visitor: {
"CallExpression|TaggedTemplateExpression": (
d: NodePath<Expression>
) => {
path = d
d.stop()
},
},
},
],
})
return path
}
function createMacroCtx() {
return createMacroJsContext(
(identifier, macro) => {
return identifier.name === macro
},
false, // stripNonEssentialProps
false // stripMessageProp
)
}
describe("js macro", () => {
describe("tokenizeTemplateLiteral", () => {
it("simple message without arguments", () => {
const exp = parseExpression("t`Message`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message",
},
])
})
it("with custom lingui instance", () => {
const exp = parseExpression("t(i18n)`Message`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message",
},
])
})
it("message with named argument", () => {
const exp = parseExpression("t`Message ${name}`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "name",
value: expect.objectContaining({
name: "name",
type: "Identifier",
}),
},
])
})
it("message with positional argument", () => {
const exp = parseExpression("t`Message ${obj.name}`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "0",
value: expect.objectContaining({
type: "MemberExpression",
}),
},
])
})
it("message with plural", () => {
const exp = parseExpression("t`Message ${plural(count, {})}`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "count",
value: expect.objectContaining({
type: "Identifier",
}),
format: "plural",
options: {},
},
])
})
it("message with unicode \\u chars is interpreted by babel", () => {
const exp = parseExpression("t`Message \\u0020`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
])
})
it("message with unicode \\x chars is interpreted by babel", () => {
const exp = parseExpression("t`Bienvenue\\xA0!`")
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toEqual([
{
type: "text",
// Looks like an empty space, but it isn't
value: "Bienvenue !",
},
])
})
it("message with double escaped literals it's stripped", () => {
const exp = parseExpression(
"t`Passing \\`${argSet}\\` is not supported.`"
)
const tokens = tokenizeTemplateLiteral(exp.node, createMacroCtx())
expect(tokens).toMatchObject([
{
type: "text",
value: "Passing `",
},
{
name: "argSet",
type: "arg",
value: {
loc: {
identifierName: "argSet",
},
name: "argSet",
type: "Identifier",
},
},
{
type: "text",
value: "` is not supported.",
},
])
})
})
describe("tokenizeChoiceComponent", () => {
it("plural", () => {
const exp = parseExpression(
"plural(count, { one: '# book', other: '# books'})"
)
const tokens = tokenizeChoiceComponent(
(exp as NodePath<CallExpression>).node,
JsMacroName.plural,
createMacroCtx()
)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: "# book",
other: "# books",
},
})
})
it("plural with offset", () => {
const exp = parseExpression(
`plural(count, {
offset: 1,
0: 'No books',
one: '# book',
other: '# books'
})`
)
const tokens = tokenizeChoiceComponent(
(exp as NodePath<CallExpression>).node,
JsMacroName.plural,
createMacroCtx()
)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
offset: 1,
"=0": "No books",
one: "# book",
other: "# books",
},
})
})
it("plural with template literal", () => {
const exp = parseExpression(
"plural(count, { one: `# glass of ${drink}`, other: `# glasses of ${drink}`})"
)
const tokens = tokenizeChoiceComponent(
(exp as NodePath<CallExpression>).node,
JsMacroName.plural,
createMacroCtx()
)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: [
{
type: "text",
value: "# glass of ",
},
{
type: "arg",
name: "drink",
value: expect.objectContaining({
name: "drink",
type: "Identifier",
}),
},
],
other: [
{
type: "text",
value: "# glasses of ",
},
{
type: "arg",
name: "drink",
value: expect.objectContaining({
name: "drink",
type: "Identifier",
}),
},
],
},
})
})
it("plural with select", () => {
const exp = parseExpression(
`plural(count, {
one: select(gender, {
male: hePronoun,
female: "she",
other: "they"
}),
other: otherText
})`
)
const tokens = tokenizeChoiceComponent(
(exp as NodePath<CallExpression>).node,
JsMacroName.plural,
createMacroCtx()
)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: [
{
type: "arg",
name: "gender",
value: expect.objectContaining({
name: "gender",
type: "Identifier",
}),
format: "select",
options: {
male: expect.objectContaining({
type: "arg",
name: "hePronoun",
value: expect.objectContaining({
name: "hePronoun",
type: "Identifier",
}),
}),
female: "she",
other: "they",
},
},
],
other: expect.objectContaining({
type: "arg",
name: "otherText",
value: expect.objectContaining({
name: "otherText",
type: "Identifier",
}),
}),
},
})
})
it("select", () => {
const exp = parseExpression(
`select(gender, {
male: "he",
female: "she",
other: "they"
})`
)
const tokens = tokenizeChoiceComponent(
(exp as NodePath<CallExpression>).node,
JsMacroName.select,
createMacroCtx()
)
expect(tokens).toMatchObject({
format: "select",
name: "gender",
options: expect.objectContaining({
female: "she",
male: "he",
offset: undefined,
other: "they",
}),
type: "arg",
value: {
name: "gender",
type: "Identifier",
},
})
})
})
})
@@ -0,0 +1,332 @@
import * as t from "@babel/types"
import {
ObjectExpression,
Expression,
TemplateLiteral,
Identifier,
Node,
CallExpression,
StringLiteral,
ObjectProperty,
} from "@babel/types"
import { MsgDescriptorPropKey, JsMacroName } from "./constants"
import { Token, TextToken, ArgToken } from "./icu"
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
import { makeCounter } from "./utils"
export type MacroJsContext = {
// Positional expressions counter (e.g. for placeholders `Hello {0}, today is {1}`)
getExpressionIndex: () => number
stripNonEssentialProps: boolean
stripMessageProp: boolean
isLinguiIdentifier: (node: Identifier, macro: JsMacroName) => boolean
}
export function createMacroJsContext(
isLinguiIdentifier: MacroJsContext["isLinguiIdentifier"],
stripNonEssentialProps: boolean,
stripMessageProp: boolean
): MacroJsContext {
return {
getExpressionIndex: makeCounter(),
isLinguiIdentifier,
stripNonEssentialProps,
stripMessageProp,
}
}
/**
* `processDescriptor` expand macros inside message descriptor.
* Message descriptor is used in `defineMessage`.
*
* {
* comment: "Description",
* message: plural("value", { one: "book", other: "books" })
* }
*
* ↓ ↓ ↓ ↓ ↓ ↓
*
* {
* comment: "Description",
* id: <hash>
* message: "{value, plural, one {book} other {books}}"
* }
*
*/
export function processDescriptor(
descriptor: ObjectExpression,
ctx: MacroJsContext
) {
const messageProperty = getObjectPropertyByKey(
descriptor,
MsgDescriptorPropKey.message
)
const idProperty = getObjectPropertyByKey(descriptor, MsgDescriptorPropKey.id)
const contextProperty = getObjectPropertyByKey(
descriptor,
MsgDescriptorPropKey.context
)
const commentProperty = getObjectPropertyByKey(
descriptor,
MsgDescriptorPropKey.comment
)
let tokens: Token[] = []
// if there's `message` property, replace macros with formatted message
if (messageProperty) {
// Inside message descriptor the `t` macro in `message` prop is optional.
// Template strings are always processed as if they were wrapped by `t`.
const messageValue = messageProperty.value
tokens = t.isTemplateLiteral(messageValue)
? tokenizeTemplateLiteral(messageValue, ctx)
: tokenizeNode(messageValue, true, ctx)
}
return createMessageDescriptorFromTokens(
tokens,
descriptor.loc,
ctx.stripNonEssentialProps,
ctx.stripMessageProp,
{
id: idProperty,
context: contextProperty,
comment: commentProperty,
}
)
}
export function tokenizeNode(
node: Node,
ignoreExpression = false,
ctx: MacroJsContext
): Token[] {
if (isI18nMethod(node, ctx)) {
// t
return tokenizeTemplateLiteral(node as Expression, ctx)
}
if (t.isCallExpression(node) && isArgDecorator(node, ctx)) {
return [tokenizeArg(node, ctx)]
}
const choiceMethod = isChoiceMethod(node, ctx)
// plural, select and selectOrdinal
if (choiceMethod) {
return [tokenizeChoiceComponent(node as CallExpression, choiceMethod, ctx)]
}
if (t.isStringLiteral(node)) {
return [
{
type: "text",
value: node.value,
} satisfies TextToken,
]
}
// if (isFormatMethod(node.callee)) {
// // date, number
// return transformFormatMethod(node, file, props, root)
if (!ignoreExpression) {
return [tokenizeExpression(node, ctx)]
}
}
/**
* `node` is a TemplateLiteral. node.quasi contains
* text chunks and node.expressions contains expressions.
* Both arrays must be zipped together to get the final list of tokens.
*/
export function tokenizeTemplateLiteral(
node: Expression,
ctx: MacroJsContext
): Token[] {
const tpl = t.isTaggedTemplateExpression(node)
? node.quasi
: (node as TemplateLiteral)
const expressions = tpl.expressions as Expression[]
return tpl.quasis.flatMap((text, i) => {
const value = text.value.cooked
let argTokens: Token[] = []
const currExp = expressions[i]
if (currExp) {
argTokens = t.isCallExpression(currExp)
? tokenizeNode(currExp, false, ctx)
: [tokenizeExpression(currExp, ctx)]
}
const textToken: TextToken = {
type: "text",
value,
}
return [...(value ? [textToken] : []), ...argTokens]
})
}
export function tokenizeChoiceComponent(
node: CallExpression,
componentName: string,
ctx: MacroJsContext
): ArgToken {
const format = componentName.toLowerCase()
const token: ArgToken = {
...tokenizeExpression(node.arguments[0], ctx),
format: format,
options: {
offset: undefined,
},
}
const props = (node.arguments[1] as ObjectExpression).properties
for (const attr of props) {
if (!t.isObjectProperty(attr)) {
throw new Error("Expected an ObjectProperty")
}
const key = attr.key
const attrValue = attr.value as Expression
// name is either:
// NumericLiteral => convert to `={number}`
// StringLiteral => key.value
// Identifier => key.name
const name = t.isNumericLiteral(key)
? `=${key.value}`
: (key as Identifier).name || (key as StringLiteral).value
if (format !== "select" && name === "offset") {
token.options.offset = (attrValue as StringLiteral).value
} else {
let value: ArgToken["options"][string]
if (t.isTemplateLiteral(attrValue)) {
value = tokenizeTemplateLiteral(attrValue, ctx)
} else if (t.isCallExpression(attrValue)) {
value = tokenizeNode(attrValue, false, ctx)
} else if (t.isStringLiteral(attrValue)) {
value = attrValue.value
} else if (t.isExpression(attrValue)) {
value = tokenizeExpression(attrValue, ctx)
} else {
value = (attrValue as unknown as StringLiteral).value
}
token.options[name] = value
}
}
return token
}
export function tokenizeExpression(
node: Node | Expression,
ctx: MacroJsContext
): ArgToken {
return {
type: "arg",
name: expressionToArgument(node as Expression, ctx),
value: node as Expression,
}
}
export function tokenizeArg(
node: CallExpression,
ctx: MacroJsContext
): ArgToken {
const arg = node.arguments[0] as Expression
return {
type: "arg",
name: expressionToArgument(arg, ctx),
raw: true,
value: arg,
}
}
export function expressionToArgument(
exp: Expression,
ctx: MacroJsContext
): string {
if (t.isIdentifier(exp)) {
return exp.name
} else if (t.isStringLiteral(exp)) {
return exp.value
} else {
return String(ctx.getExpressionIndex())
}
}
export function isArgDecorator(node: Node, ctx: MacroJsContext): boolean {
return (
t.isCallExpression(node) &&
isLinguiIdentifier(node.callee, JsMacroName.arg, ctx)
)
}
export function isDefineMessage(node: Node, ctx: MacroJsContext): boolean {
return (
isLinguiIdentifier(node, JsMacroName.defineMessage, ctx) ||
isLinguiIdentifier(node, JsMacroName.msg, ctx)
)
}
export function isI18nMethod(node: Node, ctx: MacroJsContext) {
if (!t.isTaggedTemplateExpression(node)) {
return
}
const tag = node.tag
return (
isLinguiIdentifier(tag, JsMacroName.t, ctx) ||
(t.isCallExpression(tag) &&
isLinguiIdentifier(tag.callee, JsMacroName.t, ctx))
)
}
export function isLinguiIdentifier(
node: Node,
name: JsMacroName,
ctx: MacroJsContext
) {
if (!t.isIdentifier(node)) {
return false
}
return ctx.isLinguiIdentifier(node, name)
}
export function isChoiceMethod(node: Node, ctx: MacroJsContext) {
if (!t.isCallExpression(node)) {
return
}
if (isLinguiIdentifier(node.callee, JsMacroName.plural, ctx)) {
return JsMacroName.plural
}
if (isLinguiIdentifier(node.callee, JsMacroName.select, ctx)) {
return JsMacroName.select
}
if (isLinguiIdentifier(node.callee, JsMacroName.selectOrdinal, ctx)) {
return JsMacroName.selectOrdinal
}
}
function getObjectPropertyByKey(
objectExp: ObjectExpression,
key: string
): ObjectProperty {
return objectExp.properties.find(
(property) =>
t.isObjectProperty(property) &&
t.isIdentifier(property.key as Expression, {
name: key,
})
) as ObjectProperty
}
@@ -0,0 +1,321 @@
import type { JSXElement } from "@babel/types"
import * as types from "@babel/types"
import { MacroJSX } from "./macroJsx"
import { transformSync } from "@babel/core"
import type { NodePath } from "@babel/traverse"
import { JsxMacroName } from "./constants"
const parseExpression = (expression: string) => {
let path: NodePath<JSXElement>
const importExp = `import {Trans, Plural, Select, SelectOrdinal} from "@lingui/react/macro";\n`
transformSync(importExp + expression, {
filename: "unit-test.js",
configFile: false,
presets: [],
plugins: [
"@babel/plugin-syntax-jsx",
{
visitor: {
JSXElement: (d) => {
path = d
d.stop()
},
},
},
],
})
return path
}
function createMacro() {
return new MacroJSX(
{ types },
{
stripNonEssentialProps: false,
stripMessageProp: false,
transImportName: "Trans",
}
)
}
describe("jsx macro", () => {
describe("tokenizeTrans", () => {
it("simple message without arguments", () => {
const macro = createMacro()
const exp = parseExpression("<Trans>Message</Trans>")
const tokens = macro.tokenizeTrans(exp)
expect(tokens).toEqual([
{
type: "text",
value: "Message",
},
])
})
it("message with named argument", () => {
const macro = createMacro()
const exp = parseExpression("<Trans>Message {name}</Trans>")
const tokens = macro.tokenizeTrans(exp)
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "name",
value: expect.objectContaining({
name: "name",
type: "Identifier",
}),
},
])
})
it("message with positional argument", () => {
const macro = createMacro()
const exp = parseExpression("<Trans>Message {obj.name}</Trans>")
const tokens = macro.tokenizeTrans(exp)
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "0",
value: expect.objectContaining({
type: "MemberExpression",
}),
},
])
})
it("message with plural", () => {
const macro = createMacro()
const exp = parseExpression(
"<Trans>Message <Plural value={count} /></Trans>"
)
const tokens = macro.tokenizeTrans(exp)
expect(tokens).toEqual([
{
type: "text",
value: "Message ",
},
{
type: "arg",
name: "count",
value: expect.objectContaining({
type: "Identifier",
}),
format: "plural",
options: {},
},
])
})
})
describe("tokenizeChoiceComponent", () => {
it("plural", () => {
const macro = createMacro()
const exp = parseExpression(
"<Plural value={count} one='# book' other='# books' />"
)
const tokens = macro.tokenizeChoiceComponent(exp, JsxMacroName.Plural)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: "# book",
other: "# books",
},
})
})
it("plural with offset", () => {
const macro = createMacro()
const exp = parseExpression(
`<Plural
value={count}
offset={1}
_0='No books'
one='# book'
other='# books'
/>`
)
const tokens = macro.tokenizeChoiceComponent(exp, JsxMacroName.Plural)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
offset: 1,
"=0": "No books",
one: "# book",
other: "# books",
},
})
})
it("plural with key should be omitted", () => {
const macro = createMacro()
const exp = parseExpression(
`<Plural
key='somePLural'
value={count}
_0='No books'
one='# book'
other='# books'
/>`
)
const tokens = macro.tokenizeChoiceComponent(exp, JsxMacroName.Plural)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
"=0": "No books",
one: "# book",
other: "# books",
},
})
})
it("plural with template literal", () => {
const macro = createMacro()
const exp = parseExpression(
"<Plural value={count} one={`# glass of ${drink}`} other={`# glasses of ${drink}`} />"
)
const tokens = macro.tokenizeChoiceComponent(exp, JsxMacroName.Plural)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: [
{
type: "text",
value: "# glass of ",
},
{
type: "arg",
name: "drink",
value: expect.objectContaining({
name: "drink",
type: "Identifier",
}),
},
],
other: [
{
type: "text",
value: "# glasses of ",
},
{
type: "arg",
name: "drink",
value: expect.objectContaining({
name: "drink",
type: "Identifier",
}),
},
],
},
})
})
it("plural with select", () => {
const macro = createMacro()
const exp = parseExpression(
`<Plural
value={count}
one={
<Select
value={gender}
_male="he"
_female="she"
other="they"
/>
}
/>`
)
const tokens = macro.tokenizeChoiceComponent(exp, JsxMacroName.Plural)
expect(tokens).toEqual({
type: "arg",
name: "count",
value: expect.objectContaining({
name: "count",
type: "Identifier",
}),
format: "plural",
options: {
one: [
{
type: "arg",
name: "gender",
value: expect.objectContaining({
name: "gender",
type: "Identifier",
}),
format: "select",
options: {
male: "he",
female: "she",
other: "they",
},
},
],
},
})
})
it("Select", () => {
const macro = createMacro()
const exp = parseExpression(
`<Select
value={gender}
male="he"
one="heone"
female="she"
other="they"
/>`
)
const tokens = macro.tokenizeNode(exp)
expect(tokens[0]).toMatchObject({
format: "select",
name: "gender",
options: {
female: "she",
male: "he",
offset: undefined,
other: "they",
},
type: "arg",
value: {
name: "gender",
type: "Identifier",
},
})
})
})
})
@@ -0,0 +1,436 @@
import * as babelTypes from "@babel/types"
import {
ConditionalExpression,
Expression,
JSXAttribute,
JSXElement,
JSXExpressionContainer,
JSXIdentifier,
JSXSpreadAttribute,
Literal,
Node,
StringLiteral,
TemplateLiteral,
SourceLocation,
} from "@babel/types"
import { NodePath } from "@babel/traverse"
import { ArgToken, ElementToken, TextToken, Token } from "./icu"
import { makeCounter } from "./utils"
import {
JsxMacroName,
MACRO_REACT_PACKAGE,
MACRO_SOLID_PACKAGE,
MACRO_LEGACY_PACKAGE,
MsgDescriptorPropKey,
} from "./constants"
import cleanJSXElementLiteralChild from "./utils/cleanJSXElementLiteralChild"
import { createMessageDescriptorFromTokens } from "./messageDescriptorUtils"
const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/
const jsx2icuExactChoice = (value: string) =>
value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1")
type JSXChildPath = NodePath<JSXElement["children"][number]>
function maybeNodeValue(node: Node): { text: string; loc: SourceLocation } {
if (!node) return null
if (node.type === "StringLiteral") return { text: node.value, loc: node.loc }
if (node.type === "JSXAttribute") return maybeNodeValue(node.value)
if (node.type === "JSXExpressionContainer")
return maybeNodeValue(node.expression)
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
return { text: node.quasis[0].value.raw, loc: node.loc }
return null
}
export type MacroJsxOpts = {
stripNonEssentialProps: boolean
stripMessageProp: boolean
transImportName: string
}
export class MacroJSX {
types: typeof babelTypes
expressionIndex = makeCounter()
elementIndex = makeCounter()
stripNonEssentialProps: boolean
stripMessageProp: boolean
transImportName: string
constructor({ types }: { types: typeof babelTypes }, opts: MacroJsxOpts) {
this.types = types
this.stripNonEssentialProps = opts.stripNonEssentialProps
this.stripMessageProp = opts.stripMessageProp
this.transImportName = opts.transImportName
}
replacePath = (path: NodePath): false | Node => {
if (!path.isJSXElement()) {
return false
}
const tokens = this.tokenizeNode(path, true, true)
if (!tokens) {
return false
}
const { attributes, id, comment, context } = this.stripMacroAttributes(
path as NodePath<JSXElement>
)
if (!tokens.length) {
throw new Error("Incorrect usage of Trans")
}
const messageDescriptor = createMessageDescriptorFromTokens(
tokens,
path.node.loc,
this.stripNonEssentialProps,
this.stripMessageProp,
{
id,
context,
comment,
}
)
attributes.push(this.types.jsxSpreadAttribute(messageDescriptor))
const newNode = this.types.jsxElement(
this.types.jsxOpeningElement(
this.types.jsxIdentifier(this.transImportName),
attributes,
true
),
null,
[],
true
)
newNode.loc = path.node.loc
return newNode
}
attrName = (names: string[], exclude = false) => {
const namesRe = new RegExp("^(" + names.join("|") + ")$")
return (attr: JSXAttribute | JSXSpreadAttribute) => {
const name = ((attr as JSXAttribute).name as JSXIdentifier).name
return exclude ? !namesRe.test(name) : namesRe.test(name)
}
}
stripMacroAttributes = (path: NodePath<JSXElement>) => {
const { attributes } = path.node.openingElement
const id = attributes.find(this.attrName([MsgDescriptorPropKey.id]))
const message = attributes.find(
this.attrName([MsgDescriptorPropKey.message])
)
const comment = attributes.find(
this.attrName([MsgDescriptorPropKey.comment])
)
const context = attributes.find(
this.attrName([MsgDescriptorPropKey.context])
)
let reserved: string[] = [
MsgDescriptorPropKey.id,
MsgDescriptorPropKey.message,
MsgDescriptorPropKey.comment,
MsgDescriptorPropKey.context,
]
if (this.isChoiceComponent(path)) {
reserved = [
...reserved,
"_\\w+",
"_\\d+",
"zero",
"one",
"two",
"few",
"many",
"other",
"value",
"offset",
]
}
return {
id: maybeNodeValue(id),
message: maybeNodeValue(message),
comment: maybeNodeValue(comment),
context: maybeNodeValue(context),
attributes: attributes.filter(this.attrName(reserved, true)),
}
}
tokenizeNode = (
path: NodePath,
ignoreExpression = false,
ignoreElement = false
): Token[] => {
if (this.isTransComponent(path)) {
// t
return this.tokenizeTrans(path)
}
const componentName = this.isChoiceComponent(path)
if (componentName) {
// plural, select and selectOrdinal
return [
this.tokenizeChoiceComponent(
path as NodePath<JSXElement>,
componentName
),
]
}
if (path.isJSXElement() && !ignoreElement) {
return [this.tokenizeElement(path)]
}
if (!ignoreExpression) {
return [this.tokenizeExpression(path)]
}
}
tokenizeTrans = (path: NodePath<JSXElement>): Token[] => {
return path
.get("children")
.flatMap((child) => this.tokenizeChildren(child))
.filter(Boolean)
}
tokenizeChildren = (path: JSXChildPath): Token[] => {
if (path.isJSXExpressionContainer()) {
const exp = path.get("expression") as NodePath<Expression>
if (exp.isStringLiteral()) {
return [this.tokenizeText(exp.node.value)]
}
if (exp.isTemplateLiteral()) {
return this.tokenizeTemplateLiteral(exp)
}
if (exp.isConditionalExpression()) {
return [this.tokenizeConditionalExpression(exp)]
}
if (exp.isJSXElement()) {
return this.tokenizeNode(exp)
}
return [this.tokenizeExpression(exp)]
} else if (path.isJSXElement()) {
return this.tokenizeNode(path)
} else if (path.isJSXSpreadChild()) {
throw new Error(
"Incorrect usage of Trans: Spread could not be used as Trans children"
)
} else if (path.isJSXText()) {
return [this.tokenizeText(cleanJSXElementLiteralChild(path.node.value))]
} else {
// impossible path
// return this.tokenizeText(node.value)
}
}
tokenizeTemplateLiteral(exp: NodePath<TemplateLiteral>): Token[] {
const expressions = exp.get("expressions") as NodePath<Expression>[]
return exp.get("quasis").flatMap(({ node: text }, i) => {
const value = text.value.cooked
let argTokens: Token[] = []
const currExp = expressions[i]
if (currExp) {
argTokens = currExp.isCallExpression()
? this.tokenizeNode(currExp)
: [this.tokenizeExpression(currExp)]
}
return [...(value ? [this.tokenizeText(value)] : []), ...argTokens]
})
}
tokenizeChoiceComponent = (
path: NodePath<JSXElement>,
componentName: JsxMacroName
): Token => {
const element = path.get("openingElement")
const format = componentName.toLowerCase()
const props = element.get("attributes").filter((attr) => {
return this.attrName(
[
MsgDescriptorPropKey.id,
MsgDescriptorPropKey.comment,
MsgDescriptorPropKey.message,
MsgDescriptorPropKey.context,
"key",
// we remove <Trans /> react props that are not useful for translation
"render",
"component",
"components",
],
true
)(attr.node)
})
const token: Token = {
type: "arg",
format,
name: null,
value: undefined,
options: {
offset: undefined,
},
}
for (const _attr of props) {
if (_attr.isJSXSpreadAttribute()) {
continue
}
const attr = _attr as NodePath<JSXAttribute>
if (this.types.isJSXNamespacedName(attr.node.name)) {
continue
}
const name = attr.node.name.name
const value = attr.get("value") as
| NodePath<Literal>
| NodePath<JSXExpressionContainer>
if (name === "value") {
const exp = value.isLiteral() ? value : value.get("expression")
token.name = this.expressionToArgument(exp)
token.value = exp.node as Expression
} else if (format !== "select" && name === "offset") {
// offset is static parameter, so it must be either string or number
token.options.offset =
value.isStringLiteral() || value.isNumericLiteral()
? (value.node.value as string)
: (
(value as NodePath<JSXExpressionContainer>).get(
"expression"
) as NodePath<StringLiteral>
).node.value
} else {
let option: ArgToken["options"][number]
if (value.isStringLiteral()) {
option = (value.node.extra.raw as string).replace(
/(["'])(.*)\1/,
"$2"
)
} else {
option = this.tokenizeChildren(value as JSXChildPath)
}
if (pluralRuleRe.test(name)) {
token.options[jsx2icuExactChoice(name)] = option
} else {
token.options[name] = option
}
}
}
return token
}
tokenizeElement = (path: NodePath<JSXElement>): ElementToken => {
// !!! Important: Calculate element index before traversing children.
// That way outside elements are numbered before inner elements. (...and it looks pretty).
const name = this.elementIndex()
return {
type: "element",
name,
value: {
...path.node,
children: [],
openingElement: {
...path.node.openingElement,
selfClosing: true,
},
},
children: this.tokenizeTrans(path),
}
}
tokenizeExpression = (path: NodePath<Expression | Node>): ArgToken => {
return {
type: "arg",
name: this.expressionToArgument(path),
value: path.node as Expression,
}
}
tokenizeConditionalExpression = (
exp: NodePath<ConditionalExpression>
): ArgToken => {
exp.traverse({
JSXElement: (el) => {
if (this.isTransComponent(el) || this.isChoiceComponent(el)) {
this.replacePath(el)
el.skip()
}
},
})
return {
type: "arg",
name: this.expressionToArgument(exp),
value: exp.node,
}
}
tokenizeText = (value: string): TextToken => {
return {
type: "text",
value,
}
}
expressionToArgument(path: NodePath<Expression | Node>): string {
return path.isIdentifier() ? path.node.name : String(this.expressionIndex())
}
isLinguiComponent = (
path: NodePath,
name: JsxMacroName
): path is NodePath<JSXElement> => {
if (!path.isJSXElement()) {
return false
}
const identifier = path.get("openingElement").get("name")
return (
identifier.referencesImport(MACRO_REACT_PACKAGE, name) ||
identifier.referencesImport(MACRO_SOLID_PACKAGE, name) ||
identifier.referencesImport(MACRO_LEGACY_PACKAGE, name)
)
}
isTransComponent = (path: NodePath): path is NodePath<JSXElement> => {
return this.isLinguiComponent(path, JsxMacroName.Trans)
}
isChoiceComponent = (path: NodePath): JsxMacroName => {
if (this.isLinguiComponent(path, JsxMacroName.Plural)) {
return JsxMacroName.Plural
}
if (this.isLinguiComponent(path, JsxMacroName.Select)) {
return JsxMacroName.Select
}
if (this.isLinguiComponent(path, JsxMacroName.SelectOrdinal)) {
return JsxMacroName.SelectOrdinal
}
}
}
@@ -0,0 +1,192 @@
import { ICUMessageFormat, Tokens, ParsedResult } from "./icu"
import {
SourceLocation,
ObjectProperty,
ObjectExpression,
Expression,
} from "@babel/types"
import { EXTRACT_MARK, MsgDescriptorPropKey } from "./constants"
import * as types from "@babel/types"
import { generateMessageId } from "@lingui/message-utils/generateMessageId"
function buildICUFromTokens(tokens: Tokens) {
const messageFormat = new ICUMessageFormat()
return messageFormat.fromTokens(tokens)
}
type TextWithLoc = {
text: string
loc?: SourceLocation
}
function isObjectProperty(
node: TextWithLoc | ObjectProperty
): node is ObjectProperty {
return "type" in node
}
export function createMessageDescriptorFromTokens(
tokens: Tokens,
oldLoc: SourceLocation,
stripNonEssentialProps: boolean,
stripMessageProp: boolean,
defaults: {
id?: TextWithLoc | ObjectProperty
context?: TextWithLoc | ObjectProperty
comment?: TextWithLoc | ObjectProperty
} = {}
) {
return createMessageDescriptor(
buildICUFromTokens(tokens),
oldLoc,
stripNonEssentialProps,
stripMessageProp,
defaults
)
}
export function createMessageDescriptor(
result: Partial<ParsedResult>,
oldLoc: SourceLocation,
stripNonEssentialProps: boolean,
stripMessageProp: boolean,
defaults: {
id?: TextWithLoc | ObjectProperty
context?: TextWithLoc | ObjectProperty
comment?: TextWithLoc | ObjectProperty
} = {}
) {
const { message, values, elements } = result
const properties: ObjectProperty[] = []
properties.push(
defaults.id
? isObjectProperty(defaults.id)
? defaults.id
: createStringObjectProperty(
MsgDescriptorPropKey.id,
defaults.id.text,
defaults.id.loc
)
: createIdProperty(
message,
defaults.context
? isObjectProperty(defaults.context)
? getTextFromExpression(defaults.context.value as Expression)
: defaults.context.text
: null
)
)
if (!stripMessageProp) {
if (message) {
properties.push(
createStringObjectProperty(MsgDescriptorPropKey.message, message)
)
}
}
if (!stripNonEssentialProps) {
if (defaults.comment) {
properties.push(
isObjectProperty(defaults.comment)
? defaults.comment
: createStringObjectProperty(
MsgDescriptorPropKey.comment,
defaults.comment.text,
defaults.comment.loc
)
)
}
if (defaults.context) {
properties.push(
isObjectProperty(defaults.context)
? defaults.context
: createStringObjectProperty(
MsgDescriptorPropKey.context,
defaults.context.text,
defaults.context.loc
)
)
}
}
if (values) {
properties.push(createValuesProperty(MsgDescriptorPropKey.values, values))
}
if (elements) {
properties.push(
createValuesProperty(MsgDescriptorPropKey.components, elements)
)
}
return createMessageDescriptorObjectExpression(
properties,
// preserve line numbers for extractor
oldLoc
)
}
function createIdProperty(message: string, context?: string) {
return createStringObjectProperty(
MsgDescriptorPropKey.id,
generateMessageId(message, context)
)
}
function createValuesProperty(key: string, values: Record<string, Expression>) {
const valuesObject = Object.keys(values).map((key) =>
types.objectProperty(types.identifier(key), values[key])
)
if (!valuesObject.length) return
return types.objectProperty(
types.identifier(key),
types.objectExpression(valuesObject)
)
}
export function createStringObjectProperty(
key: string,
value: string,
oldLoc?: SourceLocation
) {
const property = types.objectProperty(
types.identifier(key),
types.stringLiteral(value)
)
if (oldLoc) {
property.loc = oldLoc
}
return property
}
function getTextFromExpression(exp: Expression): string {
if (types.isStringLiteral(exp)) {
return exp.value
}
if (types.isTemplateLiteral(exp)) {
if (exp?.quasis.length === 1) {
return exp.quasis[0]?.value?.cooked
}
}
}
function createMessageDescriptorObjectExpression(
properties: ObjectProperty[],
oldLoc?: SourceLocation
): ObjectExpression {
const newDescriptor = types.objectExpression(properties.filter(Boolean))
types.addComment(newDescriptor, "leading", EXTRACT_MARK)
if (oldLoc) {
newDescriptor.loc = oldLoc
}
return newDescriptor
}
@@ -0,0 +1,4 @@
export const makeCounter =
(index = 0) =>
() =>
index++
@@ -0,0 +1,45 @@
// taken from babel repo -> packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts
export default function cleanJSXElementLiteralChild(value: string) {
const lines = value.split(/\r\n|\n|\r/)
let lastNonEmptyLine = 0
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/[^ \t]/)) {
lastNonEmptyLine = i
}
}
let str = ""
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const isFirstLine = i === 0
const isLastLine = i === lines.length - 1
const isLastNonEmptyLine = i === lastNonEmptyLine
// replace rendered whitespace tabs with spaces
let trimmedLine = line.replace(/\t/g, " ")
// trim whitespace touching a newline
if (!isFirstLine) {
trimmedLine = trimmedLine.replace(/^[ ]+/, "")
}
// trim whitespace touching an endline
if (!isLastLine) {
trimmedLine = trimmedLine.replace(/[ ]+$/, "")
}
if (trimmedLine) {
if (!isLastNonEmptyLine) {
trimmedLine += " "
}
str += trimmedLine
}
}
return str
}
@@ -0,0 +1,203 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Production - only essential props are kept 1`] = `
import { defineMessage } from "@lingui/core/macro";
const msg = defineMessage({
message: \`Hello \${name}\`,
id: "msgId",
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
const msg =
/*i18n*/
{
id: "msgId",
values: {
name: name,
},
};
`;
exports[`Production - only essential props are kept, without id 1`] = `
import { defineMessage } from "@lingui/core/macro";
const msg = defineMessage({
message: \`Hello \${name}\`,
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
const msg =
/*i18n*/
{
id: "oT92lS",
values: {
name: name,
},
};
`;
exports[`defineMessage can be called by alias \`msg\` 1`] = `
import { msg } from "@lingui/core/macro";
const message1 = msg\`Message\`;
const message2 = msg({ message: "Message" });
↓ ↓ ↓ ↓ ↓ ↓
const message1 =
/*i18n*/
{
id: "xDAtGP",
message: "Message",
};
const message2 =
/*i18n*/
{
id: "xDAtGP",
message: "Message",
};
`;
exports[`defineMessage macro could be renamed 1`] = `
import {
defineMessage as defineMessage2,
plural as plural2,
} from "@lingui/core/macro";
const message = defineMessage2({
comment: "Description",
message: plural2(value, { one: "book", other: "books" }),
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "SlmyxX",
message: "{value, plural, one {book} other {books}}",
comment: "Description",
values: {
value: value,
},
};
`;
exports[`defineMessage should support template literal 1`] = `
import { defineMessage } from "@lingui/core/macro";
const message = defineMessage\`Message\`;
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "xDAtGP",
message: "Message",
};
`;
exports[`should expand macros in message property 1`] = `
import { defineMessage, plural, arg } from "@lingui/core/macro";
const message = defineMessage({
comment: "Description",
message: plural(value, { one: "book", other: "books" }),
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "SlmyxX",
message: "{value, plural, one {book} other {books}}",
comment: "Description",
values: {
value: value,
},
};
`;
exports[`should left string message intact 1`] = `
import { defineMessage } from "@lingui/core/macro";
const message = defineMessage({
message: "Message",
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "xDAtGP",
message: "Message",
};
`;
exports[`should preserve custom id 1`] = `
import { defineMessage } from "@lingui/core/macro";
const message = defineMessage({
id: "msg.id",
message: "Message",
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "msg.id",
message: "Message",
};
`;
exports[`should preserve values 1`] = `
import { defineMessage, t } from "@lingui/core/macro";
const message = defineMessage({
message: t\`Hello \${name}\`,
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "OVaF9k",
message: "Hello {name}",
values: {
name: name,
},
};
`;
exports[`should transform template literals 1`] = `
import { defineMessage } from "@lingui/core/macro";
const message = defineMessage({
message: \`Message \${name}\`,
});
↓ ↓ ↓ ↓ ↓ ↓
const message =
/*i18n*/
{
id: "A2aVLF",
message: "Message {name}",
values: {
name: name,
},
};
`;
@@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Macro is used in expression assignment 1`] = `
import { plural } from "@lingui/core/macro";
const a = plural(count, {
one: \`# book\`,
other: "# books",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: count,
},
}
);
`;
exports[`Macro with expression only choice 1`] = `
import { plural } from "@lingui/core/macro";
plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: someOtherExp,
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "0mcXIe",
message:
"{0, plural, offset:1 =0 {No books} =1 {1 book} other {{someOtherExp}}}",
values: {
0: users.length,
someOtherExp: someOtherExp,
},
}
);
`;
exports[`Macro with offset and exact matches 1`] = `
import { plural } from "@lingui/core/macro";
plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: "# books",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "CF5t+7",
message: "{0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}",
values: {
0: users.length,
},
}
);
`;
exports[`plural macro could be renamed 1`] = `
import { plural as plural2 } from "@lingui/core/macro";
const a = plural2(count, {
one: \`# book\`,
other: "# books",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: count,
},
}
);
`;
@@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Nested macros 1`] = `
import { select, plural } from "@lingui/core/macro";
select(gender, {
male: plural(numOfGuests, {
one: "He invites one guest",
other: "He invites # guests",
}),
female: \`She is \${gender}\`,
other: \`They is \${gender}\`,
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "G8xqGf",
message:
"{gender, select, male {{numOfGuests, plural, one {He invites one guest} other {He invites # guests}}} female {She is {gender}} other {They is {gender}}}",
values: {
gender: gender,
numOfGuests: numOfGuests,
},
}
);
`;
exports[`Nested macros with pure expressions option 1`] = `
import { select, plural } from "@lingui/core/macro";
select(gender, {
male: plural(numOfGuests, {
one: "He invites one guest",
other: "He invites # guests",
}),
female: \`She is \${gender}\`,
other: someOtherExp,
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "j9PNNm",
message:
"{gender, select, male {{numOfGuests, plural, one {He invites one guest} other {He invites # guests}}} female {She is {gender}} other {{someOtherExp}}}",
values: {
gender: gender,
numOfGuests: numOfGuests,
someOtherExp: someOtherExp,
},
}
);
`;
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#1 1`] = `
import { t, selectOrdinal } from "@lingui/core/macro";
t\`This is my \${selectOrdinal(count, {
one: "#st",
two: \`#nd\`,
other: "#rd",
})} cat\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "dJXd3T",
message:
"This is my {count, selectordinal, one {#st} two {#nd} other {#rd}} cat",
values: {
count: count,
},
}
);
`;
@@ -0,0 +1,629 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Anything variables except simple identifiers are used as positional arguments 1`] = `
import { t } from "@lingui/core/macro";
t\` Property \${props.name}, function \${random()}, array \${
array[index]
}, constant \${42}, object \${new Date()} anything \${props.messages[
index
].value()}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "vVZNZ5",
message:
" Property {0}, function {1}, array {2}, constant {3}, object {4} anything {5}",
values: {
0: props.name,
1: random(),
2: array[index],
3: 42,
4: new Date(),
5: props.messages[index].value(),
},
}
);
`;
exports[`Context might be passed as template literal 1`] = `
import { t } from "@lingui/core/macro";
t({ message: "Hello", context: "my custom" });
t({ message: "Hello", context: \`my custom\` });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "BYqAaU",
message: "Hello",
context: "my custom",
}
);
_i18n._(
/*i18n*/
{
id: "BYqAaU",
message: "Hello",
context: \`my custom\`,
}
);
`;
exports[`Macro is used in call expression 1`] = `
import { t } from "@lingui/core/macro";
const msg = message.error(t({ message: "dasd" }));
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = message.error(
_i18n._(
/*i18n*/
{
id: "9ZMZjU",
message: "dasd",
}
)
);
`;
exports[`Macro is used in expression assignment 1`] = `
import { t } from "@lingui/core/macro";
const a = t\`Expression assignment\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "mjnlP0",
message: "Expression assignment",
}
);
`;
exports[`Macro is used in expression assignment, with custom lingui instance 1`] = `
import { t } from "@lingui/core/macro";
import { customI18n } from "./lingui";
const a = t(customI18n)\`Expression assignment\`;
↓ ↓ ↓ ↓ ↓ ↓
import { customI18n } from "./lingui";
const a = customI18n._(
/*i18n*/
{
id: "mjnlP0",
message: "Expression assignment",
}
);
`;
exports[`Newlines are preserved 1`] = `
import { t } from "@lingui/core/macro";
t\`Multiline
string\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "+8iwDA",
message: "Multiline\\n string",
}
);
`;
exports[`Production - all props kept if extract: true 1`] = `
import { t } from "@lingui/core/macro";
const msg = t({
message: \`Hello \${name}\`,
id: "msgId",
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
message: "Hello {name}",
comment: "description for translators",
context: "My Context",
values: {
name: name,
},
}
);
`;
exports[`Production - message prop is kept if stripMessageField: false 1`] = `
import { t } from "@lingui/macro";
const msg = t({
message: \`Hello \${name}\`,
id: "msgId",
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
message: "Hello {name}",
values: {
name: name,
},
}
);
`;
exports[`Production - only essential props are kept 1`] = `
import { t } from "@lingui/core/macro";
const msg = t\`Message\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "xDAtGP",
}
);
`;
exports[`Production - only essential props are kept 2`] = `
import { t } from "@lingui/core/macro";
const msg = t({
message: \`Hello \${name}\`,
id: "msgId",
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
values: {
name: name,
},
}
);
`;
exports[`Production - only essential props are kept, with custom i18n instance 1`] = `
import { t } from "@lingui/core/macro";
import { i18n } from "./lingui";
const msg = t(i18n)({
message: \`Hello \${name}\`,
id: "msgId",
comment: "description for translators",
context: "My Context",
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n } from "./lingui";
const msg = i18n._(
/*i18n*/
{
id: "msgId",
values: {
name: name,
},
}
);
`;
exports[`Production - only essential props are kept, with plural, with custom i18n instance 1`] = `
import { t, plural } from "@lingui/core/macro";
const msg = t({
id: "msgId",
comment: "description for translators",
context: "some context",
message: plural(val, { one: "...", other: "..." }),
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
values: {
val: val,
},
}
);
`;
exports[`Should generate different id when context provided 1`] = `
import { t } from "@lingui/core/macro";
t({ message: "Hello" });
t({ message: "Hello", context: "my custom" });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "uzTaYi",
message: "Hello",
}
);
_i18n._(
/*i18n*/
{
id: "BYqAaU",
message: "Hello",
context: "my custom",
}
);
`;
exports[`Should not crash when a variable passed 1`] = `
import { t } from "@lingui/core/macro";
const msg = t(msg);
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(msg);
`;
exports[`Support id and comment in t macro as callExpression 1`] = `
import { t, plural } from "@lingui/core/macro";
const msg = t({
id: "msgId",
comment: "description for translators",
message: plural(val, { one: "...", other: "..." }),
});
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
message: "{val, plural, one {...} other {...}}",
comment: "description for translators",
values: {
val: val,
},
}
);
`;
exports[`Support id in template literal 1`] = `
import { t } from "@lingui/core/macro";
const msg = t({ id: \`msgId\` });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: \`msgId\`,
}
);
`;
exports[`Support id with message interpolation 1`] = `
import { t } from "@lingui/core/macro";
const msg = t({ id: "msgId", message: \`Some \${value}\` });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "msgId",
message: "Some {value}",
values: {
value: value,
},
}
);
`;
exports[`Support t in t 1`] = `
import { t } from "@lingui/core/macro";
t\`Field \${t\`First Name\`} is required\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "O8dJMg",
message: "Field {0} is required",
values: {
0: _i18n._(
/*i18n*/
{
id: "kODvZJ",
message: "First Name",
}
),
},
}
);
`;
exports[`Support template strings in t macro message 1`] = `
import { t } from "@lingui/core/macro";
const msg = t({ message: \`Hello \${name}\` });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "OVaF9k",
message: "Hello {name}",
values: {
name: name,
},
}
);
`;
exports[`Support template strings in t macro message, with custom i18n instance 1`] = `
import { t } from "@lingui/core/macro";
import { i18n } from "./lingui";
const msg = t(i18n)({ message: \`Hello \${name}\` });
↓ ↓ ↓ ↓ ↓ ↓
import { i18n } from "./lingui";
const msg = i18n._(
/*i18n*/
{
id: "OVaF9k",
message: "Hello {name}",
values: {
name: name,
},
}
);
`;
exports[`Support template strings in t macro message, with custom i18n instance object property 1`] = `
import { t } from "@lingui/core/macro";
const msg = t(global.i18n)({ message: \`Hello \${name}\` });
↓ ↓ ↓ ↓ ↓ ↓
const msg = global.i18n._(
/*i18n*/
{
id: "OVaF9k",
message: "Hello {name}",
values: {
name: name,
},
}
);
`;
exports[`Variables are replaced with named arguments 1`] = `
import { t } from "@lingui/core/macro";
t\`Variable \${name}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "xRRkAE",
message: "Variable {name}",
values: {
name: name,
},
}
);
`;
exports[`Variables should be deduplicated 1`] = `
import { t } from "@lingui/core/macro";
t\`\${duplicate} variable \${duplicate}\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "+nhkwg",
message: "{duplicate} variable {duplicate}",
values: {
duplicate: duplicate,
},
}
);
`;
exports[`Variables with escaped double quotes are correctly formatted 1`] = `
import { t } from "@lingui/core/macro";
t\`Variable "name"\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "CcPIZW",
message: 'Variable "name"',
}
);
`;
exports[`Variables with escaped template literals are correctly formatted 1`] = `
import { t } from "@lingui/core/macro";
t\`Variable \\\`\${name}\\\`\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "ICBco+",
message: "Variable \`{name}\`",
values: {
name: name,
},
}
);
`;
exports[`should correctly process nested macro when referenced from different imports 1`] = `
import { t } from "@lingui/core/macro";
import { plural } from "@lingui/core/macro";
t\`Ola! \${plural(count, { one: "1 user", many: "# users" })} is required\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "EUO+Gb",
message: "Ola! {count, plural, one {1 user} many {# users}} is required",
values: {
count: count,
},
}
);
`;
exports[`should correctly process nested macro when referenced from different imports 2 1`] = `
import { t as t1, plural as plural1 } from "@lingui/core/macro";
import { plural as plural2, t as t2 } from "@lingui/core/macro";
t1\`Ola! \${plural2(count, { one: "1 user", many: "# users" })} Ola!\`;
t2\`Ola! \${plural1(count, { one: "1 user", many: "# users" })} Ola!\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
_i18n._(
/*i18n*/
{
id: "aui5Gr",
message: "Ola! {count, plural, one {1 user} many {# users}} Ola!",
values: {
count: count,
},
}
);
_i18n._(
/*i18n*/
{
id: "wJ7AD9",
message: "Ola! {count, plural, one {1 user} many {# users}} Ola!",
values: {
count: count,
},
}
);
`;
exports[`should not crash when no params passed 1`] = `
import { t } from "@lingui/core/macro";
const msg = t();
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._();
`;
exports[`stripMessageField option - message prop is removed if stripMessageField: true 1`] = `
import { t } from "@lingui/macro";
const msg = t\`Message\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const msg = _i18n._(
/*i18n*/
{
id: "xDAtGP",
}
);
`;
exports[`t\`\` macro could be renamed 1`] = `
import { t as t2 } from "@lingui/core/macro";
const a = t2\`Expression assignment\`;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
const a = _i18n._(
/*i18n*/
{
id: "mjnlP0",
message: "Expression assignment",
}
);
`;
@@ -0,0 +1,418 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`does not crash when no params 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t();
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t();
}
`;
exports[`inserted statement should not clash with existing variables 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const _t = "i'm here";
const { t: _ } = useLingui();
const a = _\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const _t = "i'm here";
const { _: _t2 } = _useLingui();
const a = _t2(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`should not break on function currying 1`] = `
import { useLingui } from "@lingui/core/macro";
const result = curryingFoo()();
console.log("curryingFoo", result);
↓ ↓ ↓ ↓ ↓ ↓
const result = curryingFoo()();
console.log("curryingFoo", result);
`;
exports[`should process macro with matching name in correct scopes 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
{
// here is child scope with own "t" binding, shouldn't be processed
const t = () => {};
t\`Text\`;
}
{
// here is child scope which should be processed, since 't' relates to outer scope
t\`Text\`;
}
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
{
// here is child scope with own "t" binding, shouldn't be processed
const t = () => {};
t\`Text\`;
}
{
// here is child scope which should be processed, since 't' relates to outer scope
_t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
}
`;
exports[`support a variable 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t(msg);
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(msg);
}
`;
exports[`support configuring runtime module import using LinguiConfig.runtimeConfigModule 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { myUselingui as _useLingui } from "@my/lingui-react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`support i18n export 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { i18n } = useLingui();
console.log(i18n);
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { i18n } = _useLingui();
console.log(i18n);
}
`;
exports[`support message descriptor 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t({ message: "Hello", context: "my custom" });
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "BYqAaU",
message: "Hello",
context: "my custom",
}
);
}
`;
exports[`support nested macro 1`] = `
import { useLingui } from "@lingui/react/macro";
import { plural } from "@lingui/core/macro";
function MyComponent() {
const { t } = useLingui();
const a = t\`Text \${plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: "# books",
})}\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "hJRCh6",
message:
"Text {0, plural, offset:1 =0 {No books} =1 {1 book} other {# books}}",
values: {
0: users.length,
},
}
);
}
`;
exports[`support passing t variable as dependency 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = useMemo(() => t\`Text\`, [t]);
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = useMemo(
() =>
_t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
),
[_t]
);
}
`;
exports[`support renamed destructuring 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t: _ } = useLingui();
const a = _\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`tagged template literal style 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`transform to standard useLingui statement 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { i18n, t } = useLingui();
console.log(i18n);
const a = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { i18n, _: _t } = _useLingui();
console.log(i18n);
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`work with existing useLingui statement 1`] = `
import { useLingui as useLinguiMacro } from "@lingui/react/macro";
import { useLingui } from "@lingui/react";
function MyComponent() {
const { _ } = useLingui();
console.log(_);
const { t } = useLinguiMacro();
const a = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
import { useLingui } from "@lingui/react";
function MyComponent() {
const { _ } = useLingui();
console.log(_);
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`work with multiple react components 1`] = `
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
function MyComponent2() {
const { t } = useLingui();
const b = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
function MyComponent2() {
const { _: _t2 } = _useLingui();
const b = _t2(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
exports[`work with renamed existing useLingui statement 1`] = `
import { useLingui as useLinguiRenamed } from "@lingui/react";
import { useLingui as useLinguiMacro } from "@lingui/react/macro";
function MyComponent() {
const { _ } = useLinguiRenamed();
console.log(_);
const { t } = useLinguiMacro();
const a = t\`Text\`;
}
↓ ↓ ↓ ↓ ↓ ↓
import { useLingui as useLinguiRenamed } from "@lingui/react";
import { useLingui as _useLingui } from "@lingui/react";
function MyComponent() {
const { _ } = useLinguiRenamed();
console.log(_);
const { _: _t } = _useLingui();
const a = _t(
/*i18n*/
{
id: "xeiujy",
message: "Text",
}
);
}
`;
@@ -0,0 +1,263 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#1 1`] = `
import { Plural } from "@lingui/react/macro";
<Plural
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "tYX0sm",
message:
"{count, plural, offset:1 =0 {Zero items} few {{count} items} other {<0>A lot of them</0>}}",
values: {
count: count,
},
components: {
0: <a href="/more" />,
},
}
}
/>;
`;
exports[`#4 1`] = `
import { Trans, Plural } from "@lingui/react/macro";
<Plural
value={count}
one={
<Trans>
<strong>#</strong> slot added
</Trans>
}
other={
<Trans>
<strong>#</strong> slots added
</Trans>
}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "X8eyr1",
message:
"{count, plural, one {<0>#</0> slot added} other {<1>#</1> slots added}}",
values: {
count: count,
},
components: {
0: <strong />,
1: <strong />,
},
}
}
/>;
`;
exports[`#6 1`] = `
import { Plural } from "@lingui/react/macro";
<Plural
id="msg.plural"
render="strong"
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
render="strong"
{
/*i18n*/
...{
id: "msg.plural",
message:
"{count, plural, offset:1 =0 {Zero items} few {{count} items} other {<0>A lot of them</0>}}",
values: {
count: count,
},
components: {
0: <a href="/more" />,
},
}
}
/>;
`;
exports[`#7 1`] = `
import { Trans, Plural } from "@lingui/react/macro";
<Trans id="inner-id-removed">
Looking for{" "}
<Plural
value={items.length}
offset={1}
_0="zero items"
few={\`\${items.length} items \${42}\`}
other={<a href="/more">a lot of them</a>}
/>
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "inner-id-removed",
message:
"Looking for {0, plural, offset:1 =0 {zero items} few {{1} items {2}} other {<0>a lot of them</0>}}",
values: {
0: items.length,
1: items.length,
2: 42,
},
components: {
0: <a href="/more" />,
},
}
}
/>;
`;
exports[`#8 1`] = `
import { Plural } from "@lingui/react/macro";
<Plural
value={count}
_0="Zero items"
one={oneText}
other={<a href="/more">A lot of them</a>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "EQvNfC",
message:
"{count, plural, =0 {Zero items} one {{oneText}} other {<0>A lot of them</0>}}",
values: {
count: count,
oneText: oneText,
},
components: {
0: <a href="/more" />,
},
}
}
/>;
`;
exports[`Plural macro could be renamed 1`] = `
import { Plural as Plural2 } from "@lingui/react/macro";
<Plural2 value={count} one={"..."} other={"..."} />;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "EMgKyP",
message: "{count, plural, one {...} other {...}}",
values: {
count: count,
},
}
}
/>;
`;
exports[`Should preserve reserved props: \`comment\`, \`context\`, \`render\`, \`id\` 1`] = `
import { Plural } from "@lingui/react/macro";
<Plural
comment="Comment for translator"
context="translation context"
id="custom.id"
render={() => {}}
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
render={() => {}}
{
/*i18n*/
...{
id: "custom.id",
message:
"{count, plural, offset:1 =0 {Zero items} few {{count} items} other {<0>A lot of them</0>}}",
comment: "Comment for translator",
context: "translation context",
values: {
count: count,
},
components: {
0: <a href="/more" />,
},
}
}
/>;
`;
exports[`Should return cases without leading or trailing spaces for nested Trans inside Plural 1`] = `
import { Trans, Plural } from "@lingui/react/macro";
<Plural
one={<Trans>One hello</Trans>}
other={<Trans>Other hello</Trans>}
value={count}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "oukcm6",
message: "{count, plural, one {One hello} other {Other hello}}",
values: {
count: count,
},
}
}
/>;
`;
@@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#1 1`] = `
import { Select } from "@lingui/react/macro";
<Select
value={count}
_male="He"
_female={\`She\`}
other={<strong>Other</strong>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "Imwef9",
message: "{count, select, male {He} female {She} other {<0>Other</0>}}",
values: {
count: count,
},
components: {
0: <strong />,
},
}
}
/>;
`;
exports[`#2 1`] = `
import { Select } from "@lingui/react/macro";
<Select
id="msg.select"
render="strong"
value={user.gender}
_male="He"
_female={\`She\`}
other={<strong>Other</strong>}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
render="strong"
{
/*i18n*/
...{
id: "msg.select",
message: "{0, select, male {He} female {She} other {<0>Other</0>}}",
values: {
0: user.gender,
},
components: {
0: <strong />,
},
}
}
/>;
`;
exports[`#4 1`] = `
import { Select } from "@lingui/react/macro";
<Select
id="msg.select"
render="strong"
value={user.gender}
_male="He"
_female={\`She\`}
other={otherText}
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
render="strong"
{
/*i18n*/
...{
id: "msg.select",
message: "{0, select, male {He} female {She} other {{otherText}}}",
values: {
0: user.gender,
otherText: otherText,
},
}
}
/>;
`;
exports[`Select should support JSX elements in cases 1`] = `
import { Select, Trans } from "@lingui/react/macro";
<Select
value="happy"
_happy={
<Trans>
Hooray! <Icon />
</Trans>
}
_sad={
<Trans>
Oh no! <Icon />
</Trans>
}
other="Dunno"
/>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "f1ZLwG",
message:
"{0, select, happy {Hooray! <0/>} sad {Oh no! <1/>} other {Dunno}}",
values: {
0: "happy",
},
components: {
0: <Icon />,
1: <Icon />,
},
}
}
/>;
`;
@@ -0,0 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#1 1`] = `
import { Trans, SelectOrdinal } from "@lingui/react/macro";
<Trans>
This is my{" "}
<SelectOrdinal
value={count}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/>{" "}
cat.
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "3YEV8L",
message:
"This is my {count, selectordinal, one {#st} two {#nd} other {<0>#rd</0>}} cat.",
values: {
count: count,
},
components: {
0: <strong />,
},
}
}
/>;
`;
exports[`#2 1`] = `
import { Trans, SelectOrdinal } from "@lingui/react/macro";
<Trans>
This is my
<SelectOrdinal
value={count}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/>{" "}
cat.
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "Dz3XK1",
message:
"This is my{count, selectordinal, one {#st} two {#nd} other {<0>#rd</0>}} cat.",
values: {
count: count,
},
components: {
0: <strong />,
},
}
}
/>;
`;
exports[`#3 1`] = `
import { Trans, SelectOrdinal } from "@lingui/react/macro";
<Trans>
This is my{" "}
<SelectOrdinal
value={user.numCats}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/>{" "}
cat.
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "CDpzE+",
message:
"This is my {0, selectordinal, one {#st} two {#nd} other {<0>#rd</0>}} cat.",
values: {
0: user.numCats,
},
components: {
0: <strong />,
},
}
}
/>;
`;
@@ -0,0 +1,832 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Elements are replaced with placeholders 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Hello <strong>World!</strong>
<br />
<p>
My name is{" "}
<a href="/about">
{" "}
<em>{name}</em>
</a>
</p>
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "k9gsHO",
message: "Hello <0>World!</0><1/><2>My name is <3> <4>{name}</4></3></2>",
values: {
name: name,
},
components: {
0: <strong />,
1: <br />,
2: <p />,
3: <a href="/about" />,
4: <em />,
},
}
}
/>;
`;
exports[`Elements inside expression container 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>{<span>Component inside expression container</span>}</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "1cZQQW",
message: "<0>Component inside expression container</0>",
components: {
0: <span />,
},
}
}
/>;
`;
exports[`Elements without children 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>{<br />}</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "SCJtqt",
message: "<0/>",
components: {
0: <br />,
},
}
}
/>;
`;
exports[`Expressions are converted to positional arguments 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Property {props.name}, function {random()}, array {array[index]}, constant{" "}
{42}, object {new Date()}, everything {props.messages[index].value()}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "HjKDmx",
message:
"Property {0}, function {1}, array {2}, constant {3}, object {4}, everything {5}",
values: {
0: props.name,
1: random(),
2: array[index],
3: 42,
4: new Date(),
5: props.messages[index].value(),
},
}
}
/>;
`;
exports[`Generate ID from message 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "mY42CM",
message: "Hello World",
}
}
/>;
`;
exports[`Generate different id when context provided 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>Hello World</Trans>;
<Trans context="my context">Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "mY42CM",
message: "Hello World",
}
}
/>;
<_Trans
{
/*i18n*/
...{
id: "SO/WB8",
message: "Hello World",
context: "my context",
}
}
/>;
`;
exports[`HTML attributes are handled 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
<Text>This should work &nbsp;</Text>
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "K/1Xpr",
message: "<0>This should work \\xA0</0>",
components: {
0: <Text />,
},
}
}
/>;
`;
exports[`Ignore JSXEmptyExpression 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>Hello {/* and I cannot stress this enough */} World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "i0M2R8",
message: "Hello World",
}
}
/>;
`;
exports[`JSX Macro inside JSX conditional expressions 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Hello, {props.world ? <Trans>world</Trans> : <Trans>guys</Trans>}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "UT5PlM",
message: "Hello, {0}",
values: {
0: props.world ? (
<_Trans
{
/*i18n*/
...{
id: "ELi2P3",
message: "world",
}
}
/>
) : (
<_Trans
{
/*i18n*/
...{
id: "39nd+2",
message: "guys",
}
}
/>
),
},
}
}
/>;
`;
exports[`JSX Macro inside JSX multiple nested conditional expressions 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Hello,{" "}
{props.world ? (
<Trans>world</Trans>
) : props.b ? (
<Trans>nested</Trans>
) : (
<Trans>guys</Trans>
)}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "UT5PlM",
message: "Hello, {0}",
values: {
0: props.world ? (
<_Trans
{
/*i18n*/
...{
id: "ELi2P3",
message: "world",
}
}
/>
) : props.b ? (
<_Trans
{
/*i18n*/
...{
id: "lV+268",
message: "nested",
}
}
/>
) : (
<_Trans
{
/*i18n*/
...{
id: "39nd+2",
message: "guys",
}
}
/>
),
},
}
}
/>;
`;
exports[`Preserve custom ID (literal expression) 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id={"msg.hello"}>Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
message: "Hello World",
}
}
/>;
`;
exports[`Preserve custom ID (string literal) 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id="msg.hello">Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
message: "Hello World",
}
}
/>;
`;
exports[`Preserve custom ID (template expression) 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id={\`msg.hello\`}>Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
message: "Hello World",
}
}
/>;
`;
exports[`Production - all props kept if extract: true 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id="msg.hello" comment="Hello World">
Hello World
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
message: "Hello World",
comment: "Hello World",
}
}
/>;
`;
exports[`Production - import type doesn't interference on normal import 1`] = `
import type { withI18nProps } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
<Trans id="msg.hello" comment="Hello World">
Hello World
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
}
}
/>;
`;
exports[`Production - message prop is kept if stripMessageField: false 1`] = `
import { Trans } from "@lingui/macro";
<Trans id="msg.hello" comment="Hello World">
Hello World
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
message: "Hello World",
}
}
/>;
`;
exports[`Production - only essential props are kept 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans id="msg.hello" context="my context" comment="Hello World">
Hello World
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
}
}
/>;
`;
exports[`Quoted JSX attributes are handled 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>Speak "friend"!</Trans>;
<Trans id="custom-id">Speak "friend"!</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "NWmRwM",
message: 'Speak "friend"!',
}
}
/>;
<_Trans
{
/*i18n*/
...{
id: "custom-id",
message: 'Speak "friend"!',
}
}
/>;
`;
exports[`Should not process non JSXElement nodes 1`] = `
import { Trans } from "@lingui/react/macro";
type X = typeof Trans;
const cmp = <Trans>Hello</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
const cmp = (
<_Trans
{
/*i18n*/
...{
id: "uzTaYi",
message: "Hello",
}
}
/>
);
`;
exports[`Should preserve reserved props: \`comment\`, \`context\`, \`render\`, \`id\` 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans
comment="Comment for translator"
context="translation context"
id="custom.id"
render={() => {}}
>
Hello World
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
render={() => {}}
{
/*i18n*/
...{
id: "custom.id",
message: "Hello World",
comment: "Comment for translator",
context: "translation context",
}
}
/>;
`;
exports[`Strings as children are preserved 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>{"hello {count, plural, one {world} other {worlds}}"}</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "U8dd/d",
message: "hello {count, plural, one {world} other {worlds}}",
}
}
/>;
`;
exports[`Strip whitespace around arguments 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>Strip whitespace around arguments: '{name}'</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "tRMgLt",
message: "Strip whitespace around arguments: '{name}'",
values: {
name: name,
},
}
}
/>;
`;
exports[`Strip whitespace around tags but keep forced spaces 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Strip whitespace around tags, but keep <strong>forced spaces</strong>!
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "Ud4KOf",
message: "Strip whitespace around tags, but keep <0>forced spaces</0>!",
components: {
0: <strong />,
},
}
}
/>;
`;
exports[`Strip whitespace around tags but keep whitespaces in JSX containers 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
{"Wonderful framework "}
<a href="https://nextjs.org">Next.js</a>
{" say hi. And "}
<a href="https://nextjs.org">Next.js</a>
{" say hi."}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "3YVd0H",
message:
"Wonderful framework <0>Next.js</0> say hi. And <1>Next.js</1> say hi.",
components: {
0: <a href="https://nextjs.org" />,
1: <a href="https://nextjs.org" />,
},
}
}
/>;
`;
exports[`Template literals as children 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>{\`How much is \${expression}? \${count}\`}</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "exe3kM",
message: "How much is {expression}? {count}",
values: {
expression: expression,
count: count,
},
}
}
/>;
`;
exports[`Trans macro could be renamed 1`] = `
import { Trans as Trans2 } from "@lingui/react/macro";
<Trans2>Hello World</Trans2>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "mY42CM",
message: "Hello World",
}
}
/>;
`;
exports[`Use a js macro inside a JSX Attribute of a component handled by JSX macro 1`] = `
import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro";
<Trans>
Read{" "}
<a href="/more" title={t\`Full content of \${articleName}\`}>
more
</a>
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "QZyANg",
message: "Read <0>more</0>",
components: {
0: (
<a
href="/more"
title={_i18n._(
/*i18n*/
{
id: "qzc3IN",
message: "Full content of {articleName}",
values: {
articleName: articleName,
},
}
)}
/>
),
},
}
}
/>;
`;
exports[`Use a js macro inside a JSX Attribute of a non macro JSX component 1`] = `
import { plural } from "@lingui/core/macro";
<a
href="/about"
title={plural(count, {
one: "# book",
other: "# books",
})}
>
About
</a>;
↓ ↓ ↓ ↓ ↓ ↓
import { i18n as _i18n } from "@lingui/core";
<a
href="/about"
title={_i18n._(
/*i18n*/
{
id: "esnaQO",
message: "{count, plural, one {# book} other {# books}}",
values: {
count: count,
},
}
)}
>
About
</a>;
`;
exports[`Use decoded html entities 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>&amp;</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "EwTON7",
message: "&",
}
}
/>;
`;
exports[`Variables are converted to named arguments 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
Hi {yourName}, my name is {myName}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "y10VRI",
message: "Hi {yourName}, my name is {myName}",
values: {
yourName: yourName,
myName: myName,
},
}
}
/>;
`;
exports[`Variables are deduplicated 1`] = `
import { Trans } from "@lingui/react/macro";
<Trans>
{duplicate} variable {duplicate}
</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "+nhkwg",
message: "{duplicate} variable {duplicate}",
values: {
duplicate: duplicate,
},
}
}
/>;
`;
exports[`stripMessageField option - message prop is removed if stripMessageField: true 1`] = `
import { Trans } from "@lingui/macro";
<Trans id="msg.hello">Hello World</Trans>;
↓ ↓ ↓ ↓ ↓ ↓
import { Trans as _Trans } from "@lingui/react";
<_Trans
{
/*i18n*/
...{
id: "msg.hello",
}
}
/>;
`;
@@ -0,0 +1,8 @@
import { i18n as _i18n } from "@lingui/core"
_i18n._(
/*i18n*/
{
id: "LBYoFK",
message: "Multiline with continuation",
}
)
@@ -0,0 +1,4 @@
import { t } from "@lingui/core/macro"
t`Multiline\
with continuation`
@@ -0,0 +1,28 @@
import { i18n as _i18n } from "@lingui/core"
function scoped(foo) {
if (foo) {
const bar = 50
_i18n._(
/*i18n*/
{
id: "EvVtyn",
message: "This is bar {bar}",
values: {
bar: bar,
},
}
)
} else {
const bar = 10
_i18n._(
/*i18n*/
{
id: "e6QGtZ",
message: "This is a different bar {bar}",
values: {
bar: bar,
},
}
)
}
}
@@ -0,0 +1,11 @@
import { t } from "@lingui/core/macro"
function scoped(foo) {
if (foo) {
const bar = 50
t`This is bar ${bar}`
} else {
const bar = 10
t`This is a different bar ${bar}`
}
}
@@ -0,0 +1,10 @@
import { Trans as _Trans } from "@lingui/react"
;<_Trans
{
/*i18n*/
...{
id: "9xE5pD",
message: "Keep multiple\nforced\nnewlines!",
}
}
/>
@@ -0,0 +1,6 @@
import { Trans } from "@lingui/react/macro"
;<Trans>
Keep multiple{"\n"}
forced{"\n"}
newlines!
</Trans>
@@ -0,0 +1,17 @@
import { Trans as _Trans } from "@lingui/react"
;<_Trans
{
/*i18n*/
...{
id: "n0a/bN",
message:
"{genderOfHost, select, female {{numGuests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to her party.} =2 {{host} invites {guest} and one other person to her party.} other {{host} invites {guest} and # other people to her party.}}} male {{numGuests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to his party.} =2 {{host} invites {guest} and one other person to his party.} other {{host} invites {guest} and # other people to his party.}}} other {{numGuests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to their party.} =2 {{host} invites {guest} and one other person to their party.} other {{host} invites {guest} and # other people to their party.}}}}",
values: {
genderOfHost: genderOfHost,
numGuests: numGuests,
host: host,
guest: guest,
},
}
}
/>
@@ -0,0 +1,34 @@
import { Select, Plural } from "@lingui/react/macro"
;<Select
value={genderOfHost}
_female={
<Plural
value={numGuests}
offset="1"
_0={`${host} does not give a party.`}
_1={`${host} invites ${guest} to her party.`}
_2={`${host} invites ${guest} and one other person to her party.`}
other={`${host} invites ${guest} and # other people to her party.`}
/>
}
_male={
<Plural
value={numGuests}
offset="1"
_0={`${host} does not give a party.`}
_1={`${host} invites ${guest} to his party.`}
_2={`${host} invites ${guest} and one other person to his party.`}
other={`${host} invites ${guest} and # other people to his party.`}
/>
}
other={
<Plural
value={numGuests}
offset="1"
_0={`${host} does not give a party.`}
_1={`${host} invites ${guest} to their party.`}
_2={`${host} invites ${guest} and one other person to their party.`}
other={`${host} invites ${guest} and # other people to their party.`}
/>
}
/>
@@ -0,0 +1,230 @@
import path from "path"
import { transformSync } from "@babel/core"
import { getDefaultBabelOptions } from "./macroTester"
describe("macro", function () {
process.env.LINGUI_CONFIG = path.join(__dirname, "lingui.config.js")
const transformTypes = ["plugin", "macro"] as const
function forTransforms(
run: (_transformCode: (code: string) => () => string) => any
) {
return () =>
transformTypes.forEach((transformType) => {
test(transformType, () => {
return run((code) => transformCode(code, transformType))
})
})
}
// return function, so we can test exceptions
const transformCode =
(code: string, transformType: "plugin" | "macro" = "plugin") =>
() => {
try {
return transformSync(
code,
getDefaultBabelOptions(transformType)
).code.trim()
} catch (e) {
;(e as Error).message = (e as Error).message.replace(/([^:]*:){2}/, "")
throw e
}
}
it("Should throw error if used without babel-macro-plugin", async () => {
await expect(async () => {
const mod = await import("../src/macro")
return (mod as any).Trans
}).rejects.toThrow('The macro you imported from "@lingui/core/macro"')
})
describe.skip("validation", function () {
describe("plural/select/selectordinal", function () {
it("value is missing", function () {
const code = `
plural({
0: "No books",
1: "1 book",
other: "# books"
});`
expect(transformCode(code)).toThrowErrorMatchingSnapshot()
})
it("offset must be number or string, not variable", function () {
const code = `
plural({
offset: count,
0: "No books",
1: "1 book",
other: "# books"
});`
expect(transformCode(code)).toThrowErrorMatchingSnapshot()
})
it("plural forms are missing", function () {
const plural = `
plural({
value: count
});`
expect(transformCode(plural)).toThrowErrorMatchingSnapshot()
const select = `
plural({
value: count
});`
expect(transformCode(select)).toThrowErrorMatchingSnapshot()
const selectOrdinal = `
plural({
value: count
});`
expect(transformCode(selectOrdinal)).toThrowErrorMatchingSnapshot()
})
it("plural forms cannot be variables", function () {
const code = `
plural({
value: count,
[one]: "Book"
});`
expect(transformCode(code)).toThrowErrorMatchingSnapshot()
})
it("plural rules must be valid", function () {
const plural = `
plural({
value: count,
one: "Book",
three: "Invalid",
other: "Books"
});`
expect(transformCode(plural)).toThrowErrorMatchingSnapshot()
const selectOrdinal = `
selectOrdinal({
value: count,
one: "st",
three: "Invalid",
other: "rd"
});`
expect(transformCode(selectOrdinal)).toThrowErrorMatchingSnapshot()
})
})
describe("formats", function () {
it("value is missing", function () {
expect(transformCode("date();")).toThrowErrorMatchingSnapshot()
})
it("format must be either string, variable or object with custom format", function () {
expect(transformCode('number(value, "currency");')).not.toThrow()
expect(transformCode("number(value, currency);")).not.toThrow()
expect(transformCode("number(value, { digits: 4 });")).not.toThrow()
expect(
transformCode("number(value, 42);")
).toThrowErrorMatchingSnapshot()
})
})
describe("Plural/Select/SelectOrdinal", function () {
it("children are not allowed", function () {
expect(
transformCode("<Plural>Not allowed</Plural>")
).toThrowErrorMatchingSnapshot()
expect(
transformCode("<Select>Not allowed</Select>")
).toThrowErrorMatchingSnapshot()
expect(
transformCode("<SelectOrdinal>Not allowed</SelectOrdinal>")
).toThrowErrorMatchingSnapshot()
})
it("value is missing", function () {
const code = `<Plural one="Book" other="Books" />`
expect(transformCode(code)).toThrowErrorMatchingSnapshot()
})
it("offset must be number or string, not variable", function () {
const variable = `<Plural value={value} offset={offset} one="Book" other="Books" />`
expect(transformCode(variable)).toThrowErrorMatchingSnapshot()
})
it("plural forms are missing", function () {
const plural = `<Plural value={value} />`
expect(transformCode(plural)).toThrowErrorMatchingSnapshot()
const select = `<Select value={value} />`
expect(transformCode(select)).toThrowErrorMatchingSnapshot()
const ordinal = `<SelectOrdinal value={value} />`
expect(transformCode(ordinal)).toThrowErrorMatchingSnapshot()
})
it("plural rules must be valid", function () {
const plural = `<Plural value={value} three="Invalid" one="Book" other="Books" />`
expect(transformCode(plural)).toThrowErrorMatchingSnapshot()
const ordinal = `<SelectOrdinal value={value} three="Invalid" one="st" other="rd" />`
expect(transformCode(ordinal)).toThrowErrorMatchingSnapshot()
})
})
})
describe("useLingui validation", () => {
describe(
"Should throw if used not in the variable declaration",
forTransforms((transformCode) => {
const code = `
import {useLingui} from "@lingui/react/macro";
useLingui()
`
expect(transformCode(code)).toThrowError(
"Error: `useLingui` macro must be used in variable declaration."
)
})
)
describe(
"Should throw if not used with destructuring",
forTransforms((transformCode) => {
const code = `
import {useLingui} from "@lingui/react/macro";
const lingui = useLingui()
`
expect(transformCode(code)).toThrowError(
"You have to destructure `t` when using the `useLingui` macro"
)
})
)
})
describe("Trans validation", () => {
describe(
"Should throw if spread used in children",
forTransforms((transformCode) => {
const code = `
import { Trans } from '@lingui/react/macro';
<Trans>{...spread}</Trans>
`
expect(transformCode(code)).toThrowError("Incorrect usage of Trans")
})
)
describe(
"Should throw if used without children",
forTransforms((transformCode) => {
const code = `
import { Trans } from '@lingui/react/macro';
<Trans id={msg} />;
`
expect(transformCode(code)).toThrowError("Incorrect usage of Trans")
})
)
})
})
@@ -0,0 +1,103 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
name: "defineMessage should support template literal",
code: `
import { defineMessage } from '@lingui/core/macro';
const message = defineMessage\`Message\`
`,
},
{
name: "defineMessage can be called by alias `msg`",
code: `
import { msg } from '@lingui/core/macro';
const message1 = msg\`Message\`
const message2 = msg({message: "Message"})
`,
},
{
name: "should expand macros in message property",
code: `
import { defineMessage, plural, arg } from '@lingui/core/macro';
const message = defineMessage({
comment: "Description",
message: plural(value, { one: "book", other: "books" })
})
`,
},
{
name: "defineMessage macro could be renamed",
code: `
import { defineMessage as defineMessage2, plural as plural2 } from '@lingui/core/macro';
const message = defineMessage2({
comment: "Description",
message: plural2(value, { one: "book", other: "books" })
})
`,
},
{
name: "should left string message intact",
code: `
import { defineMessage } from '@lingui/core/macro';
const message = defineMessage({
message: "Message"
})
`,
},
{
name: "should transform template literals",
code: `
import { defineMessage } from '@lingui/core/macro';
const message = defineMessage({
message: \`Message \${name}\`
})
`,
},
{
name: "should preserve custom id",
code: `
import { defineMessage } from '@lingui/core/macro';
const message = defineMessage({
id: "msg.id",
message: "Message"
})
`,
},
{
name: "Production - only essential props are kept, without id",
production: true,
code: `
import { defineMessage } from '@lingui/core/macro';
const msg = defineMessage({
message: \`Hello $\{name\}\`,
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "Production - only essential props are kept",
production: true,
code: `
import { defineMessage } from '@lingui/core/macro';
const msg = defineMessage({
message: \`Hello $\{name\}\`,
id: 'msgId',
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "should preserve values",
code: `
import { defineMessage, t } from '@lingui/core/macro';
const message = defineMessage({
message: t\`Hello $\{name\}\`
})
`,
},
],
})
@@ -0,0 +1,50 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
name: "Macro is used in expression assignment",
code: `
import { plural } from '@lingui/core/macro'
const a = plural(count, {
"one": \`# book\`,
other: "# books"
});
`,
},
{
name: "plural macro could be renamed",
code: `
import { plural as plural2 } from '@lingui/core/macro'
const a = plural2(count, {
"one": \`# book\`,
other: "# books"
});
`,
},
{
name: "Macro with offset and exact matches",
code: `
import { plural } from '@lingui/core/macro'
plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: "# books"
});
`,
},
{
name: "Macro with expression only choice",
code: `
import { plural } from '@lingui/core/macro'
plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: someOtherExp
});
`,
},
],
})
@@ -0,0 +1,34 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
name: "Nested macros",
code: `
import { select, plural } from '@lingui/core/macro'
select(gender, {
"male": plural(numOfGuests, {
one: "He invites one guest",
other: "He invites # guests"
}),
female: \`She is \${gender}\`,
other: \`They is \${gender}\`
});
`,
},
{
name: "Nested macros with pure expressions option",
code: `
import { select, plural } from '@lingui/core/macro'
select(gender, {
"male": plural(numOfGuests, {
one: "He invites one guest",
other: "He invites # guests"
}),
female: \`She is \${gender}\`,
other: someOtherExp
});
`,
},
],
})
@@ -0,0 +1,16 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
code: `
import { t, selectOrdinal } from '@lingui/core/macro'
t\`This is my \${selectOrdinal(count, {
one: "#st",
"two": \`#nd\`,
other: ("#rd")
})} cat\`
`,
},
],
})
@@ -0,0 +1,281 @@
import { macroTester } from "./macroTester"
describe.skip("", () => {})
macroTester({
cases: [
{
name: "Macro is used in expression assignment",
code: `
import { t } from '@lingui/core/macro';
const a = t\`Expression assignment\`;
`,
},
{
name: "Macro is used in call expression",
code: `
import { t } from '@lingui/core/macro';
const msg = message.error(t({message: "dasd"}))
`,
},
{
name: "t`` macro could be renamed",
code: `
import { t as t2 } from '@lingui/core/macro';
const a = t2\`Expression assignment\`;
`,
},
{
name: "Macro is used in expression assignment, with custom lingui instance",
code: `
import { t } from '@lingui/core/macro';
import { customI18n } from './lingui';
const a = t(customI18n)\`Expression assignment\`;
`,
},
{
name: "Variables are replaced with named arguments",
code: `
import { t } from '@lingui/core/macro';
t\`Variable \${name}\`;
`,
},
{
name: "Variables with escaped template literals are correctly formatted",
code: `
import { t } from '@lingui/core/macro';
t\`Variable \\\`\${name}\\\`\`;
`,
},
{
name: "Variables with escaped double quotes are correctly formatted",
code: `
import { t } from '@lingui/core/macro';
t\`Variable \"name\"\`;
`,
},
{
name: "Variables should be deduplicated",
code: `
import { t } from '@lingui/core/macro';
t\`\${duplicate} variable \${duplicate}\`;
`,
},
{
name: "Anything variables except simple identifiers are used as positional arguments",
code: `
import { t } from '@lingui/core/macro';
t\`\
Property \${props.name},\
function \${random()},\
array \${array[index]},\
constant \${42},\
object \${new Date()}\
anything \${props.messages[index].value()}\
\`
`,
},
{
name: "Newlines are preserved",
code: `
import { t } from '@lingui/core/macro';
t\`Multiline
string\`
`,
},
{
name: "Support template strings in t macro message",
code: `
import { t } from '@lingui/core/macro'
const msg = t({ message: \`Hello \${name}\` })
`,
},
{
name: "Support template strings in t macro message, with custom i18n instance",
code: `
import { t } from '@lingui/core/macro'
import { i18n } from './lingui'
const msg = t(i18n)({ message: \`Hello \${name}\` })
`,
},
{
name: "Support template strings in t macro message, with custom i18n instance object property",
code: `
import { t } from '@lingui/core/macro'
const msg = t(global.i18n)({ message: \`Hello \${name}\` })
`,
},
{
name: "Should generate different id when context provided",
code: `
import { t } from '@lingui/core/macro'
t({ message: "Hello" })
t({ message: "Hello", context: "my custom" })
`,
},
{
name: "Context might be passed as template literal",
code: `
import { t } from '@lingui/core/macro'
t({ message: "Hello", context: "my custom" })
t({ message: "Hello", context: \`my custom\` })
`,
},
{
name: "Support id and comment in t macro as callExpression",
code: `
import { t, plural } from '@lingui/core/macro'
const msg = t({ id: 'msgId', comment: 'description for translators', message: plural(val, { one: '...', other: '...' }) })
`,
},
{
name: "Support id with message interpolation",
code: `
import { t } from '@lingui/core/macro'
const msg = t({ id: 'msgId', message: \`Some \${value}\` })
`,
},
{
name: "Support id in template literal",
code: `
import { t } from '@lingui/core/macro'
const msg = t({ id: \`msgId\` })
`,
},
{
name: "Should not crash when a variable passed",
code: `
import { t } from '@lingui/core/macro'
const msg = t(msg)
`,
},
{
name: "should not crash when no params passed",
code: `
import { t } from '@lingui/core/macro'
const msg = t()
`,
},
{
name: "stripMessageField option - message prop is removed if stripMessageField: true",
macroOpts: {
stripMessageField: true,
},
code: `
import { t } from '@lingui/macro'
const msg = t\`Message\`
`,
},
{
name: "Production - only essential props are kept",
production: true,
code: `
import { t } from '@lingui/core/macro';
const msg = t\`Message\`
`,
},
{
name: "Production - only essential props are kept, with plural, with custom i18n instance",
production: true,
code: `
import { t, plural } from '@lingui/core/macro';
const msg = t({
id: 'msgId',
comment: 'description for translators',
context: 'some context',
message: plural(val, { one: '...', other: '...' })
})
`,
},
{
name: "Production - only essential props are kept, with custom i18n instance",
production: true,
code: `
import { t } from '@lingui/core/macro';
import { i18n } from './lingui';
const msg = t(i18n)({
message: \`Hello $\{name\}\`,
id: 'msgId',
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "Production - only essential props are kept",
production: true,
code: `
import { t } from '@lingui/core/macro';
const msg = t({
message: \`Hello $\{name\}\`,
id: 'msgId',
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "Production - message prop is kept if stripMessageField: false",
production: true,
macroOpts: {
stripMessageField: false,
},
code: `
import { t } from '@lingui/macro';
const msg = t({
message: \`Hello $\{name\}\`,
id: 'msgId',
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "Production - all props kept if extract: true",
production: true,
macroOpts: {
extract: true,
},
code: `
import { t } from '@lingui/core/macro';
const msg = t({
message: \`Hello $\{name\}\`,
id: 'msgId',
comment: 'description for translators',
context: 'My Context',
})
`,
},
{
name: "Newlines after continuation character are removed",
filename: "js-t-continuation-character.js",
},
{
filename: "js-t-var/js-t-var.js",
},
{
name: "Support t in t",
code: `
import { t } from '@lingui/core/macro'
t\`Field \${t\`First Name\`} is required\`
`,
},
{
name: "should correctly process nested macro when referenced from different imports",
code: `
import { t } from '@lingui/core/macro'
import { plural } from '@lingui/core/macro'
t\`Ola! \${plural(count, {one: "1 user", many: "# users"})} is required\`
`,
},
{
name: "should correctly process nested macro when referenced from different imports 2",
code: `
import { t as t1, plural as plural1 } from '@lingui/core/macro'
import { plural as plural2, t as t2 } from '@lingui/core/macro'
t1\`Ola! \${plural2(count, {one: "1 user", many: "# users"})} Ola!\`
t2\`Ola! \${plural1(count, {one: "1 user", many: "# users"})} Ola!\`
`,
},
],
})
@@ -0,0 +1,227 @@
import { makeConfig } from "@lingui/conf"
import { macroTester } from "./macroTester"
describe.skip("", () => {})
macroTester({
cases: [
{
name: "tagged template literal style",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
`,
},
{
name: "support renamed destructuring",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t: _ } = useLingui();
const a = _\`Text\`;
}
`,
},
{
name: "should process macro with matching name in correct scopes",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
{
// here is child scope with own "t" binding, shouldn't be processed
const t = () => {};
t\`Text\`;
}
{
// here is child scope which should be processed, since 't' relates to outer scope
t\`Text\`;
}
}
`,
},
{
name: "inserted statement should not clash with existing variables",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const _t = "i'm here";
const { t: _ } = useLingui();
const a = _\`Text\`;
}
`,
},
{
name: "support nested macro",
code: `
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
function MyComponent() {
const { t } = useLingui();
const a = t\`Text \${plural(users.length, {
offset: 1,
0: "No books",
1: "1 book",
other: "# books"
})}\`;
}
`,
},
{
name: "support message descriptor",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t({ message: "Hello", context: "my custom" });
}
`,
},
{
name: "support a variable",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t(msg);
}
`,
},
{
name: "does not crash when no params",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t();
}
`,
},
{
name: "support passing t variable as dependency",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = useMemo(() => t\`Text\`, [t]);
}
`,
},
{
name: "transform to standard useLingui statement",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { i18n, t } = useLingui();
console.log(i18n);
const a = t\`Text\`;
}
`,
},
{
name: "support i18n export",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { i18n } = useLingui();
console.log(i18n);
}
`,
},
{
name: "work with existing useLingui statement",
code: `
import { useLingui as useLinguiMacro } from '@lingui/react/macro';
import { useLingui } from '@lingui/react';
function MyComponent() {
const { _ } = useLingui();
console.log(_);
const { t } = useLinguiMacro();
const a = t\`Text\`;
}
`,
},
{
name: "work with renamed existing useLingui statement",
code: `
import { useLingui as useLinguiRenamed } from '@lingui/react';
import { useLingui as useLinguiMacro } from '@lingui/react/macro';
function MyComponent() {
const { _ } = useLinguiRenamed();
console.log(_);
const { t } = useLinguiMacro();
const a = t\`Text\`;
}
`,
},
{
name: "should not break on function currying",
code: `
import { useLingui } from '@lingui/core/macro';
const result = curryingFoo()()
console.log('curryingFoo', result)
`,
},
{
name: "work with multiple react components",
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
function MyComponent2() {
const { t } = useLingui();
const b = t\`Text\`;
}`,
},
{
name: "support configuring runtime module import using LinguiConfig.runtimeConfigModule",
macroOpts: {
linguiConfig: makeConfig(
{
runtimeConfigModule: {
useLingui: ["@my/lingui-react", "myUselingui"],
},
},
{ skipValidation: true }
),
},
code: `
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { t } = useLingui();
const a = t\`Text\`;
}
`,
},
],
})
@@ -0,0 +1,128 @@
import { macroTester } from "./macroTester"
describe.skip("", () => {})
macroTester({
cases: [
{
code: `
import { Plural } from '@lingui/react/macro';
<Plural
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
`,
},
{
name: "Plural macro could be renamed",
code: `
import { Plural as Plural2 } from '@lingui/react/macro';
<Plural2
value={count}
one={"..."}
other={"..."}
/>;
`,
},
{
name: "Should preserve reserved props: `comment`, `context`, `render`, `id`",
code: `
import { Plural } from '@lingui/react/macro';
<Plural
comment="Comment for translator"
context="translation context"
id="custom.id"
render={() => {}}
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
`,
},
{
code: `
import { Trans, Plural } from '@lingui/react/macro';
<Plural
value={count}
one={
<Trans>
<strong>#</strong> slot added
</Trans>
}
other={
<Trans>
<strong>#</strong> slots added
</Trans>
}
/>;
`,
},
{
name: "Should return cases without leading or trailing spaces for nested Trans inside Plural",
code: `
import { Trans, Plural } from '@lingui/react/macro';
<Plural
one={
<Trans>
One hello
</Trans>
}
other={
<Trans>
Other hello
</Trans>
}
value={count}
/>;
`,
},
{
code: `
import { Plural } from '@lingui/react/macro';
<Plural
id="msg.plural"
render="strong"
value={count}
offset="1"
_0="Zero items"
few={\`\${count} items\`}
other={<a href="/more">A lot of them</a>}
/>;
`,
},
{
code: `
import { Trans, Plural } from '@lingui/react/macro';
<Trans id="inner-id-removed">
Looking for{" "}
<Plural
value={items.length}
offset={1}
_0="zero items"
few={\`\${items.length} items \${42}\`}
other={<a href="/more">a lot of them</a>}
/>
</Trans>
`,
},
{
code: `
import { Plural } from '@lingui/react/macro';
<Plural
value={count}
_0="Zero items"
one={oneText}
other={<a href="/more">A lot of them</a>}
/>;
`,
},
{
filename: `jsx-plural-select-nested.js`,
},
],
})
@@ -0,0 +1,59 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
code: `
import { Select } from '@lingui/react/macro';
<Select
value={count}
_male="He"
_female={\`She\`}
other={<strong>Other</strong>}
/>;
`,
},
{
code: `
import { Select } from '@lingui/react/macro';
<Select
id="msg.select"
render="strong"
value={user.gender}
_male="He"
_female={\`She\`}
other={<strong>Other</strong>}
/>;
`,
},
{
name: "Select should support JSX elements in cases",
code: `
import { Select, Trans } from '@lingui/react/macro';
<Select
value="happy"
_happy={
<Trans>Hooray! <Icon /></Trans>
}
_sad={
<Trans>Oh no! <Icon /></Trans>
}
other="Dunno"
/>
`,
},
{
code: `
import { Select } from '@lingui/react/macro';
<Select
id="msg.select"
render="strong"
value={user.gender}
_male="He"
_female={\`She\`}
other={otherText}
/>;
`,
},
],
})
@@ -0,0 +1,47 @@
import { macroTester } from "./macroTester"
macroTester({
cases: [
{
code: `
import { Trans, SelectOrdinal } from '@lingui/react/macro';
<Trans>
This is my <SelectOrdinal
value={count}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/> cat.
</Trans>;
`,
},
{
// without trailing whitespace ICU expression on the next line will not have a space
code: `
import { Trans, SelectOrdinal } from '@lingui/react/macro';
<Trans>
This is my
<SelectOrdinal
value={count}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/> cat.
</Trans>;
`,
},
{
code: `
import { Trans, SelectOrdinal } from '@lingui/react/macro';
<Trans>
This is my <SelectOrdinal
value={user.numCats}
one="#st"
two={\`#nd\`}
other={<strong>#rd</strong>}
/> cat.
</Trans>;
`,
},
],
})
@@ -0,0 +1,297 @@
import { macroTester } from "./macroTester"
describe.skip("", () => {})
macroTester({
cases: [
{
name: "Generate ID from message",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hello World</Trans>;
`,
},
{
name: "Generate different id when context provided",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hello World</Trans>;
<Trans context="my context">Hello World</Trans>;
`,
},
{
name: "Preserve custom ID (string literal)",
code: `
import { Trans } from '@lingui/react/macro';
<Trans id="msg.hello">Hello World</Trans>;
`,
},
{
name: "Preserve custom ID (literal expression)",
code: `
import { Trans } from '@lingui/react/macro';
<Trans id={"msg.hello"}>Hello World</Trans>;
`,
},
{
name: "Preserve custom ID (template expression)",
code: `
import { Trans } from '@lingui/react/macro';
<Trans id={\`msg.hello\`}>Hello World</Trans>;
`,
},
{
name: "Should preserve reserved props: `comment`, `context`, `render`, `id`",
code: `
import { Trans } from '@lingui/react/macro';
<Trans
comment="Comment for translator"
context="translation context"
id="custom.id"
render={() => {}}
>Hello World</Trans>;
`,
},
{
name: "Trans macro could be renamed",
code: `
import { Trans as Trans2 } from '@lingui/react/macro';
<Trans2>Hello World</Trans2>;
`,
},
{
name: "Variables are converted to named arguments",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hi {yourName}, my name is {myName}</Trans>;
`,
},
{
name: "Variables are deduplicated",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>{duplicate} variable {duplicate}</Trans>;
`,
},
{
name: "Quoted JSX attributes are handled",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Speak "friend"!</Trans>;
<Trans id="custom-id">Speak "friend"!</Trans>;
`,
},
{
name: "HTML attributes are handled",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>
<Text>This should work &nbsp;</Text>
</Trans>;
`,
},
{
name: "Template literals as children",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>{\`How much is \${expression}? \${count}\`}</Trans>;
`,
},
{
name: "Strings as children are preserved",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>{"hello {count, plural, one {world} other {worlds}}"}</Trans>;
`,
},
{
name: "Expressions are converted to positional arguments",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>
Property {props.name},
function {random()},
array {array[index]},
constant {42},
object {new Date()},
everything {props.messages[index].value()}
</Trans>;
`,
},
{
name: "JSX Macro inside JSX conditional expressions",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hello, {props.world ? <Trans>world</Trans> : <Trans>guys</Trans>}</Trans>
`,
},
{
name: "JSX Macro inside JSX multiple nested conditional expressions",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hello, {props.world ? <Trans>world</Trans> : (
props.b
? <Trans>nested</Trans>
: <Trans>guys</Trans>
)
}</Trans>
`,
},
{
name: "Elements are replaced with placeholders",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>
Hello <strong>World!</strong><br />
<p>
My name is <a href="/about">{" "}
<em>{name}</em></a>
</p>
</Trans>;
`,
},
{
name: "Elements inside expression container",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>{<span>Component inside expression container</span>}</Trans>;
`,
},
{
name: "Elements without children",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>{<br />}</Trans>;
`,
},
{
name: "stripMessageField option - message prop is removed if stripMessageField: true",
macroOpts: {
stripMessageField: true,
},
code: `
import { Trans } from '@lingui/macro';
<Trans id="msg.hello">Hello World</Trans>
`,
},
{
name: "Production - only essential props are kept",
production: true,
code: `
import { Trans } from '@lingui/react/macro';
<Trans id="msg.hello" context="my context" comment="Hello World">Hello World</Trans>
`,
},
{
name: "Production - message prop is kept if stripMessageField: false",
production: true,
macroOpts: {
stripMessageField: false,
},
code: `
import { Trans } from '@lingui/macro';
<Trans id="msg.hello" comment="Hello World">Hello World</Trans>
`,
},
{
name: "Production - all props kept if extract: true",
production: true,
macroOpts: {
extract: true,
},
code: `
import { Trans } from '@lingui/react/macro';
<Trans id="msg.hello" comment="Hello World">Hello World</Trans>
`,
},
{
name: "Production - import type doesn't interference on normal import",
production: true,
useTypescriptPreset: true,
code: `
import type { withI18nProps } from '@lingui/react'
import { Trans } from '@lingui/react/macro';
<Trans id="msg.hello" comment="Hello World">Hello World</Trans>
`,
},
{
name: "Strip whitespace around arguments",
code: `
import { Trans } from "@lingui/react/macro";
<Trans>
Strip whitespace around arguments: '
{name}
'
</Trans>
`,
},
{
name: "Strip whitespace around tags but keep forced spaces",
code: `
import { Trans } from "@lingui/react/macro";
<Trans>
Strip whitespace around tags, but keep{" "}
<strong>forced spaces</strong>
!
</Trans>
`,
},
{
name: "Strip whitespace around tags but keep whitespaces in JSX containers",
code: `
import { Trans } from "@lingui/react/macro";
<Trans>
{"Wonderful framework "}
<a href="https://nextjs.org">Next.js</a>
{" say hi. And "}
<a href="https://nextjs.org">Next.js</a>
{" say hi."}
</Trans>
`,
},
{
name: "Keep forced newlines",
filename: "./jsx-keep-forced-newlines.js",
},
{
name: "Use a js macro inside a JSX Attribute of a component handled by JSX macro",
code: `
import { Trans } from '@lingui/react/macro';
import { t } from '@lingui/core/macro';
<Trans>Read <a href="/more" title={t\`Full content of \${articleName}\`}>more</a></Trans>
`,
},
{
name: "Use a js macro inside a JSX Attribute of a non macro JSX component",
code: `
import { plural } from '@lingui/core/macro';
<a href="/about" title={plural(count, {
one: "# book",
other: "# books"
})}>About</a>
`,
},
{
name: "Ignore JSXEmptyExpression",
code: `
import { Trans } from '@lingui/react/macro';
<Trans>Hello {/* and I cannot stress this enough */} World</Trans>;
`,
},
{
name: "Use decoded html entities",
code: `
import { Trans } from "@lingui/react/macro";
<Trans>&amp;</Trans>
`,
},
{
name: "Should not process non JSXElement nodes",
useTypescriptPreset: true,
code: `
import { Trans } from "@lingui/react/macro";
type X = typeof Trans;
const cmp = <Trans>Hello</Trans>
`,
},
],
})
@@ -0,0 +1,3 @@
module.exports = {
locales: ["en"],
}
@@ -0,0 +1,150 @@
// use package path instead relative because we want
// to test it in from /dist folder in integration tests
import linguiMacroPlugin, {
LinguiPluginOpts,
} from "@lingui-solid/babel-plugin-lingui-macro"
import { transformFileSync, transformSync, TransformOptions } from "@babel/core"
import prettier from "prettier"
import path from "path"
import fs from "fs"
export type TestCase = TestCaseInline | TestCaseFixture
type TestCaseInline = {
/**
* Input code for testing
*/
code: string
/**
* If not set, snapshot testing will be used
*/
expected?: string
} & TestCaseCommon
type TestCaseFixture = {
filename: string
} & TestCaseCommon
type TestCaseCommon = {
name?: string
production?: boolean
useTypescriptPreset?: boolean
macroOpts?: LinguiPluginOpts
only?: boolean
skip?: boolean
}
export type MacroTesterOptions = {
cases: TestCase[]
}
export function macroTester(opts: MacroTesterOptions) {
process.env.LINGUI_CONFIG = path.join(__dirname, "lingui.config.js")
const clean = (value: string) =>
prettier.format(value, { parser: "babel-ts" }).replace(/\n+/, "\n")
opts.cases.forEach((testCase, index) => {
const { name, production, only, skip, useTypescriptPreset, macroOpts } =
testCase
let group = test
if (only) group = test.only
if (skip) group = test.skip
const groupName = name != null ? name : `#${index + 1}`
group(groupName, () => {
const originalEnv = process.env.NODE_ENV
if (production) {
process.env.NODE_ENV = "production"
}
try {
if ("filename" in testCase) {
const inputPath = path.relative(
process.cwd(),
path.join(__dirname, "fixtures", testCase.filename)
)
const expectedPath = inputPath.replace(/\.js$/, ".expected.js")
const expected = fs
.readFileSync(expectedPath, "utf8")
.replace(/\r/g, "")
.trim()
const actualPlugin = transformFileSync(inputPath, {
...getDefaultBabelOptions("plugin", macroOpts, useTypescriptPreset),
cwd: path.dirname(inputPath),
})
.code.replace(/\r/g, "")
.trim()
const actualMacro = transformFileSync(inputPath, {
...getDefaultBabelOptions("plugin", macroOpts, useTypescriptPreset),
cwd: path.dirname(inputPath),
})
.code.replace(/\r/g, "")
.trim()
// output from plugin transformation should be the same to macro transformation
expect(actualPlugin).toBe(actualMacro)
expect(clean(actualPlugin)).toEqual(clean(expected))
} else {
const actualPlugin = transformSync(
testCase.code,
getDefaultBabelOptions("plugin", macroOpts, useTypescriptPreset)
).code.trim()
const actualMacro = transformSync(
testCase.code,
getDefaultBabelOptions("macro", macroOpts, useTypescriptPreset)
).code.trim()
// output from plugin transformation should be the same to macro transformation
expect(actualPlugin).toBe(actualMacro)
if (testCase.expected) {
expect(clean(actualPlugin)).toEqual(clean(testCase.expected))
} else {
expect(
clean(testCase.code) + "\n↓ ↓ ↓ ↓ ↓ ↓\n\n" + clean(actualPlugin)
).toMatchSnapshot()
}
}
} finally {
process.env.LINGUI_CONFIG = ""
process.env.NODE_ENV = originalEnv
}
})
})
}
export const getDefaultBabelOptions = (
transformType: "plugin" | "macro" = "plugin",
macroOpts: LinguiPluginOpts = {},
isTs: boolean = false
): TransformOptions => {
return {
filename: "<filename>" + (isTs ? ".tsx" : "jsx"),
configFile: false,
babelrc: false,
presets: [...(isTs ? ["@babel/preset-typescript"] : [])],
plugins: [
"@babel/plugin-syntax-jsx",
transformType === "plugin"
? [linguiMacroPlugin, macroOpts]
: [
"macros",
{
lingui: macroOpts,
// macro plugin uses package `resolve` to find a path of macro file
// this will not follow jest pathMapping and will resolve path from ./build
// instead of ./src which makes testing & developing hard.
// here we override resolve and provide correct path for testing
resolvePath: (source: string) => require.resolve(source),
},
],
],
}
}
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"strictNullChecks": false
}
}
+47
View File
@@ -0,0 +1,47 @@
import { mockEnv, mockConsole } from "./index"
describe("mocks - testing utilities", function () {
describe("mockEnv", function () {
it("should mock NODE_ENV", function () {
expect(process.env.NODE_ENV).not.toEqual("xyz")
mockEnv("xyz", () => {
expect(process.env.NODE_ENV).toEqual("xyz")
})
expect(process.env.NODE_ENV).not.toEqual("xyz")
})
it("should restore original NODE_ENV or error", function () {
expect(process.env.NODE_ENV).not.toEqual("xyz")
expect(() =>
mockEnv("xyz", () => expect(true).toBeFalsy())
).toThrowError()
expect(process.env.NODE_ENV).not.toEqual("xyz")
})
})
describe("mockConsole", function () {
it("should mock console object", function () {
expect(typeof console.log).toEqual("function")
mockConsole(
() => {
expect(typeof console.log).toEqual("string")
},
{ log: "log" }
)
expect(typeof console.log).toEqual("function")
})
it("should restore original NODE_ENV or error", function () {
expect(typeof console.log).toEqual("function")
expect(() =>
mockConsole(() => expect(true).toBeFalsy(), { log: "log" })
).toThrowError()
expect(typeof console.log).toEqual("function")
})
})
})
+58
View File
@@ -0,0 +1,58 @@
export function getConsoleMockCalls({ mock }: jest.MockInstance<any, any>) {
if (!mock.calls.length) return
return mock.calls.map((call) => call[0]).join("\n")
}
export function mockConsole(
testCase: (console: jest.Mocked<Console>) => any,
mock = {}
) {
function restoreConsole() {
global.console = originalConsole
}
const originalConsole = global.console
const defaults = {
log: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}
global.console = {
...defaults,
...mock,
} as any
let result
try {
result = testCase(global.console as jest.Mocked<Console>)
} catch (e) {
restoreConsole()
throw e
}
if (result && typeof result.then === "function") {
return result.then(restoreConsole).catch((e) => {
restoreConsole()
throw e
})
} else {
restoreConsole()
return result
}
}
export function mockEnv(env, testCase) {
const oldEnv = process.env.NODE_ENV
process.env.NODE_ENV = env
try {
testCase()
} catch (e) {
process.env.NODE_ENV = oldEnv
throw e
}
process.env.NODE_ENV = oldEnv
}
+32
View File
@@ -0,0 +1,32 @@
{
"private": true,
"name": "@lingui/jest-mocks",
"version": "3.0.3",
"description": "Mocks for Jest",
"main": "index.js",
"author": {
"name": "Tomáš Ehrlich",
"email": "tomas.ehrlich@gmail.com"
},
"license": "MIT",
"keywords": [
"jest",
"testing",
"mock"
],
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=20.0.0"
},
"files": [
"LICENSE",
"README.md",
"index.js"
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"plugins": ["solid"],
"extends": ["../../.eslintrc", "plugin:solid/typescript"]
}
+4
View File
@@ -0,0 +1,4 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+34
View File
@@ -0,0 +1,34 @@
[![License][badge-license]][license]
[![Version][badge-version]][package]
[![Downloads][badge-downloads]][package]
# @lingui-solid/solid
> SolidJS components for internationalization
`@lingui-solid/solid` is part of [LinguiJS][linguijs]. See the [documentation][documentation] for all information, tutorials and examples.
## Installation
```sh
npm install --save @lingui-solid/solid
# yarn add @lingui-solid/solid
```
## Usage
See the [tutorial][tutorial] or [reference][reference] documentation.
## License
[MIT][license]
[license]: https://github.com/lingui/js-lingui/blob/main/LICENSE
[linguijs]: https://github.com/lingui/js-lingui
[documentation]: https://lingui.dev
[tutorial]: https://lingui.dev/tutorials/solid
[reference]: https://lingui.dev/ref/solid
[package]: https://www.npmjs.com/package/@lingui-solid/solid
[badge-downloads]: https://img.shields.io/npm/dw/@lingui-solid/solid.svg
[badge-version]: https://img.shields.io/npm/v/@lingui-solid/solid.svg
[badge-license]: https://img.shields.io/npm/l/@lingui-solid/solid.svg
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: 16,
},
modules: "commonjs",
},
],
"@babel/preset-typescript",
"babel-preset-solid",
],
}
@@ -0,0 +1,130 @@
import { expectType } from "tsd"
import type { I18n } from "@lingui/core"
import {
Trans,
Plural,
Select,
SelectOrdinal,
useLingui,
} from "@lingui-solid/solid/macro"
const gender = "male"
let m: any
///////////////////
//// JSX Trans
///////////////////
m = <Trans>Message</Trans>
m = (
<Trans id="custom.id" comment="comment" context="context">
Message
</Trans>
)
// @ts-expect-error: children are required here
m = <Trans id="custom.id" comment="comment" context="context" />
///////////////////
//// JSX Plural
///////////////////
m = (
// @ts-expect-error: children are not allowed
<Plural value={5} other={"..."}>
Message
</Plural>
)
// @ts-expect-error: value is required
m = <Plural />
m = <Plural value={5} offset={1} one={"..."} other={"..."} _0="" _1={"..."} />
// @ts-expect-error: offset could be number only
m = <Plural value={5} offset={"1"} one={"..."} other={"..."} />
// @ts-expect-error: not allowed prop is passed
m = <Plural value={5} unsupported={"should be error"} />
// should support JSX element as Props
m = <Plural value={5} one={<Trans>...</Trans>} other={<Trans>...</Trans>} />
// value as string
m = <Plural value={"5"} one={"..."} other={"..."} />
// @ts-expect-error: `other` should always be present
m = <Plural value={"5"} one={"..."} />
// additional properties
m = (
<Plural
value={5}
comment={"Comment"}
context={"Context"}
id={"custom.id"}
one={"..."}
other={"..."}
/>
)
///////////////////
//// JSX SelectOrdinal is the same s Plural, so just smoke test it
///////////////////
m = (
<SelectOrdinal
value={5}
offset={1}
one={"..."}
other={"..."}
_0=""
_1={"..."}
/>
)
///////////////////
//// JSX Select
///////////////////
m = (
// @ts-expect-error: children are not allowed here
<Select value={gender} other={"string"}>
Message
</Select>
)
// @ts-expect-error: `value` could be string only
m = <Select value={5} other={"string"} />
// @ts-expect-error: `other` required
m = <Select value={"male"} />
// @ts-expect-error: `value` required
m = <Select other={"male"} />
m = <Select value={gender} _male="..." _female="..." other={"..."} />
// @ts-expect-error: exact cases should be prefixed with underscore
m = <Select value={gender} male="..." female=".." other={"..."} />
// should support JSX in props
m = (
<Select
value={"male"}
_male={<Trans>...</Trans>}
other={<Trans>...</Trans>}
/>
)
////////////////////////
//// SolidJS useLingui()
////////////////////////
function MyComponent() {
const { t, i18n } = useLingui()
expectType<string>(t`Hello world`)
expectType<string>(t({ message: "my message" }))
// @ts-expect-error: you could not pass a custom instance here
t(i18n())({ message: "my message" })
expectType<I18n>(i18n())
}
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"esModuleInterop": true,
"skipLibCheck": true
},
"paths": {}
}
+137
View File
@@ -0,0 +1,137 @@
import { Component, JSX, ParentComponent } from 'solid-js'
import type { TransRenderCallbackOrComponent, I18nContext } from "@lingui-solid/solid"
import type { MacroMessageDescriptor } from "@lingui/core/macro"
type CommonProps = TransRenderCallbackOrComponent & {
id?: string
comment?: string
context?: string
}
type TransProps = {
children: JSX.Element
} & CommonProps
type PluralChoiceProps = {
value: string | number
/** Offset of value when calculating plural forms */
offset?: number
zero?: JSX.Element
one?: JSX.Element
two?: JSX.Element
few?: JSX.Element
many?: JSX.Element
/** Catch-all option */
other: JSX.Element
/** Exact match form, corresponds to =N rule */
[digit: `_${number}`]: JSX.Element
} & CommonProps
type SelectChoiceProps = {
value: string
/** Catch-all option */
other: JSX.Element
[option: `_${string}`]: JSX.Element
} & CommonProps
/**
* Trans is the basic macro for static messages,
* messages with variables, but also for messages with inline markup
*
* @example
* ```
* <Trans>Hello {username}. Read the <a href="/docs">docs</a>.</Trans>
* ```
* @example
* ```
* <Trans id="custom.id">Hello {username}.</Trans>
* ```
*/
export const Trans: ParentComponent<TransProps>
/**
* Props of Plural macro are transformed into plural format.
*
* @example
* ```
* import { Plural } from "@lingui/core/macro"
* <Plural value={numBooks} one="Book" other="Books" />
*
* // ↓ ↓ ↓ ↓ ↓ ↓
* import { Trans } from "@lingui-solid/solid"
* <Trans id="{numBooks, plural, one {Book} other {Books}}" values={{ numBooks }} />
* ```
*/
export const Plural: Component<PluralChoiceProps>
/**
* Props of SelectOrdinal macro are transformed into selectOrdinal format.
*
* @example
* ```
* // count == 1 -> 1st
* // count == 2 -> 2nd
* // count == 3 -> 3rd
* // count == 4 -> 4th
* <SelectOrdinal
* value={count}
* one="#st"
* two="#nd"
* few="#rd"
* other="#th"
* />
* ```
*/
export const SelectOrdinal: Component<PluralChoiceProps>
/**
* Props of Select macro are transformed into select format
*
* @example
* ```
* // gender == "female" -> Her book
* // gender == "male" -> His book
* // gender == "non-binary" -> Their book
*
* <Select
* value={gender}
* _male="His book"
* _female="Her book"
* other="Their book"
* />
* ```
*/
export const Select: Component<SelectChoiceProps>
declare function _t(descriptor: MacroMessageDescriptor): string
declare function _t(
literals: TemplateStringsArray,
...placeholders: any[]
): string
/**
*
* Macro version of useLingui replaces _ function with `t` macro function which is bound to i18n passed from context
*
* Returned `t` macro function has all the same signatures as global `t`
*
* @example
* ```
* const { t } = useLingui();
* const message = t`Text`;
* ```
*
* @example
* ```
* const { i18n, t } = useLingui();
* const locale = i18n.locale;
* const message = t({
* id: "msg.hello",
* comment: "Greetings at the homepage",
* message: `Hello ${name}`,
* });
* ```
*/
export function useLingui(): Omit<I18nContext, "_"> & {
t: typeof _t
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("@lingui-solid/babel-plugin-lingui-macro/macro")
+7
View File
@@ -0,0 +1,7 @@
import macro from "@lingui-solid/solid/macro"
describe("solid-macro", () => {
it("Should re-export Macro", () => {
expect((macro as any).isBabelMacro).toBeTruthy()
})
})
+97
View File
@@ -0,0 +1,97 @@
{
"name": "@lingui-solid/solid",
"version": "5.0.0",
"sideEffects": false,
"description": "SolidJS components for translations",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"author": {
"name": "Tomáš Ehrlich",
"email": "tomas.ehrlich@gmail.com"
},
"license": "MIT",
"keywords": [
"solidjs",
"component",
"i18n",
"internationalization",
"i9n",
"translation",
"icu",
"messageformat",
"multilingual",
"localization",
"l10n"
],
"scripts": {
"build": "rimraf ./dist && vite build",
"stub": "rimraf ./dist && vite build"
},
"repository": {
"type": "git",
"url": "https://github.com/lingui/js-lingui.git"
},
"bugs": {
"url": "https://github.com/lingui/js-lingui/issues"
},
"engines": {
"node": ">=16.0.0"
},
"exports": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
},
"./macro": {
"types": "./macro/index.d.ts",
"default": "./macro/index.js"
},
"./package.json": "./package.json"
},
"files": [
"LICENSE",
"README.md",
"dist/",
"macro/index.d.ts",
"macro/index.js"
],
"peerDependencies": {
"@lingui-solid/babel-plugin-lingui-macro": "5.0.0",
"babel-plugin-macros": "2 || 3",
"solid-js": "^1.6.0"
},
"peerDependenciesMeta": {
"@lingui-solid/babel-plugin-lingui-macro": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@lingui-solid/solid": "5.0.0"
},
"devDependencies": {
"@lingui/core": "^5.1.2",
"@lingui/jest-mocks": "*",
"@lingui/message-utils": "^5.1.2",
"@rollup/plugin-babel": "^6.0.4",
"@solidjs/testing-library": "^0.8.10",
"babel-preset-solid": "^1.9.3",
"eslint-plugin-solid": "0.13.2",
"rimraf": "3.0.2",
"solid-js": "^1.9.3",
"tsd": "0.26.1",
"vite": "^6.0.1",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-solid": "^2.11.0"
}
}
+223
View File
@@ -0,0 +1,223 @@
import { createEffect, createMemo, JSX, on } from "solid-js"
import { render } from "@solidjs/testing-library"
import { I18nProvider, useLingui } from "./I18nProvider"
import { I18n, setupI18n } from "@lingui/core"
describe("I18nProvider", () => {
it(
"should pass i18n context to wrapped components, " +
"and re-render components that consume the context through useLingui()",
() => {
const i18n = setupI18n({
locale: "en",
messages: {
en: {},
cs: {},
},
})
let staticRenderCount = 0,
dynamicRenderCount = 0
const WithoutLinguiHook = (
props: JSX.HTMLAttributes<HTMLDivElement> & { i18n: I18n }
) => {
staticRenderCount++
return <div {...props}>{props.i18n.locale}</div>
}
const WithLinguiHook = (props: JSX.HTMLAttributes<HTMLDivElement>) => {
const { i18n } = useLingui()
createEffect(on(i18n, () => {
dynamicRenderCount++
}))
return <div {...props}>{i18n().locale}</div>
}
const { getByTestId } = render(() =>
<I18nProvider i18n={i18n}>
<WithoutLinguiHook i18n={i18n} data-testid="static" />
<WithLinguiHook data-testid="dynamic" />
</I18nProvider>
)
i18n.activate("cs")
expect(getByTestId("static").textContent).toEqual("en")
expect(getByTestId("dynamic").textContent).toEqual("cs")
i18n.activate("en")
expect(getByTestId("static").textContent).toEqual("en")
expect(getByTestId("dynamic").textContent).toEqual("en")
expect(staticRenderCount).toEqual(1)
expect(dynamicRenderCount).toEqual(3) // initial, cs, en
}
)
it("should subscribe for locale changes upon mount", () => {
const i18n = setupI18n({
locale: "cs",
messages: {
cs: {},
},
})
i18n.on = jest.fn(() => jest.fn())
expect(i18n.on).not.toBeCalled()
render(() =>
<I18nProvider i18n={i18n}>
<div />
</I18nProvider>
)
expect(i18n.on).toBeCalledWith("change", expect.any(Function))
})
it("should unsubscribe for locale changes on unmount", () => {
const unsubscribe = jest.fn()
const i18n = setupI18n({
locale: "cs",
messages: {
cs: {},
},
})
i18n.on = jest.fn(() => unsubscribe)
const { unmount } = render(() =>
<I18nProvider i18n={i18n}>
<div />
</I18nProvider>
)
expect(unsubscribe).not.toBeCalled()
unmount()
expect(unsubscribe).toBeCalled()
})
it("I18nProvider renders `null` until locale is activated. Children are rendered after activation.", () => {
expect.assertions(3)
const i18n = setupI18n()
const CurrentLocaleStatic = () => {
return <span data-testid="static">1_{i18n.locale}</span>
}
const CurrentLocaleContextConsumer = () => {
const { i18n } = useLingui()
return <span data-testid="dynamic">2_{i18n().locale}</span>
}
const { container } = render(() =>
<I18nProvider i18n={i18n}>
<CurrentLocaleStatic />
<CurrentLocaleContextConsumer />
</I18nProvider>
)
// First render — locale isn't activated
expect(container.textContent).toEqual("")
i18n.load("cs", {})
// Catalog is loaded, but locale still isn't activated.
expect(container.textContent).toEqual("")
i18n.activate("cs")
// After loading and activating locale, components are rendered for the first time
expect(container.textContent).toEqual("1_cs2_cs")
})
it(
"given 'en' locale, if activate('cs') call happens before i18n.on-change subscription in useEffect(), " +
"I18nProvider detects that it's stale and re-renders with the 'cs' locale value",
() => {
const i18n = setupI18n({
locale: "en",
messages: { en: {} },
})
let renderCount = 0
const CurrentLocaleContextConsumer = () => {
const { i18n } = useLingui()
createEffect(on(i18n, () => renderCount++))
return <span data-testid="child">{i18n().locale}</span>
}
/**
* Note that we're doing exactly what the description says:
* but to simulate the equivalent situation, we pass our own mock subscriber
* to i18n.on("change", ...) and in it we call i18n.activate("cs") ourselves
* so that the condition in useEffect() is met and the component re-renders
* */
const mockSubscriber = jest.fn(() => {
i18n.load("cs", {})
i18n.activate("cs")
return () => {
// unsubscriber - noop to make TS happy
}
})
jest.spyOn(i18n, "on").mockImplementation(mockSubscriber)
const { getByTestId } = render(() =>
<I18nProvider i18n={i18n}>
<CurrentLocaleContextConsumer />
</I18nProvider>
)
expect(mockSubscriber).toHaveBeenCalledWith(
"change",
expect.any(Function)
)
expect(getByTestId("child").textContent).toBe("cs")
expect(renderCount).toBe(1)
}
)
it("should render children", () => {
const i18n = setupI18n({
locale: "en",
messages: { en: {} },
})
const child = <div data-testid="child" />
const { getByTestId } = render(() =>
<I18nProvider i18n={i18n}>{child}</I18nProvider>
)
expect(getByTestId("child")).toBeTruthy()
})
it("using the _ function from useLingui renders fresh translations even when memoized", () => {
const greetingId = "greeting"
const i18n = setupI18n({
locale: "en",
messages: {
en: {
[greetingId]: "Hello World",
},
cs: {
[greetingId]: "Ahoj světe",
},
},
})
const ComponentWithMemo = () => {
const { _ } = useLingui()
const message = createMemo(() => _(greetingId))
return <div>{message()}</div>
}
const { getByText } = render(() =>
<I18nProvider i18n={i18n}>
<ComponentWithMemo />
</I18nProvider>
)
expect(getByText("Hello World")).toBeTruthy()
i18n.activate("cs")
expect(getByText("Ahoj světe")).toBeTruthy()
})
})
+89
View File
@@ -0,0 +1,89 @@
import type { I18n } from "@lingui/core"
import { createContext, Accessor, createSignal, onCleanup, on, ParentComponent, useContext, createMemo, Show, createEffect } from "solid-js"
import type { TransRenderProps } from "./TransNoContext"
export type I18nContext = {
i18n: Accessor<I18n>
_: I18n["_"]
defaultComponent: Accessor<ParentComponent<TransRenderProps> | undefined>
}
export type I18nProviderProps = {
i18n: I18n
defaultComponent?: ParentComponent<TransRenderProps>
}
export const LinguiContext = createContext<I18nContext | null>(null)
export const useLinguiInternal = (devErrorMessage?: string): I18nContext => {
const context = useContext(LinguiContext)
if (process.env.NODE_ENV !== "production") {
if (context == null) {
throw new Error(
devErrorMessage ?? "useLingui hook was used without I18nProvider."
)
}
}
return context as I18nContext
}
export function useLingui(): I18nContext {
return useLinguiInternal()
}
export const I18nProvider: ParentComponent<I18nProviderProps> = (props) => {
// eslint-disable-next-line solid/reactivity
const [latestKnownLocale, setLatestKnownLocale] = createSignal<string | undefined>(props.i18n.locale)
let unsubscribe: () => void
// eslint-disable-next-line solid/reactivity
const [i18n, setI18n] = createSignal(props.i18n, { equals: (_, next) => next.locale != latestKnownLocale() })
const defaultComponent = createMemo(() => props.defaultComponent)
const _: I18n["_"] = ((...args: any[]) => (i18n()._ as Function).apply(i18n(), args))
/**
* Subscribe for locale/message changes
*
* I18n object from `@lingui/core` is the single source of truth for all i18n related
* data (active locale, catalogs). When new messages are loaded or locale is changed
* we need to trigger re-rendering of LinguiContext.Consumers.
*/
createEffect(on([() => props.i18n, () => props.defaultComponent], () => {
const updateContext = () => {
setLatestKnownLocale(props.i18n.locale)
setI18n(props.i18n)
}
if (unsubscribe)
unsubscribe()
unsubscribe = props.i18n.on("change", updateContext)
/**
* unlikely, but if the locale changes before the onChange listener
* was added, we need to trigger a rerender
* */
if (latestKnownLocale() !== props.i18n.locale) {
updateContext()
}
}))
onCleanup(() => unsubscribe())
createEffect(() => {
if (!latestKnownLocale()) {
process.env.NODE_ENV === "development" &&
console.log(
"I18nProvider rendered `<></>`. A call to `i18n.activate` needs to happen in order for translations to be activated and for the I18nProvider to render." +
"This is not an error but an informational message logged only in development."
)
}
})
return (
<LinguiContext.Provider value={{ i18n, defaultComponent, _ }}>
<Show when={!!latestKnownLocale()}>
{props.children}
</Show>
</LinguiContext.Provider>
)
}
+470
View File
@@ -0,0 +1,470 @@
import { render } from "@solidjs/testing-library"
import { createSignal, JSX, ParentComponent } from "solid-js"
import {
Trans,
I18nProvider,
TransRenderProps,
TransRenderCallbackOrComponent,
I18nContext,
} from "@lingui-solid/solid"
import { setupI18n } from "@lingui/core"
import { mockConsole } from "@lingui/jest-mocks"
import { TransNoContext } from "./TransNoContext"
describe("Trans component", () => {
/*
* Setup context, define helpers
*/
const i18n = setupI18n({
locale: "cs",
messages: {
cs: {
"All human beings are born free and equal in dignity and rights.":
"Všichni lidé rodí se svobodní a sobě rovní co do důstojnosti a práv.",
"My name is {name}": "Jmenuji se {name}",
Original: "Původní",
Updated: "Aktualizovaný",
"msg.currency": "{value, number, currency}",
ID: "Translation",
},
},
})
const renderWithI18n = (node: () => JSX.Element) =>
render(() => <I18nProvider i18n={i18n}>{node()}</I18nProvider>)
const text = (node: () => JSX.Element) =>
renderWithI18n(node).container.textContent
const html = (node: () => JSX.Element) =>
renderWithI18n(node).container.innerHTML
/*
* Tests
*/
describe("should log console.error", () => {
const renderProp: ParentComponent<TransRenderProps> = (props) => (
<span>render_{props.children}</span>
)
const component: ParentComponent<TransRenderProps> = (props) => (
<span>component_{props.children}</span>
)
test.each<{
description: string
props: any
expectedLog: string
expectedTextContent: string
}>([
{
description:
"both `render` and `component` are used, and return `render`",
props: {
render: renderProp,
component,
},
expectedLog:
"You can't use both `component` and `render` prop at the same time.",
expectedTextContent: "render_Some text",
},
{
description:
"`render` is not of type function, and return `defaultComponent`",
props: {
render: "invalid",
},
expectedLog:
"Invalid value supplied to prop `render`. It must be a function, provided invalid",
expectedTextContent: "default_Some text",
},
{
description: "`component` is not of type function, and return ",
props: {
component: "invalid",
},
expectedLog:
"Invalid value supplied to prop `component`. It must be a SolidJS component, provided invalid",
expectedTextContent: "default_Some text",
},
])("when $description", ({ expectedLog, props, expectedTextContent }) => {
mockConsole((console) => {
const { container } = render(() =>
<I18nProvider
i18n={i18n}
defaultComponent={(props) => {
return <>default_{props.translation}</>
}}
>
<Trans {...props} id="Some text" />
</I18nProvider>
)
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(expectedLog)
)
expect(container.textContent).toBe(expectedTextContent)
})
})
it("when there's no i18n context available", () => {
const originalConsole = console.error
console.error = jest.fn()
expect(() => render(() => <Trans id="unknown" />))
.toThrowErrorMatchingInlineSnapshot(`
"Trans component was rendered without I18nProvider.
Attempted to render message: undefined id: unknown. Make sure this component is rendered inside a I18nProvider."
`)
expect(() =>
render(() => <Trans id="unknown" message={"some valid message"} />)
).toThrowErrorMatchingInlineSnapshot(`
"Trans component was rendered without I18nProvider.
Attempted to render message: some valid message id: unknown. Make sure this component is rendered inside a I18nProvider."
`)
console.error = originalConsole
})
it("when deprecated string built-ins are used", () => {
const originalConsole = console.error
console.error = jest.fn()
// @ts-expect-error testing the error
renderWithI18n(() => <Trans render="span" id="Some text" />)
expect(console.error).toHaveBeenCalled()
// @ts-expect-error testing the error
renderWithI18n(() => <Trans render="span" id="Some text" />)
expect(console.error).toHaveBeenCalledTimes(2)
console.error = originalConsole
})
})
it("should follow jsx semantics regarding booleans", () => {
expect(
html(() =>
<Trans
id="unknown"
message={"foo <0>{0}</0> bar"}
values={{
0: false && "lol",
}}
components={{
0: <span />,
}}
/>
)
).toEqual("foo <span></span> bar")
expect(
html(() =>
<Trans
id="unknown"
message={"foo <0>{0}</0> bar"}
values={{
0: "lol",
}}
components={{
0: <span />,
}}
/>
)
).toEqual("foo <span>lol</span> bar")
})
it("should render default string", () => {
expect(text(() => <Trans id="unknown" />)).toEqual("unknown")
expect(text(() => <Trans id="unknown" message="Not translated yet" />)).toEqual(
"Not translated yet"
)
expect(
text(() =>
<Trans
id="unknown"
message="Not translated yet, {name}"
values={{ name: "Dave" }}
/>
)
).toEqual("Not translated yet, Dave")
})
it("should render translation", () => {
const translation = text(() =>
<Trans id="All human beings are born free and equal in dignity and rights." />
)
expect(translation).toEqual(
"Všichni lidé rodí se svobodní a sobě rovní co do důstojnosti a práv."
)
})
it("should render translation from variable", () => {
const msg =
"All human beings are born free and equal in dignity and rights."
const translation = text(() => <Trans id={msg} />)
expect(translation).toEqual(
"Všichni lidé rodí se svobodní a sobě rovní co do důstojnosti a práv."
)
})
it("should render component in variables", () => {
const translation = html(() =>
<Trans id="Hello {name}" values={{ name: <strong>John</strong> }} />
)
expect(translation).toEqual("Hello <strong>John</strong>")
})
it("should render array of components in variables", () => {
const translation = html(() =>
<Trans
id="Hello {name}"
values={{
name: [<strong>John</strong>, <strong>!</strong>],
}}
/>
)
expect(translation).toEqual("Hello <strong>John</strong><strong>!</strong>")
})
it("should render named component in components", () => {
const translation = html(() =>
<Trans
id="Read <named>the docs</named>"
components={{ named: <a href="/docs" /> }}
/>
)
expect(translation).toEqual(`Read <a href="/docs">the docs</a>`)
})
it("should render nested named components in components", () => {
const translation = html(() =>
<Trans
id="Read <link>the <strong>docs</strong></link>"
components={{ link: <a href="/docs" />, strong: <strong /> }}
/>
)
expect(translation).toEqual(
`Read <a href="/docs">the <strong>docs</strong></a>`
)
})
it("should render components and array components with variable", () => {
const translation = html(() =>
<Trans
id="Read <link>the <strong>docs</strong></link>, {name}"
components={{ link: <a href="/docs" />, strong: <strong /> }}
values={{
name: [<strong>John</strong>, <strong>!</strong>],
}}
/>
)
expect(translation).toEqual(
`Read <a href="/docs">the <strong>docs</strong></a>, <strong>John</strong><strong>!</strong>`
)
})
it("should render non-named component in components", () => {
const translation = html(() =>
<Trans id="Read <0>the docs</0>" components={{ 0: <a href="/docs" /> }} />
)
expect(translation).toEqual(`Read <a href="/docs">the docs</a>`)
})
it("should render translation inside custom component", () => {
const Component: ParentComponent = (props) => (
<p class="lead">{props.children}</p>
)
const html1 = html(() => <Trans component={Component} id="Original" />)
const html2 = html(() =>
<Trans
render={(props) => <p class="lead">{props.translation}</p>}
id="Original"
/>
)
expect(html1).toEqual('<p class="lead">Původní</p>')
expect(html2).toEqual('<p class="lead">Původní</p>')
})
it("should render custom format", () => {
const translation = text(() =>
<Trans
id="msg.currency"
values={{ value: 1 }}
formats={{
currency: {
style: "currency",
currency: "EUR",
minimumFractionDigits: 2,
},
}}
/>
)
expect(translation).toEqual("1,00 €")
})
it("should render plural", () => {
const render = (count: number) =>
html(() =>
<Trans
id={"tYX0sm"}
message={
"{count, plural, =0 {Zero items} one {# item} other {# <0>A lot of them</0>}}"
}
values={{
count,
}}
components={{
0: <a href="/more" />,
}}
/>
)
expect(render(0)).toEqual("Zero items")
expect(render(1)).toEqual("1 item")
expect(render(2)).toEqual(`2 <a href="/more">A lot of them</a>`)
})
describe("rendering", () => {
it("should render a text node with no wrapper element", () => {
const txt = html(() => <Trans id="Some text" />)
expect(txt).toEqual("Some text")
})
it("should render custom element", () => {
const element = html(() =>
<Trans
render={(props) => <h1 id={props.id}>{props.translation}</h1>}
id="Headline"
/>
)
expect(element).toEqual(`<h1 id="Headline">Headline</h1>`)
})
it("supports render callback function", () => {
const spy = jest.fn()
text(() =>
<Trans
id="ID"
message="Default"
render={(props) => {
spy(props)
return <></>
}}
/>
)
expect(spy).toHaveBeenCalledWith({
id: "ID",
message: "Default",
translation: "Translation",
children: "Translation",
})
})
it("should take defaultComponent prop with a custom component", () => {
const ComponentFC: ParentComponent<TransRenderProps> = (
props
) => {
return <div>{props.children}</div>
}
const span = render(() =>
<I18nProvider i18n={i18n} defaultComponent={ComponentFC}>
<Trans id="Some text" />
</I18nProvider>
).container.innerHTML
expect(span).toEqual(`<div>Some text</div>`)
})
test.each<TransRenderCallbackOrComponent>([
{ component: null },
{ render: null },
])(
"should ignore defaultComponent when `component` or `render` is null",
(props) => {
const ComponentFC: ParentComponent<TransRenderProps> = (
props
) => {
return <div>{props.children}</div>
}
const translation = render(() =>
<I18nProvider i18n={i18n} defaultComponent={ComponentFC}>
<Trans id="Some text" {...props} />
</I18nProvider>
).container.innerHTML
expect(translation).toEqual("Some text")
}
)
})
describe("component prop rendering", () => {
it("should render function component as simple prop", () => {
const propsSpy = jest.fn()
const ComponentFC: ParentComponent<TransRenderProps> = (
props
) => {
propsSpy(props)
const [state] = createSignal("value")
return <div id={props.id}>{state()}</div>
}
const element = html(() => <Trans component={ComponentFC} id="Headline" />)
expect(element).toEqual(`<div id="Headline">value</div>`)
expect(propsSpy).toHaveBeenCalledWith({
id: "Headline",
message: undefined,
translation: "Headline",
children: "Headline",
})
})
})
describe("I18nProvider defaultComponent accepts render-like props", () => {
const DefaultComponent: ParentComponent<TransRenderProps> = (
props
) => (
<>
<div data-testid="children">{props.children}</div>
{props.id && <div data-testid="id">{props.id}</div>}
{props.message && <div data-testid="message">{props.message}</div>}
{props.translation && (
<div data-testid="translation">{props.translation}</div>
)}
</>
)
it("should render defaultComponent with Trans props", () => {
const markup = render(() =>
<I18nProvider i18n={i18n} defaultComponent={DefaultComponent}>
<Trans id="ID" message="Some message" />
</I18nProvider>
)
expect(markup.queryByTestId("id")?.innerHTML).toEqual("ID")
expect(markup.queryByTestId("message")?.innerHTML).toEqual("Some message")
expect(markup.queryByTestId("translation")?.innerHTML).toEqual(
"Translation"
)
})
describe("TransNoContext", () => {
it("Should render without provider/context", () => {
const lingui: I18nContext = {
i18n: () => i18n,
_: i18n._,
defaultComponent: () => undefined
}
const translation = render(() =>
<TransNoContext
id="All human beings are born free and equal in dignity and rights."
lingui={lingui}
/>
).container.textContent
expect(translation).toEqual(
"Všichni lidé rodí se svobodní a sobě rovní co do důstojnosti a práv."
)
})
})
})
})
+13
View File
@@ -0,0 +1,13 @@
import { ParentComponent } from "solid-js"
import { useLinguiInternal } from "./I18nProvider"
import { TransNoContext, type TransProps } from "./TransNoContext"
export const Trans: ParentComponent<TransProps> = (props) => {
let errMessage = undefined
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line solid/reactivity
errMessage = `Trans component was rendered without I18nProvider.\nAttempted to render message: ${props.message} id: ${props.id}. Make sure this component is rendered inside a I18nProvider.`
}
const lingui = useLinguiInternal(errMessage)
return <TransNoContext {...props} lingui={lingui}>{props.children}</TransNoContext>
}
+139
View File
@@ -0,0 +1,139 @@
import { createMemo, JSX, ParentComponent } from "solid-js"
import { formatElements } from "./format"
import type { MessageOptions } from "@lingui/core"
import { Dynamic } from "solid-js/web"
import { I18nContext } from "./I18nProvider"
export type TransRenderProps = {
id: string
translation: JSX.Element
children: JSX.Element
message?: string | null
}
export type TransRenderCallbackOrComponent =
| {
component?: undefined
render?:
| ((props: TransRenderProps) => JSX.Element)
| null
}
| {
component?: ParentComponent<TransRenderProps> | null
render?: undefined
}
export type TransProps = {
id: string
message?: string
values?: Record<string, unknown>
components?: { [key: string]: JSX.Element | any }
formats?: MessageOptions["formats"]
comment?: string
children?: JSX.Element
} & TransRenderCallbackOrComponent
/**
* Version of `<Trans>` component without using a Provider/Context SolidJS feature.
*
* @experimental the api of this component is not stabilized yet.
*/
export const TransNoContext: ParentComponent<TransProps & {
lingui: I18nContext
}> = (props) => {
const translatedContent = createMemo(() => {
const values = { ...props.values ?? {} }
const components = { ...props.components ?? {} }
if (props.values) {
/*
Related discussion: https://github.com/lingui/js-lingui/issues/183
Values *might* contain html elements with static content.
They're replaced with <INDEX /> placeholders and added to `components`.
Example:
Translation: 'Hello {name}'
Values: { name: <strong>Jane</strong> }
It'll become "Hello <0 />" with components=[<strong>Jane</strong>]
*/
Object.keys(props.values).forEach((key) => {
const index = Object.keys(components).length
// simple scalars should be processed as values to be able to apply formatting
if (typeof values[key] === "string" || typeof values[key] === "number") {
return
}
// falsy values should be empty string
if (!values[key]) {
values[key] = ""
return
}
// html nodes, arrays
components[index] = values[key]
values[key] = `<${index}/>`
})
}
const _translation: string =
props.lingui.i18n != null && typeof props.lingui.i18n()._ === "function"
? props.lingui.i18n()._(props.id, values, { message: props.message, formats: props.formats })
: props.id // i18n provider isn't loaded at all
const translation = _translation ? formatElements(_translation, components) : null
if (props.render === null || props.component === null) {
// Although `string` is a valid SolidJS element, types only allow `Element`
// Upstream issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544
return translation as unknown as JSX.Element
}
const FallbackComponent: ParentComponent<TransRenderProps> =
props.lingui.defaultComponent() || RenderFragment
const i18nProps: TransRenderProps = {
id: props.id,
message: props.message,
translation,
children: translation, // for type-compatibility with `component` prop
}
// Validation of `render` and `component` props
if (props.render && props.component) {
console.error(
"You can't use both `component` and `render` prop at the same time. `component` is ignored."
)
} else if (props.render && typeof props.render !== "function") {
console.error(
`Invalid value supplied to prop \`render\`. It must be a function, provided ${props.render}`
)
} else if (props.component && typeof props.component !== "function") {
console.error(
`Invalid value supplied to prop \`component\`. It must be a SolidJS component, provided ${props.component}`
)
return <Dynamic component={FallbackComponent} {...i18nProps}>{translation}</Dynamic>
}
// Rendering using a render prop
if (typeof props.render === "function") {
// Component: render={(props) => <a title={props.translation}>x</a>}
return props.render(i18nProps)
}
// `component` prop has a higher precedence over `defaultComponent`
const Component: ParentComponent<TransRenderProps> =
props.component || FallbackComponent
return <Dynamic component={Component} {...i18nProps}>{translation}</Dynamic>
})
return <>{translatedContent()}</>
}
const RenderFragment: ParentComponent<TransRenderProps> = (props) => {
// cannot use <></> directly because we're passing in props that it doesn't support
return <>{props.children}</>
}
+134
View File
@@ -0,0 +1,134 @@
import { JSX } from "solid-js"
import { render } from "@solidjs/testing-library"
import { formatElements } from "./format"
// eslint-disable-next-line import/no-extraneous-dependencies
import { mockConsole } from "@lingui/jest-mocks"
describe("formatElements", function () {
const html = (elements: () => JSX.Element) =>
render(() => <>{elements()}</>).container.innerHTML
it("should return string when there are no elements", function () {
expect(formatElements("")).toEqual("")
expect(formatElements("Text only")).toEqual("Text only")
})
it("should format unpaired elements", function () {
expect(html(() => formatElements("<0/>", { 0: <br /> }))).toEqual("<br>")
})
it("should format paired elements", function () {
expect(html(() => formatElements("<0>Inner</0>", { 0: <strong /> }))).toEqual(
"<strong>Inner</strong>"
)
expect(
html(() => formatElements("Before <0>Inner</0> After", { 0: <strong /> }))
).toEqual("Before <strong>Inner</strong> After")
})
it("should preserve element props", function () {
expect(
html(() => formatElements("<0>About</0>", { 0: <a href="/about" /> }))
).toEqual('<a href="/about">About</a>')
})
it("should preserve named element props", function () {
expect(
html(() =>
formatElements("<named>About</named>", { named: <a href="/about" /> })
)
).toEqual('<a href="/about">About</a>')
})
it("should preserve nested named element props", function () {
expect(
html(() =>
formatElements("<named>About <b>us</b></named>", {
named: <a href="/about" />,
b: <strong />,
})
)
).toEqual('<a href="/about">About <strong>us</strong></a>')
})
it("should format nested elements", function () {
expect(
html(() =>
formatElements("<0><1>Deep</1></0>", {
0: <a href="/about" />,
1: <strong />,
})
)
).toEqual('<a href="/about"><strong>Deep</strong></a>')
expect(
html(() =>
formatElements(
"Before \n<0>Inside <1>\nNested</1>\n Between <2/> After</0>",
{ 0: <a href="/about" />, 1: <strong />, 2: <br /> }
)
)
).toEqual(
'Before <a href="/about">Inside <strong>Nested</strong> Between <br> After</a>'
)
})
it("should ignore non existing element", function () {
mockConsole((console) => {
expect(html(() => formatElements("<0>First</0>"))).toEqual("First")
expect(html(() => formatElements("<0>First</0>Second"))).toEqual("FirstSecond")
expect(html(() => formatElements("First<0>Second</0>Third"))).toEqual(
"FirstSecondThird"
)
expect(html(() => formatElements("Fir<0/>st"))).toEqual("First")
expect(html(() => formatElements("<tag>text</tag>"))).toEqual("text")
expect(html(() => formatElements("text <br/>"))).toEqual("text ")
expect(console.warn).not.toBeCalled()
expect(console.error).toBeCalledTimes(6)
})
})
it("should ignore incorrect tags and print them as a text", function () {
mockConsole((console) => {
expect(html(() => formatElements("text</0>"))).toEqual("text&lt;/0&gt;")
expect(html(() => formatElements("text<0 />"))).toEqual("text&lt;0 /&gt;")
expect(console.warn).not.toBeCalled()
expect(console.error).not.toBeCalled()
})
})
it("should ignore unpaired element used as paired", function () {
mockConsole((console) => {
expect(html(() => formatElements("<0>text</0>", { 0: <br /> }))).toEqual("text")
expect(console.warn).not.toBeCalled()
expect(console.error).toBeCalled()
})
})
it("should ignore unpaired named element used as paired", function () {
mockConsole((console) => {
expect(
html(() => formatElements("<named>text</named>", { named: <br /> }))
).toEqual("text")
expect(console.warn).not.toBeCalled()
expect(console.error).toBeCalledTimes(1)
})
})
it("should ignore paired element used as unpaired", function () {
expect(html(() => formatElements("text<0/>", { 0: <span /> }))).toEqual(
"text<span></span>"
)
})
it("should ignore paired named element used as unpaired", function () {
expect(html(() => formatElements("text<named/>", { named: <span /> }))).toEqual(
"text<span></span>"
)
})
})
+121
View File
@@ -0,0 +1,121 @@
import { JSX } from "solid-js"
// match <tag>paired</tag> and <tag/> unpaired tags
const tagRe = /<([a-zA-Z0-9]+)>(.*?)<\/\1>|<([a-zA-Z0-9]+)\/>/
const nlRe = /(?:\r\n|\r|\n)/g
// For HTML, certain tags should omit their close tag. We keep a whitelist for
// those special-case tags.
const voidElementTags = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
keygen: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
menuitem: true,
}
function appendChildren(parent: HTMLElement, children: JSX.Element) {
if (children == null)
return
if (Array.isArray(children)) {
for (const node of (children as JSX.ArrayElement)) {
appendChildren(parent, node)
}
} else if (children instanceof Node) {
parent.appendChild(children as Node)
} else if (typeof children === "string") {
parent.appendChild(document.createTextNode(children))
} else {
console.error(`Invalid children type: ${typeof children}`, children)
}
}
/**
* `formatElements` - parse string and return tree of html elements
*
* `value` is string to be formatted with <tag>Paired<tag/> or <tag/> (unpaired)
* placeholders. `elements` is a array of html elements which indexes
* correspond to element indexes in formatted string
*/
function formatElements(
value: string,
elements: { [key: string]: JSX.Element } = {}
): JSX.Element {
const parts = value.replace(nlRe, "").split(tagRe)
// no inline elements, return
if (parts.length === 1) return value
const tree: Array<JSX.Element | string> = []
const before = parts.shift()
if (before) tree.push(before)
for (const [index, children, after] of getElements(parts)) {
let element = typeof index !== "undefined" ? elements[index] : undefined
if (!element) {
console.error(`Can't use element at index '${index}' as it is not declared in the original translation`)
// ignore problematic element but push its children and elements after it
element = <></>
} else if (children && (element instanceof Node) && (element as Node).nodeName.toLowerCase() in voidElementTags) {
console.error(`${(element as Node).nodeName} is a void element tag therefore it must have no children`)
// ignore problematic element but push its children and elements after it
element = <></>
}
if (children) {
if (element instanceof HTMLElement) {
appendChildren(element as HTMLElement, formatElements(children, elements))
} else {
if (!Array.isArray(element))
console.error(`Element is not HTMLElement, but have children:`, element)
element = <>{element}{formatElements(children, elements)}</>
}
}
tree.push(element)
if (after) tree.push(after)
}
return tree.length === 1 ? tree[0]! : tree
}
/*
* `getElements` - return array of element indices and element children
*
* `parts` is array of [pairedIndex, children, unpairedIndex, textAfter, ...]
* where:
* - `pairedIndex` is index of paired element (undef for unpaired)
* - `children` are children of paired element (undef for unpaired)
* - `unpairedIndex` is index of unpaired element (undef for paired)
* - `textAfter` is string after all elements (empty string, if there's nothing)
*
* `parts` length is always a multiple of 4
*
* Returns: Array<[elementIndex, children, after]>
*/
function getElements(
parts: string[]
): Array<readonly [string | undefined, string, string | undefined]> {
if (!parts.length) return []
const [paired, children, unpaired, after] = parts.slice(0, 4)
const triple = [paired || unpaired, children || "", after] as const
return [triple].concat(getElements(parts.slice(4, parts.length)))
}
export { formatElements }
+11
View File
@@ -0,0 +1,11 @@
export { I18nProvider, useLingui, LinguiContext } from "./I18nProvider"
export type { I18nProviderProps, I18nContext } from "./I18nProvider"
export { Trans } from "./Trans"
export type {
TransProps,
TransRenderProps,
TransRenderCallbackOrComponent,
} from "./TransNoContext"
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"exclude": ["dist", "macro/__typetests__"],
"compilerOptions": {
"strict": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true
}
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import solidPlugin from 'vite-plugin-solid'
import dts from "vite-plugin-dts"
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
fileName: 'index'
},
rollupOptions: {
external: ["solid-js", "@lingui/core", "@lingui-solid/solid"],
}
},
plugins: [solidPlugin(), dts({ exclude: "**/*.test.{ts,tsx}", tsconfigPath: './tsconfig.json', rollupTypes: true })],
});
+150
View File
@@ -0,0 +1,150 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.1](https://github.com/lingui/js-lingui/compare/v5.1.1...v5.1.2) (2024-12-16)
**Note:** Version bump only for package @lingui/vite-plugin
## [5.1.1](https://github.com/lingui/js-lingui/compare/v5.1.0...v5.1.1) (2024-12-16)
**Note:** Version bump only for package @lingui/vite-plugin
# [5.1.0](https://github.com/lingui/js-lingui/compare/v5.0.0...v5.1.0) (2024-12-06)
### Features
- **vite-plugin:** add support for vite@6 ([#2108](https://github.com/lingui/js-lingui/issues/2108)) ([38a0c6f](https://github.com/lingui/js-lingui/commit/38a0c6f8b7f4d961f1580228310f4ebe959eb5a5))
## [4.14.1](https://github.com/lingui/js-lingui/compare/v4.14.0...v4.14.1) (2024-11-28)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.14.0](https://github.com/lingui/js-lingui/compare/v4.13.0...v4.14.0) (2024-11-07)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.13.0](https://github.com/lingui/js-lingui/compare/v4.12.0...v4.13.0) (2024-10-15)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.12.0](https://github.com/lingui/js-lingui/compare/v4.11.4...v4.12.0) (2024-10-11)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.11.4](https://github.com/lingui/js-lingui/compare/v4.11.3...v4.11.4) (2024-09-02)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.11.3](https://github.com/lingui/js-lingui/compare/v4.11.2...v4.11.3) (2024-08-09)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.11.2](https://github.com/lingui/js-lingui/compare/v4.11.1...v4.11.2) (2024-07-03)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.11.1](https://github.com/lingui/js-lingui/compare/v4.11.0...v4.11.1) (2024-05-30)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.11.0](https://github.com/lingui/js-lingui/compare/v4.10.1...v4.11.0) (2024-05-17)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.10.1](https://github.com/lingui/js-lingui/compare/v4.10.0...v4.10.1) (2024-05-03)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.10.0](https://github.com/lingui/js-lingui/compare/v4.8.0...v4.10.0) (2024-04-12)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.9.0](https://github.com/lingui/js-lingui/compare/v4.8.0...v4.9.0) (2024-04-12)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.8.0](https://github.com/lingui/js-lingui/compare/v4.7.2...v4.8.0) (2024-04-03)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.7.2](https://github.com/lingui/js-lingui/compare/v4.7.1...v4.7.2) (2024-03-26)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.7.1](https://github.com/lingui/js-lingui/compare/v4.7.0...v4.7.1) (2024-02-20)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.7.0](https://github.com/lingui/js-lingui/compare/v4.6.0...v4.7.0) (2024-01-05)
### Features
- **vite-plugin:** add support for vite@5 ([#1827](https://github.com/lingui/js-lingui/issues/1827)) ([5548d26](https://github.com/lingui/js-lingui/commit/5548d26194296fdc0c02c1b4f2c5bbda5c94db0b))
# [4.6.0](https://github.com/lingui/js-lingui/compare/v4.5.0...v4.6.0) (2023-12-01)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.5.0](https://github.com/lingui/js-lingui/compare/v4.4.2...v4.5.0) (2023-09-14)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.4.2](https://github.com/lingui/js-lingui/compare/v4.4.1...v4.4.2) (2023-08-31)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.4.1](https://github.com/lingui/js-lingui/compare/v4.4.0...v4.4.1) (2023-08-30)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.4.0](https://github.com/lingui/js-lingui/compare/v4.3.0...v4.4.0) (2023-08-08)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.3.0](https://github.com/lingui/js-lingui/compare/v4.2.1...v4.3.0) (2023-06-29)
### Features
- **vite-plugin:** report user-friendly error when macro used without transformation ([#1720](https://github.com/lingui/js-lingui/issues/1720)) ([53f6a7c](https://github.com/lingui/js-lingui/commit/53f6a7c8adccb78536c3283bad2d9c7752d58ca9))
## [4.2.1](https://github.com/lingui/js-lingui/compare/v4.2.0...v4.2.1) (2023-06-07)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.2.0](https://github.com/lingui/js-lingui/compare/v4.1.2...v4.2.0) (2023-05-26)
**Note:** Version bump only for package @lingui/vite-plugin
## [4.1.2](https://github.com/lingui/js-lingui/compare/v4.1.1...v4.1.2) (2023-05-17)
### Bug Fixes
- **webpack + vite:** fix dependency watching in loader ([#1662](https://github.com/lingui/js-lingui/issues/1662)) ([ce660d7](https://github.com/lingui/js-lingui/commit/ce660d7a3e37defda5f5708be5f14f1cd1bcb816))
## [4.1.1](https://github.com/lingui/js-lingui/compare/v4.1.0...v4.1.1) (2023-05-17)
**Note:** Version bump only for package @lingui/vite-plugin
# [4.1.0](https://github.com/lingui/js-lingui/compare/v4.0.0...v4.1.0) (2023-05-15)
**Note:** Version bump only for package @lingui/vite-plugin
## [3.17.2](https://github.com/lingui/js-lingui/compare/v3.17.1...v3.17.2) (2023-02-24)
### Bug Fixes
- **vite:** externalize macro imports ([#1466](https://github.com/lingui/js-lingui/issues/1466)) ([1719430](https://github.com/lingui/js-lingui/commit/1719430498cc7dc5071b883cd301e6618ca41cbf))
- **vite-plugin:** change default export to named export ([#1465](https://github.com/lingui/js-lingui/issues/1465)) ([15510c1](https://github.com/lingui/js-lingui/commit/15510c1a30020669989a78ae6677679cd7562a87)), closes [#1450](https://github.com/lingui/js-lingui/issues/1450)
- **vite-plugin:** ship in dual package format for compatibility with Vite ([#1450](https://github.com/lingui/js-lingui/issues/1450)) ([e3a2b39](https://github.com/lingui/js-lingui/commit/e3a2b3936e9f2d74c1357c493537c3c291b4875f))
## [3.17.1](https://github.com/lingui/js-lingui/compare/v3.17.0...v3.17.1) (2023-02-07)
**Note:** Version bump only for package @lingui/vite-plugin
# [3.17.0](https://github.com/lingui/js-lingui/compare/v3.16.1...v3.17.0) (2023-02-01)
### Features
- implement @lingui/vite-plugin ([#1306](https://github.com/lingui/js-lingui/issues/1306)) ([db5d3c3](https://github.com/lingui/js-lingui/commit/db5d3c309041202014d98b71894b473c587f643d))

Some files were not shown because too many files have changed in this diff Show More