From b4cb7b5ca2a06c08da5cf9b8d820804920771c1e Mon Sep 17 00:00:00 2001 From: Kirill Zhumarin Date: Sun, 19 Jan 2025 20:17:41 +0000 Subject: [PATCH] Initial commit --- .eslintignore | 10 + .eslintrc | 26 + .gitignore | 24 + .prettierignore | 14 + .prettierrc.json | 3 + .yarnrc.yml | 8 + babel.config.js | 15 + jest.config.integration.js | 17 + jest.config.js | 63 + jest.config.types.js | 9 + package.json | 89 + .../CHANGELOG.md | 319 + .../babel-plugin-extract-messages/README.md | 54 + .../build.config.ts | 5 + .../package.json | 51 + .../src/index.ts | 399 + .../test/__snapshots__/index.ts.snap | 527 + .../test/fixtures/js-call-expression.js | 26 + .../test/fixtures/js-message-descriptor.js | 25 + .../test/fixtures/js-with-macros.js | 63 + .../test/fixtures/jsx-with-macros.js | 13 + .../test/fixtures/jsx-without-macros.js | 12 + .../test/fixtures/jsx-without-trans.js | 3 + .../test/fixtures/lingui.config.js | 3 + .../test/fixtures/without-lingui.js | 7 + .../test/index.ts | 401 + .../tsconfig.json | 10 + .../babel-plugin-lingui-macro/CHANGELOG.md | 16 + packages/babel-plugin-lingui-macro/README.md | 40 + .../babel-plugin-lingui-macro/package.json | 109 + packages/babel-plugin-lingui-macro/src/ast.ts | 19 + .../src/constants.ts | 32 + .../babel-plugin-lingui-macro/src/icu.test.ts | 49 + packages/babel-plugin-lingui-macro/src/icu.ts | 151 + .../babel-plugin-lingui-macro/src/index.ts | 277 + .../babel-plugin-lingui-macro/src/macro.ts | 57 + .../babel-plugin-lingui-macro/src/macroJs.ts | 322 + .../src/macroJsAst.test.ts | 376 + .../src/macroJsAst.ts | 332 + .../src/macroJsx.test.ts | 321 + .../babel-plugin-lingui-macro/src/macroJsx.ts | 436 + .../src/messageDescriptorUtils.ts | 192 + .../babel-plugin-lingui-macro/src/utils.ts | 4 + .../src/utils/cleanJSXElementLiteralChild.ts | 45 + .../js-defineMessage.test.ts.snap | 203 + .../test/__snapshots__/js-plural.test.ts.snap | 99 + .../test/__snapshots__/js-select.test.ts.snap | 60 + .../js-selectOrdinal.test.ts.snap | 26 + .../test/__snapshots__/js-t.test.ts.snap | 629 + .../__snapshots__/js-useLingui.test.ts.snap | 418 + .../__snapshots__/jsx-plural.test.ts.snap | 263 + .../__snapshots__/jsx-select.test.ts.snap | 135 + .../jsx-selectOrdinal.test.ts.snap | 106 + .../test/__snapshots__/jsx-trans.test.ts.snap | 832 + .../js-t-continuation-character.expected.js | 8 + .../fixtures/js-t-continuation-character.js | 4 + .../fixtures/js-t-var/js-t-var.expected.js | 28 + .../test/fixtures/js-t-var/js-t-var.js | 11 + .../jsx-keep-forced-newlines.expected.js | 10 + .../test/fixtures/jsx-keep-forced-newlines.js | 6 + .../jsx-plural-select-nested.expected.js | 17 + .../test/fixtures/jsx-plural-select-nested.js | 34 + .../babel-plugin-lingui-macro/test/index.ts | 230 + .../test/js-defineMessage.test.ts | 103 + .../test/js-plural.test.ts | 50 + .../test/js-select.test.ts | 34 + .../test/js-selectOrdinal.test.ts | 16 + .../test/js-t.test.ts | 281 + .../test/js-useLingui.test.ts | 227 + .../test/jsx-plural.test.ts | 128 + .../test/jsx-select.test.ts | 59 + .../test/jsx-selectOrdinal.test.ts | 47 + .../test/jsx-trans.test.ts | 297 + .../test/lingui.config.js | 3 + .../test/macroTester.ts | 150 + .../babel-plugin-lingui-macro/tsconfig.json | 10 + packages/jest-mocks/index.test.ts | 47 + packages/jest-mocks/index.ts | 58 + packages/jest-mocks/package.json | 32 + packages/solid/.eslintrc | 4 + packages/solid/CHANGELOG.md | 4 + packages/solid/README.md | 34 + packages/solid/babel.config.js | 15 + .../macro/__typetests__/index.test-d.tsx | 130 + .../solid/macro/__typetests__/tsconfig.json | 9 + packages/solid/macro/index.d.ts | 137 + packages/solid/macro/index.js | 1 + packages/solid/macro/index.test.ts | 7 + packages/solid/package.json | 97 + packages/solid/src/I18nProvider.test.tsx | 223 + packages/solid/src/I18nProvider.tsx | 89 + packages/solid/src/Trans.test.tsx | 470 + packages/solid/src/Trans.tsx | 13 + packages/solid/src/TransNoContext.tsx | 139 + packages/solid/src/format.test.tsx | 134 + packages/solid/src/format.tsx | 121 + packages/solid/src/index.ts | 11 + packages/solid/tsconfig.json | 12 + packages/solid/vite.config.js | 17 + packages/vite-plugin/CHANGELOG.md | 150 + packages/vite-plugin/README.md | 26 + packages/vite-plugin/package.json | 58 + packages/vite-plugin/src/index.ts | 104 + .../test/__snapshots__/index.ts.snap | 31 + .../vite-plugin/test/default-vite.config.ts | 21 + packages/vite-plugin/test/index.ts | 78 + .../test/json-format/entrypoint.js | 3 + .../test/json-format/lingui.config.ts | 14 + .../test/json-format/locale/en.json | 4 + .../test/json-format/vite.config.ts | 3 + .../vite-plugin/test/macro-usage/.linguirc | 10 + .../test/macro-usage/entrypoint.js | 5 + .../vite-plugin/test/macro-usage/locale/en.po | 5 + .../test/macro-usage/vite.config.ts | 11 + .../vite-plugin/test/no-macro-error/.linguirc | 10 + .../test/no-macro-error/entrypoint.js | 5 + .../test/no-macro-error/locale/en.po | 5 + .../test/no-macro-error/vite.config.ts | 3 + packages/vite-plugin/test/po-format/.linguirc | 10 + .../vite-plugin/test/po-format/entrypoint.js | 3 + .../vite-plugin/test/po-format/locale/en.po | 5 + .../vite-plugin/test/po-format/vite.config.ts | 3 + packages/vite-plugin/tsconfig.json | 20 + scripts/jest/env.js | 3 + scripts/jest/setupTimezone.js | 3 + scripts/jest/stripAnsiSerializer.js | 6 + tsconfig.json | 30 + yarn.lock | 16627 ++++++++++++++++ 128 files changed, 28558 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .yarnrc.yml create mode 100644 babel.config.js create mode 100644 jest.config.integration.js create mode 100644 jest.config.js create mode 100644 jest.config.types.js create mode 100644 package.json create mode 100644 packages/babel-plugin-extract-messages/CHANGELOG.md create mode 100644 packages/babel-plugin-extract-messages/README.md create mode 100644 packages/babel-plugin-extract-messages/build.config.ts create mode 100644 packages/babel-plugin-extract-messages/package.json create mode 100644 packages/babel-plugin-extract-messages/src/index.ts create mode 100644 packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/js-message-descriptor.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/jsx-with-macros.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/jsx-without-macros.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/jsx-without-trans.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/lingui.config.js create mode 100644 packages/babel-plugin-extract-messages/test/fixtures/without-lingui.js create mode 100644 packages/babel-plugin-extract-messages/test/index.ts create mode 100644 packages/babel-plugin-extract-messages/tsconfig.json create mode 100644 packages/babel-plugin-lingui-macro/CHANGELOG.md create mode 100644 packages/babel-plugin-lingui-macro/README.md create mode 100644 packages/babel-plugin-lingui-macro/package.json create mode 100644 packages/babel-plugin-lingui-macro/src/ast.ts create mode 100644 packages/babel-plugin-lingui-macro/src/constants.ts create mode 100644 packages/babel-plugin-lingui-macro/src/icu.test.ts create mode 100644 packages/babel-plugin-lingui-macro/src/icu.ts create mode 100644 packages/babel-plugin-lingui-macro/src/index.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macro.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macroJs.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macroJsAst.test.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macroJsAst.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macroJsx.test.ts create mode 100644 packages/babel-plugin-lingui-macro/src/macroJsx.ts create mode 100644 packages/babel-plugin-lingui-macro/src/messageDescriptorUtils.ts create mode 100644 packages/babel-plugin-lingui-macro/src/utils.ts create mode 100644 packages/babel-plugin-lingui-macro/src/utils/cleanJSXElementLiteralChild.ts create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-select.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-selectOrdinal.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/js-useLingui.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-select.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-selectOrdinal.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-trans.test.ts.snap create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.expected.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.expected.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.expected.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.expected.js create mode 100644 packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.js create mode 100644 packages/babel-plugin-lingui-macro/test/index.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-defineMessage.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-plural.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-select.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-selectOrdinal.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-t.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/js-useLingui.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/jsx-plural.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/jsx-select.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/jsx-selectOrdinal.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/jsx-trans.test.ts create mode 100644 packages/babel-plugin-lingui-macro/test/lingui.config.js create mode 100644 packages/babel-plugin-lingui-macro/test/macroTester.ts create mode 100644 packages/babel-plugin-lingui-macro/tsconfig.json create mode 100644 packages/jest-mocks/index.test.ts create mode 100644 packages/jest-mocks/index.ts create mode 100644 packages/jest-mocks/package.json create mode 100644 packages/solid/.eslintrc create mode 100644 packages/solid/CHANGELOG.md create mode 100644 packages/solid/README.md create mode 100644 packages/solid/babel.config.js create mode 100644 packages/solid/macro/__typetests__/index.test-d.tsx create mode 100644 packages/solid/macro/__typetests__/tsconfig.json create mode 100644 packages/solid/macro/index.d.ts create mode 100644 packages/solid/macro/index.js create mode 100644 packages/solid/macro/index.test.ts create mode 100644 packages/solid/package.json create mode 100644 packages/solid/src/I18nProvider.test.tsx create mode 100644 packages/solid/src/I18nProvider.tsx create mode 100644 packages/solid/src/Trans.test.tsx create mode 100644 packages/solid/src/Trans.tsx create mode 100644 packages/solid/src/TransNoContext.tsx create mode 100644 packages/solid/src/format.test.tsx create mode 100644 packages/solid/src/format.tsx create mode 100644 packages/solid/src/index.ts create mode 100644 packages/solid/tsconfig.json create mode 100644 packages/solid/vite.config.js create mode 100644 packages/vite-plugin/CHANGELOG.md create mode 100644 packages/vite-plugin/README.md create mode 100644 packages/vite-plugin/package.json create mode 100644 packages/vite-plugin/src/index.ts create mode 100644 packages/vite-plugin/test/__snapshots__/index.ts.snap create mode 100644 packages/vite-plugin/test/default-vite.config.ts create mode 100644 packages/vite-plugin/test/index.ts create mode 100644 packages/vite-plugin/test/json-format/entrypoint.js create mode 100644 packages/vite-plugin/test/json-format/lingui.config.ts create mode 100644 packages/vite-plugin/test/json-format/locale/en.json create mode 100644 packages/vite-plugin/test/json-format/vite.config.ts create mode 100644 packages/vite-plugin/test/macro-usage/.linguirc create mode 100644 packages/vite-plugin/test/macro-usage/entrypoint.js create mode 100644 packages/vite-plugin/test/macro-usage/locale/en.po create mode 100644 packages/vite-plugin/test/macro-usage/vite.config.ts create mode 100644 packages/vite-plugin/test/no-macro-error/.linguirc create mode 100644 packages/vite-plugin/test/no-macro-error/entrypoint.js create mode 100644 packages/vite-plugin/test/no-macro-error/locale/en.po create mode 100644 packages/vite-plugin/test/no-macro-error/vite.config.ts create mode 100644 packages/vite-plugin/test/po-format/.linguirc create mode 100644 packages/vite-plugin/test/po-format/entrypoint.js create mode 100644 packages/vite-plugin/test/po-format/locale/en.po create mode 100644 packages/vite-plugin/test/po-format/vite.config.ts create mode 100644 packages/vite-plugin/tsconfig.json create mode 100644 scripts/jest/env.js create mode 100644 scripts/jest/setupTimezone.js create mode 100644 scripts/jest/stripAnsiSerializer.js create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d577640 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +**/dist/* +**/node_modules/* +**/fixtures/* +**/locale/* +website/* +examples/* +README.md +**/npm/* +/packages/*/build/** +/packages/solid/babel.config.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..50820da --- /dev/null +++ b/.eslintrc @@ -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}] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..788dabd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9afd601 --- /dev/null +++ b/.prettierignore @@ -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 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..cce9d3c --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "semi": false +} diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..f86e72e --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,8 @@ +compressionLevel: mixed + +enableGlobalCache: false + +nodeLinker: node-modules + +unsafeHttpWhitelist: + - 0.0.0.0 diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..c1252fa --- /dev/null +++ b/babel.config.js @@ -0,0 +1,15 @@ +module.exports = { + presets: [ + [ + "@babel/preset-env", + { + targets: { + node: 16, + }, + modules: "commonjs", + }, + ], + "@babel/preset-typescript", + "@babel/preset-react", + ], +} diff --git a/jest.config.integration.js b/jest.config.integration.js new file mode 100644 index 0000000..a914bda --- /dev/null +++ b/jest.config.integration.js @@ -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/", "/packages/*/build/"], +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..67d4ed5 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,63 @@ +const { pathsToModuleNameMapper } = require("ts-jest") +const tsConfig = require("./tsconfig.json") + +const tsConfigPathMapping = pathsToModuleNameMapper( + tsConfig.compilerOptions.paths, + { + prefix: "/", + } +) + +const testMatch = ["**/?(*.)test.(js|ts|tsx)", "**/test/index.(js|ts|tsx)"] + +/** + * @type {import('jest').Config} + */ +module.exports = { + roots: ["/packages/"], + collectCoverageFrom: [ + "**/*.{ts,tsx}", + "!**/*.d.ts", + "!**/*.test-d.{ts,tsx}", + "!**/node_modules/**", + "!**/build/**", + "!**/fixtures/**", + ], + coverageDirectory: "/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: ["/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: [ + "/packages/babel-plugin-extract-messages", + "/packages/babel-plugin-lingui-macro", + "/packages/vite-plugin" + ], + }, + ], +} diff --git a/jest.config.types.js b/jest.config.types.js new file mode 100644 index 0000000..97f280d --- /dev/null +++ b/jest.config.types.js @@ -0,0 +1,9 @@ +/** + * @type {import("jest").Config} + */ +module.exports = { + displayName: "tsd", + testMatch: ["**/__typetests__/*.test-d.{ts,tsx}"], + runner: "jest-runner-tsd", + roots: ["/packages/"], +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a09269b --- /dev/null +++ b/package.json @@ -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" +} diff --git a/packages/babel-plugin-extract-messages/CHANGELOG.md b/packages/babel-plugin-extract-messages/CHANGELOG.md new file mode 100644 index 0000000..d5ce94e --- /dev/null +++ b/packages/babel-plugin-extract-messages/CHANGELOG.md @@ -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 diff --git a/packages/babel-plugin-extract-messages/README.md b/packages/babel-plugin-extract-messages/README.md new file mode 100644 index 0000000..77ad099 --- /dev/null +++ b/packages/babel-plugin-extract-messages/README.md @@ -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 diff --git a/packages/babel-plugin-extract-messages/build.config.ts b/packages/babel-plugin-extract-messages/build.config.ts new file mode 100644 index 0000000..602da13 --- /dev/null +++ b/packages/babel-plugin-extract-messages/build.config.ts @@ -0,0 +1,5 @@ +import { defineBuildConfig } from "unbuild" + +export default defineBuildConfig({ + externals: ["@babel/core", "@babel/types", "@babel/traverse"], +}) diff --git a/packages/babel-plugin-extract-messages/package.json b/packages/babel-plugin-extract-messages/package.json new file mode 100644 index 0000000..2c6f759 --- /dev/null +++ b/packages/babel-plugin-extract-messages/package.json @@ -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" + } +} diff --git a/packages/babel-plugin-extract-messages/src/index.ts b/packages/babel-plugin-extract-messages/src/index.ts new file mode 100644 index 0000000..afaa4ba --- /dev/null +++ b/packages/babel-plugin-extract-messages/src/index.ts @@ -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 +} + +export type ExtractPluginOpts = { + onMessageExtracted(msg: ExtractedMessage): void +} + +type RawMessage = { + id?: string + message?: string + comment?: string + context?: string + placeholders?: Record +} + +export type Origin = [filename: string, line: number, column?: number] + +function collectMessage( + path: NodePath, + 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 = {} + + ;(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, + 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 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((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) { + // 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) + }, + }, + } +} diff --git a/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap new file mode 100644 index 0000000..55acb80 --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/__snapshots__/index.ts.snap @@ -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}, + message: undefined, + origin: [ + jsx-without-macros.js, + 11, + ], + placeholders: { + count: count, + }, + }, +] +`; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js b/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js new file mode 100644 index 0000000..19bc697 --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-call-expression.js @@ -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", +}) diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-message-descriptor.js b/packages/babel-plugin-extract-messages/test/fixtures/js-message-descriptor.js new file mode 100644 index 0000000..3761efc --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-message-descriptor.js @@ -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" } diff --git a/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js new file mode 100644 index 0000000..564320d --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/js-with-macros.js @@ -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", + })}` +} diff --git a/packages/babel-plugin-extract-messages/test/fixtures/jsx-with-macros.js b/packages/babel-plugin-extract-messages/test/fixtures/jsx-with-macros.js new file mode 100644 index 0000000..c14c5c3 --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/jsx-with-macros.js @@ -0,0 +1,13 @@ +import { Trans } from "@lingui/react/macro" +import { t, plural } from "@lingui/core/macro" +;Hi, my name is {name} +;Some message +;Some other message +;Some message +; +; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-macros.js b/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-macros.js new file mode 100644 index 0000000..5a91b7e --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-macros.js @@ -0,0 +1,12 @@ +import { Trans } from "@lingui/react" + +; + +; +; +; +; +; +; +; + diff --git a/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-trans.js b/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-trans.js new file mode 100644 index 0000000..6a6d0ee --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/jsx-without-trans.js @@ -0,0 +1,3 @@ +import { Plural } from "@lingui/react/macro" +; +; diff --git a/packages/babel-plugin-extract-messages/test/fixtures/lingui.config.js b/packages/babel-plugin-extract-messages/test/fixtures/lingui.config.js new file mode 100644 index 0000000..56ad108 --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/lingui.config.js @@ -0,0 +1,3 @@ +module.exports = { + locales: ["en", "cs"] +} diff --git a/packages/babel-plugin-extract-messages/test/fixtures/without-lingui.js b/packages/babel-plugin-extract-messages/test/fixtures/without-lingui.js new file mode 100644 index 0000000..16aa895 --- /dev/null +++ b/packages/babel-plugin-extract-messages/test/fixtures/without-lingui.js @@ -0,0 +1,7 @@ +import { Select } from "awesome-form-lib" +import { Trans } from "awesome-animation-lib" + +; + Displaced element + +;` + ) + 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", + }, + }) + }) + }) +}) diff --git a/packages/babel-plugin-lingui-macro/src/macroJsx.ts b/packages/babel-plugin-lingui-macro/src/macroJsx.ts new file mode 100644 index 0000000..0243cea --- /dev/null +++ b/packages/babel-plugin-lingui-macro/src/macroJsx.ts @@ -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 + +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 + ) + + 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) => { + 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, + componentName + ), + ] + } + + if (path.isJSXElement() && !ignoreElement) { + return [this.tokenizeElement(path)] + } + + if (!ignoreExpression) { + return [this.tokenizeExpression(path)] + } + } + + tokenizeTrans = (path: NodePath): 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 + + 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): Token[] { + const expressions = exp.get("expressions") as NodePath[] + + 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, + 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 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 + + if (this.types.isJSXNamespacedName(attr.node.name)) { + continue + } + + const name = attr.node.name.name + const value = attr.get("value") as + | NodePath + | NodePath + + 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).get( + "expression" + ) as NodePath + ).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): 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): ArgToken => { + return { + type: "arg", + name: this.expressionToArgument(path), + value: path.node as Expression, + } + } + + tokenizeConditionalExpression = ( + exp: NodePath + ): 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): string { + return path.isIdentifier() ? path.node.name : String(this.expressionIndex()) + } + + isLinguiComponent = ( + path: NodePath, + name: JsxMacroName + ): path is NodePath => { + 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 => { + 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 + } + } +} diff --git a/packages/babel-plugin-lingui-macro/src/messageDescriptorUtils.ts b/packages/babel-plugin-lingui-macro/src/messageDescriptorUtils.ts new file mode 100644 index 0000000..165c003 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/src/messageDescriptorUtils.ts @@ -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, + 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) { + 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 +} diff --git a/packages/babel-plugin-lingui-macro/src/utils.ts b/packages/babel-plugin-lingui-macro/src/utils.ts new file mode 100644 index 0000000..17261d0 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/src/utils.ts @@ -0,0 +1,4 @@ +export const makeCounter = + (index = 0) => + () => + index++ diff --git a/packages/babel-plugin-lingui-macro/src/utils/cleanJSXElementLiteralChild.ts b/packages/babel-plugin-lingui-macro/src/utils/cleanJSXElementLiteralChild.ts new file mode 100644 index 0000000..4e44e6b --- /dev/null +++ b/packages/babel-plugin-lingui-macro/src/utils/cleanJSXElementLiteralChild.ts @@ -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 +} diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap new file mode 100644 index 0000000..414de0c --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-defineMessage.test.ts.snap @@ -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, + }, + }; + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap new file mode 100644 index 0000000..925fb50 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-plural.test.ts.snap @@ -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, + }, + } +); + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-select.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-select.test.ts.snap new file mode 100644 index 0000000..7130a57 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-select.test.ts.snap @@ -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, + }, + } +); + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-selectOrdinal.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-selectOrdinal.test.ts.snap new file mode 100644 index 0000000..63ca791 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-selectOrdinal.test.ts.snap @@ -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, + }, + } +); + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap new file mode 100644 index 0000000..fea76d4 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-t.test.ts.snap @@ -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", + } +); + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/js-useLingui.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-useLingui.test.ts.snap new file mode 100644 index 0000000..6fb9cb8 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/js-useLingui.test.ts.snap @@ -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", + } + ); +} + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap new file mode 100644 index 0000000..920ba58 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-plural.test.ts.snap @@ -0,0 +1,263 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#1 1`] = ` +import { Plural } from "@lingui/react/macro"; +A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +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}}", + values: { + count: count, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#4 1`] = ` +import { Trans, Plural } from "@lingui/react/macro"; + + # slot added + + } + other={ + + # slots added + + } +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "X8eyr1", + message: + "{count, plural, one {<0># slot added} other {<1># slots added}}", + values: { + count: count, + }, + components: { + 0: , + 1: , + }, + } + } +/>; + +`; + +exports[`#6 1`] = ` +import { Plural } from "@lingui/react/macro"; +A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +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}}", + values: { + count: count, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#7 1`] = ` +import { Trans, Plural } from "@lingui/react/macro"; + + Looking for{" "} + a lot of them} + /> +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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}}", + values: { + 0: items.length, + 1: items.length, + 2: 42, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#8 1`] = ` +import { Plural } from "@lingui/react/macro"; +A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +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}}", + values: { + count: count, + oneText: oneText, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`Plural macro could be renamed 1`] = ` +import { Plural as Plural2 } from "@lingui/react/macro"; +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + {}} + value={count} + offset="1" + _0="Zero items" + few={\`\${count} items\`} + other={A lot of them} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +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}}", + comment: "Comment for translator", + context: "translation context", + values: { + count: count, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`Should return cases without leading or trailing spaces for nested Trans inside Plural 1`] = ` +import { Trans, Plural } from "@lingui/react/macro"; +One hello} + other={Other hello} + 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, + }, + } + } +/>; + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-select.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-select.test.ts.snap new file mode 100644 index 0000000..de204ca --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-select.test.ts.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#1 1`] = ` +import { Select } from "@lingui/react/macro"; +Other} +/>; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + render="strong" + { + /*i18n*/ + ...{ + id: "msg.select", + message: "{0, select, male {He} female {She} other {<0>Other}}", + values: { + 0: user.gender, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#4 1`] = ` +import { Select } from "@lingui/react/macro"; + + Hooray! + + } + _sad={ + + Oh no! + + } + 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: , + 1: , + }, + } + } +/>; + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-selectOrdinal.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-selectOrdinal.test.ts.snap new file mode 100644 index 0000000..9617b9c --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-selectOrdinal.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#1 1`] = ` +import { Trans, SelectOrdinal } from "@lingui/react/macro"; + + This is my{" "} + #rd} + />{" "} + cat. +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "3YEV8L", + message: + "This is my {count, selectordinal, one {#st} two {#nd} other {<0>#rd}} cat.", + values: { + count: count, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#2 1`] = ` +import { Trans, SelectOrdinal } from "@lingui/react/macro"; + + This is my + #rd} + />{" "} + cat. +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "Dz3XK1", + message: + "This is my{count, selectordinal, one {#st} two {#nd} other {<0>#rd}} cat.", + values: { + count: count, + }, + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`#3 1`] = ` +import { Trans, SelectOrdinal } from "@lingui/react/macro"; + + This is my{" "} + #rd} + />{" "} + cat. +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "CDpzE+", + message: + "This is my {0, selectordinal, one {#st} two {#nd} other {<0>#rd}} cat.", + values: { + 0: user.numCats, + }, + components: { + 0: , + }, + } + } +/>; + +`; diff --git a/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-trans.test.ts.snap b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-trans.test.ts.snap new file mode 100644 index 0000000..87d5a50 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/__snapshots__/jsx-trans.test.ts.snap @@ -0,0 +1,832 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Elements are replaced with placeholders 1`] = ` +import { Trans } from "@lingui/react/macro"; + + Hello World! +
+

+ My name is{" "} + + {" "} + {name} + +

+
; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "k9gsHO", + message: "Hello <0>World!<1/><2>My name is <3> <4>{name}", + values: { + name: name, + }, + components: { + 0: , + 1:
, + 2:

, + 3: , + 4: , + }, + } + } +/>; + +`; + +exports[`Elements inside expression container 1`] = ` +import { Trans } from "@lingui/react/macro"; +{Component inside expression container}; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "1cZQQW", + message: "<0>Component inside expression container", + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`Elements without children 1`] = ` +import { Trans } from "@lingui/react/macro"; +{
}
; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "SCJtqt", + message: "<0/>", + components: { + 0:
, + }, + } + } +/>; + +`; + +exports[`Expressions are converted to positional arguments 1`] = ` +import { Trans } from "@lingui/react/macro"; + + Property {props.name}, function {random()}, array {array[index]}, constant{" "} + {42}, object {new Date()}, everything {props.messages[index].value()} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + This should work   +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "K/1Xpr", + message: "<0>This should work \\xA0", + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`Ignore JSXEmptyExpression 1`] = ` +import { Trans } from "@lingui/react/macro"; +Hello {/* and I cannot stress this enough */} World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello, {props.world ? world : guys} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello,{" "} + {props.world ? ( + world + ) : props.b ? ( + nested + ) : ( + guys + )} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello World +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello World +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello World +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hello World +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Speak "friend"!; +Speak "friend"!; + +↓ ↓ ↓ ↓ ↓ ↓ + +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 = Hello; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + {}} +> + Hello World +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +{"hello {count, plural, one {world} other {worlds}}"}; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Strip whitespace around arguments: '{name}'; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Strip whitespace around tags, but keep forced spaces! +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "Ud4KOf", + message: "Strip whitespace around tags, but keep <0>forced spaces!", + components: { + 0: , + }, + } + } +/>; + +`; + +exports[`Strip whitespace around tags but keep whitespaces in JSX containers 1`] = ` +import { Trans } from "@lingui/react/macro"; + + {"Wonderful framework "} +
Next.js + {" say hi. And "} + Next.js + {" say hi."} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "3YVd0H", + message: + "Wonderful framework <0>Next.js say hi. And <1>Next.js say hi.", + components: { + 0: , + 1: , + }, + } + } +/>; + +`; + +exports[`Template literals as children 1`] = ` +import { Trans } from "@lingui/react/macro"; +{\`How much is \${expression}? \${count}\`}; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Read{" "} + + more + +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as _i18n } from "@lingui/core"; +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "QZyANg", + message: "Read <0>more", + components: { + 0: ( + + ), + }, + } + } +/>; + +`; + +exports[`Use a js macro inside a JSX Attribute of a non macro JSX component 1`] = ` +import { plural } from "@lingui/core/macro"; + + About +; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { i18n as _i18n } from "@lingui/core"; + + About +; + +`; + +exports[`Use decoded html entities 1`] = ` +import { Trans } from "@lingui/react/macro"; +&; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + Hi {yourName}, my name is {myName} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; + + {duplicate} variable {duplicate} +; + +↓ ↓ ↓ ↓ ↓ ↓ + +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"; +Hello World; + +↓ ↓ ↓ ↓ ↓ ↓ + +import { Trans as _Trans } from "@lingui/react"; +<_Trans + { + /*i18n*/ + ...{ + id: "msg.hello", + } + } +/>; + +`; diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.expected.js b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.expected.js new file mode 100644 index 0000000..5958cd9 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.expected.js @@ -0,0 +1,8 @@ +import { i18n as _i18n } from "@lingui/core" +_i18n._( + /*i18n*/ + { + id: "LBYoFK", + message: "Multiline with continuation", + } +) diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.js b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.js new file mode 100644 index 0000000..ac0aa6a --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-continuation-character.js @@ -0,0 +1,4 @@ +import { t } from "@lingui/core/macro" + +t`Multiline\ + with continuation` diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.expected.js b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.expected.js new file mode 100644 index 0000000..abe4115 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.expected.js @@ -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, + }, + } + ) + } +} diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.js b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.js new file mode 100644 index 0000000..c0156da --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/js-t-var/js-t-var.js @@ -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}` + } +} diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.expected.js b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.expected.js new file mode 100644 index 0000000..b6df26f --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.expected.js @@ -0,0 +1,10 @@ +import { Trans as _Trans } from "@lingui/react" +;<_Trans + { + /*i18n*/ + ...{ + id: "9xE5pD", + message: "Keep multiple\nforced\nnewlines!", + } + } +/> diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.js b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.js new file mode 100644 index 0000000..3727d9a --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-keep-forced-newlines.js @@ -0,0 +1,6 @@ +import { Trans } from "@lingui/react/macro" +; + Keep multiple{"\n"} + forced{"\n"} + newlines! + diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.expected.js b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.expected.js new file mode 100644 index 0000000..0de8a73 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.expected.js @@ -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, + }, + } + } +/> diff --git a/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.js b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.js new file mode 100644 index 0000000..07d3ac8 --- /dev/null +++ b/packages/babel-plugin-lingui-macro/test/fixtures/jsx-plural-select-nested.js @@ -0,0 +1,34 @@ +import { Select, Plural } from "@lingui/react/macro" +;Not allowed") + ).toThrowErrorMatchingSnapshot() + expect( + transformCode("Not allowed") + ).toThrowErrorMatchingSnapshot() + }) + + it("value is missing", function () { + const code = `` + expect(transformCode(code)).toThrowErrorMatchingSnapshot() + }) + + it("offset must be number or string, not variable", function () { + const variable = `` + expect(transformCode(variable)).toThrowErrorMatchingSnapshot() + }) + + it("plural forms are missing", function () { + const plural = `` + expect(transformCode(plural)).toThrowErrorMatchingSnapshot() + + const select = `Other} + />; + `, + }, + { + code: ` + import { Select } from '@lingui/react/macro'; + Hooray! + } + _sad={ + Oh no! + } + other="Dunno" + /> + `, + }, + { + code: ` + import { Select } from '@lingui/react/macro'; + + Message + +) + +// @ts-expect-error: `value` could be string only +m = + +// @ts-expect-error: `value` required +m = + +// @ts-expect-error: exact cases should be prefixed with underscore +m = ...} + other={...} + /> +) + +//////////////////////// +//// SolidJS useLingui() +//////////////////////// +function MyComponent() { + const { t, i18n } = useLingui() + + expectType(t`Hello world`) + expectType(t({ message: "my message" })) + // @ts-expect-error: you could not pass a custom instance here + t(i18n())({ message: "my message" }) + + expectType(i18n()) +} diff --git a/packages/solid/macro/__typetests__/tsconfig.json b/packages/solid/macro/__typetests__/tsconfig.json new file mode 100644 index 0000000..75cb8de --- /dev/null +++ b/packages/solid/macro/__typetests__/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "esModuleInterop": true, + "skipLibCheck": true + }, + "paths": {} +} diff --git a/packages/solid/macro/index.d.ts b/packages/solid/macro/index.d.ts new file mode 100644 index 0000000..1d819d7 --- /dev/null +++ b/packages/solid/macro/index.d.ts @@ -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 + * ``` + * Hello {username}. Read the docs. + * ``` + * @example + * ``` + * Hello {username}. + * ``` + */ +export const Trans: ParentComponent + +/** + * Props of Plural macro are transformed into plural format. + * + * @example + * ``` + * import { Plural } from "@lingui/core/macro" + * + * + * // ↓ ↓ ↓ ↓ ↓ ↓ + * import { Trans } from "@lingui-solid/solid" + * + * ``` + */ +export const Plural: Component +/** + * Props of SelectOrdinal macro are transformed into selectOrdinal format. + * + * @example + * ``` + * // count == 1 -> 1st + * // count == 2 -> 2nd + * // count == 3 -> 3rd + * // count == 4 -> 4th + * + * ``` + */ +export const SelectOrdinal: Component + +/** + * Props of Select macro are transformed into select format + * + * @example + * ``` + * // gender == "female" -> Her book + * // gender == "male" -> His book + * // gender == "non-binary" -> Their book + * + *