mirror of
https://github.com/stoatchat/javascript-lingui-solid.git
synced 2026-07-01 21:44:23 -04:00
Initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
**/dist/*
|
||||
**/node_modules/*
|
||||
**/fixtures/*
|
||||
**/locale/*
|
||||
website/*
|
||||
examples/*
|
||||
README.md
|
||||
**/npm/*
|
||||
/packages/*/build/**
|
||||
/packages/solid/babel.config.js
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"semi": false
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
unsafeHttpWhitelist:
|
||||
- 0.0.0.0
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
node: 16,
|
||||
},
|
||||
modules: "commonjs",
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react",
|
||||
],
|
||||
}
|
||||
@@ -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/"],
|
||||
}
|
||||
@@ -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"
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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/"],
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
+25
@@ -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 </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>&</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",
|
||||
}
|
||||
}
|
||||
/>;
|
||||
|
||||
`;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { i18n as _i18n } from "@lingui/core"
|
||||
_i18n._(
|
||||
/*i18n*/
|
||||
{
|
||||
id: "LBYoFK",
|
||||
message: "Multiline with continuation",
|
||||
}
|
||||
)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
|
||||
t`Multiline\
|
||||
with continuation`
|
||||
+28
@@ -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}`
|
||||
}
|
||||
}
|
||||
+10
@@ -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>
|
||||
+17
@@ -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 </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>&</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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"plugins": ["solid"],
|
||||
"extends": ["../../.eslintrc", "plugin:solid/typescript"]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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": {}
|
||||
}
|
||||
Vendored
+137
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("@lingui-solid/babel-plugin-lingui-macro/macro")
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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."
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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}</>
|
||||
}
|
||||
@@ -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</0>")
|
||||
expect(html(() => formatElements("text<0 />"))).toEqual("text<0 />")
|
||||
|
||||
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>"
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 })],
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user