diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..7970f5667 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,40 @@ +{ + "projectName": "react-i18next", + "projectOwner": "i18next", + "repoType": "github", + "repoHost": "https://github.com/", + "files": ["README.md"], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "jamuhl", + "name": "Jan Mühlemann", + "avatar_url": "https://avatars3.githubusercontent.com/u/977772?v=4", + "profile": "http://twitter.com/jamuhl", + "contributions": ["code", "example", "doc", "question"] + }, + { + "login": "adrai", + "name": "Adriano Raiano", + "avatar_url": "https://avatars0.githubusercontent.com/u/1086194?v=4", + "profile": "http://twitter.com/#!/adrirai", + "contributions": ["code", "example", "doc", "question"] + }, + { + "login": "tigerabrodi", + "name": "Tiger Abrodi", + "avatar_url": "https://avatars1.githubusercontent.com/u/49603590?v=4", + "profile": "https://tigerabrodi.dev/", + "contributions": ["question", "code", "review"] + }, + { + "login": "pedrodurek", + "name": "Pedro Durek", + "avatar_url": "https://avatars1.githubusercontent.com/u/12190482?v=4", + "profile": "https://github.com/pedrodurek", + "contributions": ["code", "example"] + } + ], + "commitConvention": "none" +} diff --git a/.babelrc b/.babelrc index c15e35781..d34e072f2 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,31 @@ { "env": { "development": { - "presets": [ "react", "es2015", "stage-0" ] - }, - "rollup": { - "presets": [ "react", "es2015-rollup", "stage-0" ] + "presets": [ + [ + "@babel/env", + { + "targets": { "browsers": ["defaults"] } + } + ], + "@babel/react" + ], + "plugins": ["@babel/plugin-transform-runtime"] }, "jsnext": { - "presets": [ "react", ["es2015", { "modules": false }], "stage-0" ] + "presets": [ + [ + "@babel/env", + { + "targets": { "browsers": ["defaults"] }, + "modules": false, + "useBuiltIns": false + } + ], + "@babel/react" + ], + "plugins": ["@babel/plugin-transform-runtime"] } - } + }, + "comments": false } diff --git a/.codeclimate.yml b/.codeclimate.yml index bd535c48c..8d4de3d0b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -3,24 +3,29 @@ engines: enabled: true config: languages: - - ruby - - javascript: - mass_threshold: 58 - - python - - php + - ruby + - javascript: + mass_threshold: 58 + - python + - php eslint: - enabled: true + # Disabled since this engine used a different set of version of + # `@typescript/eslint` and `typescript` + # + # @see https://github.com/i18next/i18next/pull/2098 + enabled: false + channel: 'eslint-8' fixme: enabled: true ratings: paths: - - "src/**/*" + - 'src/**/*' exclude_paths: -- test/ -- coverage/ -- dist/ -- example/ -- "react-i18next.js" -- "react-i18next.min.js" -- "rollup.config.js" -- "src/shallowEqual.js" + - test/ + - coverage/ + - dist/ + - example/ + - 'react-i18next.js' + - 'react-i18next.min.js' + - 'rollup.config.js' + - 'src/shallowEqual.js' diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..08de8e674 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: a7NPPsDW6jNJ3x23jBgWuO6KxV3Eq0mF2 diff --git a/.editorconfig b/.editorconfig index 5a3c9e0b5..5baa6a735 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,10 @@ # EditorConfig is awesome: http://EditorConfig.org root = true -[*.{js,jsx,json}] -end_of_line = lf -insert_final_newline = true +[*] charset = utf-8 -indent_style = space +end_of_line = lf indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintignore b/.eslintignore index c8ef29a15..22e295c24 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ -**/dist/* -**/node_modules/* -**/*.min.* +**/dist/ +**/node_modules/ +example/**/* +**/react-i18next.js +**/react-i18next.min.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 12bad7df5..000000000 --- a/.eslintrc +++ /dev/null @@ -1,27 +0,0 @@ -parser: babel-eslint -extends: airbnb -plugins: - - react - -rules: - max-len: [0, 100] - no-constant-condition: 0 - arrow-body-style: [1, "as-needed"] - comma-dangle: [2, "never"] - padded-blocks: [0, "never"] - no-unused-vars: [2, {vars: all, args: none}] - no-restricted-syntax: - - 1 - no-plusplus: 0 - no-param-reassign: 1 - react/forbid-prop-types: - - 1 - react/prop-types: - - 0 - - ignore: #coming from hoc - - location - - fields - - handleSubmit - -globals: - expect: false diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..dfae61bda --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,74 @@ +{ + "extends": ["airbnb", "prettier"], + "parser": "@babel/eslint-parser", + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "impliedStrict": true, + "classes": true + } + }, + "env": { + "browser": true, + "node": true + }, + "reportUnusedDisableDirectives": true, + "rules": { + "no-debugger": 0, + "no-alert": 0, + "no-unused-vars": [1, { "argsIgnorePattern": "res|next|^err" }], + "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }], + "prefer-const": ["error", { "destructuring": "all" }], + "arrow-body-style": [2, "as-needed"], + "no-unused-expressions": [2, { "allowTaggedTemplates": true }], + "no-param-reassign": [2, { "props": false }], + "no-console": 0, + "no-use-before-define": 0, + "no-nested-ternary": 0, + "import/prefer-default-export": 0, + "import/no-extraneous-dependencies": 1, + "import/no-named-as-default-member": 1, + "import": 0, + "func-names": 0, + "space-before-function-paren": 0, + "comma-dangle": 0, + "max-len": 0, + "import/extensions": 0, + "no-underscore-dangle": 0, + "consistent-return": 0, + "react/display-name": 1, + "react/no-array-index-key": 0, + "react/jsx-no-useless-fragment": ["error", { "allowExpressions": true }], + "react/react-in-jsx-scope": 0, + "react/prefer-stateless-function": 0, + "react/forbid-prop-types": 0, + "react/no-unescaped-entities": 0, + "react/prop-types": 0, + "jsx-a11y/accessible-emoji": 0, + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/no-unknown-property": ["error", { "ignore": ["i18nIsDynamicList"] }], + "radix": 0, + "no-shadow": [ + 2, + { "hoist": "all", "allow": ["resolve", "reject", "done", "next", "err", "error"] } + ], + "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "jsx-a11y/href-no-hash": "off", + "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }], + "react/jsx-props-no-spreading": 0 + }, + "overrides": [ + { + "files": ["test/*"], + "extends": ["plugin:testing-library/react", "plugin:jest-dom/recommended"], + "globals": { + "globalThis": false + }, + "rules": { + "testing-library/no-node-access": ["error", { "allowContainerFirstChild": true }], + "testing-library/no-manual-cleanup": "off" + } + } + ] +} diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..4350ba1fb --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,24 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 7 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - 'discussion' + - 'feature request' + - 'bug' + - 'breaking change' + - 'doc' + - 'issue' + - 'help wanted' + - 'good first issue' + - 'pr hold' +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 000000000..3dcac264e --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + # types: [opened, synchronize, reopened, ready_for_review] + branches: + - '**' + +jobs: + codeQuality: + name: Check code quality (lint and format) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Format check + run: npm run format + + - name: Lint + run: npm run lint + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build + + test: + name: Test on node ${{ matrix.node }} and ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: ['20.x', '18.x'] + os: [ubuntu-latest] + # Collect coverage only for node 20 and ubuntu-latest, no need to run it twice + # @see https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#expanding-or-adding-matrix-configurations + include: + - collectCoverage: true + node: '20.x' + os: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Test + if: ${{ !matrix.collectCoverage }} + run: npm test + + - name: Test with coverage + if: ${{ matrix.collectCoverage }} + run: npm run test:coverage + + - name: Coveralls + if: ${{ matrix.collectCoverage }} + uses: coverallsapp/github-action@v2 + + testTypescript: + name: Test typescript + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Test + run: npm run test:typescript diff --git a/.gitignore b/.gitignore index 03331124b..8e9f683ca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,13 @@ npm-debug.log npm-debug.log.* *.dat +.idea +.vscode +.eslintcache # Ignore various temporary files *~ *.swp -.idea/ # Ignore various Node.js related directories and files @@ -20,4 +22,11 @@ coverage/**/* example/build/**/* dist/**/* .next -package-lock.json +yarn.lock +out + +# ignore packaged releases +*.tgz + +# vitest temp / cache files +tsconfig.vitest-temp.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..5c3e95f02 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm exec lint-staged diff --git a/.npmignore b/.npmignore index 8bf802ee4..b6c1706a1 100644 --- a/.npmignore +++ b/.npmignore @@ -8,5 +8,15 @@ bin/ .eslintrc .gitignore bower.json -gulpfile.js -karma.conf.js +.github +.circleci +.vscode +rollup.config.js +rollup.config.mjs +.all-contributorsrc +.codeclimate.yml +.coveralls.yml +tsconfig*.json +.prettierrc.json +vitest.config.mts +vitest.workspace.mts \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..855fd7e23 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +**/dist/ +**/react-i18next.js +**/react-i18next.min.js diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..54806b53f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc.json", + "bracketSpacing": true, + "bracketSameLine": false, + "printWidth": 100, + "semi": true, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1ba3e6ae6..000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -node_js: - - "8" -script: - - npm test -after_success: - - npm run test:coverage - - cat ./coverage/lcov.info | node_modules/.bin/coveralls --verbose - - rm -rf ./coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index f14293d4d..6de5e9662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,49 +1,1095 @@ +### 15.5.1 + +add typescript as optional peer dependency [1843](https://github.com/i18next/react-i18next/pull/1843) + +### 15.5.0 + +feat: use const type parameters for useTranslation() [1842](https://github.com/i18next/react-i18next/pull/1842) + +### 15.4.1 + +fix: unique key warning on componentized element [1835](https://github.com/i18next/react-i18next/pull/1835) + +### 15.4.0 + +feat: add meta with codes on warnings to allow conditional logging [1826](https://github.com/i18next/react-i18next/pull/1826) + +### 15.3.0 + +Uses the i18next logger instead of the default console logger, if there is a valid i18next instance. Now the debug i18next option is respected, and you can also inject your own logger module: https://www.i18next.com/misc/creating-own-plugins#logger + +### 15.2.0 + +This version may be breaking if you still use React < v18 with TypeScript. +For JS users this version is equal to v15.1.4 + +- fix: Global JSX namespace is deprecated [1823](https://github.com/i18next/react-i18next/issues/1823) with [1822](https://github.com/i18next/react-i18next/pull/1822) + +### 15.1.4 + +- Fix: warning each child should have a unique key [1820](https://github.com/i18next/react-i18next/pull/1820) + +### 15.1.3 + +- fix: Self-closing REACT components in translation strings should not attempt to replace the component's children [1815](https://github.com/i18next/react-i18next/issues/1815) [1816](https://github.com/i18next/react-i18next/pull/1816) + +### 15.1.2 + +- fix: Attempted to assign to readonly property [1813](https://github.com/i18next/react-i18next/pull/1813) + +### 15.1.1 + +- fix: Not all namespaces are loaded when passing the lng option to useTranslate [1809](https://github.com/i18next/react-i18next/issues/1809) + +### 15.1.0 + +- fix: `` warns 'Each child in a list should have a unique "key" prop.' for react 19 [1806](https://github.com/i18next/react-i18next/pull/1806) + +### 15.0.3 + +- try to fix [unexpected token issue](https://github.com/i18next/next-i18next/issues/2302) + +### 15.0.2 + +- try to fix Trans handling with alwaysFormat set to true [1801](https://github.com/i18next/react-i18next/issues/1801) + +### 15.0.1 + +- revert arrow function in class property to address [this](https://github.com/i18next/react-i18next/commit/46e8ea5ff69325b73087811a2ce6a2b1faffa971#r145061161) + +### 15.0.0 + +- use optional chaining, nullish coalescing and nullish coalescing assignment [1774](https://github.com/i18next/react-i18next/pull/1774) +- Build config and optimizations [1769](https://github.com/i18next/react-i18next/pull/1769) +- some dependency updates [1768](https://github.com/i18next/react-i18next/pull/1768) +- use modern hasLoadedNamespace code (now requires at least i18next > v19.4.5 (introduced in june 2020)) + +### 14.1.3 + +- create a isObject helper function [1766](https://github.com/i18next/react-i18next/pull/1766) +- optimize nodesToString [1765](https://github.com/i18next/react-i18next/pull/1765) +- Simplifies hasValidReactChildren [1764](https://github.com/i18next/react-i18next/pull/1764) +- create a isString helper to avoid code duplication [1763](https://github.com/i18next/react-i18next/pull/1763) +- use arrow functions where possible [1762](https://github.com/i18next/react-i18next/pull/1762) +- use the commented out async code [1761](https://github.com/i18next/react-i18next/pull/1761) + +### 14.1.2 + +- bring back internal interpolationOverride handling for Trans component (if there are childrens), fixes [1754](https://github.com/i18next/react-i18next/issues/1754) + +### 14.1.1 + +- do not modify passed tOptions context property to address [1745](https://github.com/i18next/react-i18next/issues/1745) + +### 14.1.0 + +- types(`Trans`): add typechecking on context prop [1732](https://github.com/i18next/react-i18next/pull/1732) (might break if using "internal" `Trans` or `TransProps`) + +### 14.0.8 + +- fix: issue [1728](https://github.com/i18next/react-i18next/issues/1728) when useSuspense is false and default ns [1731](https://github.com/i18next/react-i18next/pull/1731) + +### 14.0.7 + +- try to get rid of internal interpolationOverride handling for Trans component, fixes [1729](https://github.com/i18next/react-i18next/issues/1729) + +### 14.0.6 + +- align context handling of Trans component with t function, fixes [1729](https://github.com/i18next/react-i18next/issues/1729) + +### 14.0.5 + +- Fix [1691](https://github.com/i18next/react-i18next/issues/1691) for strict mode, by preserving change language binding [1720](https://github.com/i18next/react-i18next/pull/1720) + +### 14.0.4 + +- fix interpolation of the count prop [1719](https://github.com/i18next/react-i18next/issues/1719) + +### 14.0.3 + +- revert changes done in v14.0.2 since it breaks normal language change render updates + +### 14.0.2 + +- Fix/bug [1691](https://github.com/i18next/react-i18next/issues/1691) make returned t function identical upon second effect run in strict mode [1716](https://github.com/i18next/react-i18next/pull/1716) + +### 14.0.1 + +- types: fix typo in `CustomInstanceExtensions` [1713](https://github.com/i18next/react-i18next/pull/1713) + +### 14.0.0 + +- types: reportNamespaces is now optional, should fix [1693](https://github.com/i18next/react-i18next/issues/1693) + +### 13.5.0 + +- self-closing components in translation strings should not attempt to replace the component's children [1695](https://github.com/i18next/react-i18next/issues/1695) + +### 13.4.1 + +- types: use CustomInstanceExtenstions to extend reportNamespaces + +### 13.4.0 + +- fix: separate cjs and mjs typings + +### 13.3.2 + +- types: fix consider importing '\*.js' + +### 13.3.1 + +- optimize defaultVariables feature introduced in last release + +### 13.3.0 + +- Respect defaultVariables in the interpolation options [1685](https://github.com/i18next/react-i18next/issues/1685) + +### 13.2.2 + +- Fix missing TransWithoutContext type [1672](https://github.com/i18next/react-i18next/pull/1672) + +### 13.2.1 + +- types: Allow iterable ReactI18NextChildren as children [1669](https://github.com/i18next/react-i18next/pull/1669) + +### 13.2.0 + +- Don't use defaults prop as default key [1664](https://github.com/i18next/react-i18next/pull/1664) + +### 13.1.2 + +- postpone usage of newer ES syntax + +### 13.1.1 + +- Render all children regardless of type when using i18nIsDynamicList prop [1661](https://github.com/i18next/react-i18next/pull/1661) + +### 13.1.0 + +- Fix non-list dynamic content in Trans component [1660](https://github.com/i18next/react-i18next/pull/1660) + +### 13.0.3 + +- fix unescape is not consistently called for all values [1657](https://github.com/i18next/react-i18next/issues/1657) + +### 13.0.2 + +- export icu.macro [1652](https://github.com/i18next/react-i18next/issues/1652) + +### 13.0.1 + +- types: Fix performance issue in Trans component [1646](https://github.com/i18next/react-i18next/pull/1646) + +### 13.0.0 + +- Update types to support t function redesign [1615](https://github.com/i18next/react-i18next/pull/1615) +- requires i18next >= v23.0.1 + +### 12.3.1 + +- optimization for optional lng prop for useTranslation, should now prevent missings when lazy loading translations [1637](https://github.com/i18next/react-i18next/issues/1637) + +### 12.3.0 + +- optional lng prop for useTranslation (helping on server side [1637](https://github.com/i18next/react-i18next/issues/1637)) + +### 12.2.2 + +- try to fix conditional exports in package.json + +### 12.2.1 + +- type fix: the type of defaultNS in I18nextProvider should support string[] [1633](https://github.com/i18next/react-i18next/pull/1633) + +### 12.2.0 + +- if defaultValue is passed in not ready t functio (via useTranslation) return that instead of the key, even though the user-land could should be fixed [1618](https://github.com/i18next/react-i18next/issues/1618) + +### 12.1.5 + +- fix react merged types [1606](https://github.com/i18next/react-i18next/pull/1606) originally introduced with #1531 to address #1506 + +### 12.1.4 + +- fix crash in gatsby [1594](https://github.com/i18next/react-i18next/issues/1594) + +### 12.1.3 + +- fix fallback of t function in Trans component + +### 12.1.2 + +- fix crash in gatsby [1594](https://github.com/i18next/react-i18next/issues/1594) + +### 12.1.1 + +- fix for node resolution [1589](https://github.com/i18next/react-i18next/issues/1589) + +### 12.1.0 + +- context-less version of Trans component to be used for environments without react context [1588](https://github.com/i18next/react-i18next/pull/1588) + +### 12.0.0 + +- Update t function types to rely on types coming from i18next [1501](https://github.com/i18next/react-i18next/pull/1501) + +### 11.18.6 + +- types: nsMode [1554](https://github.com/i18next/react-i18next/issues/1554) + +### 11.18.5 + +- support unescaping forward slash [1548](https://github.com/i18next/react-i18next/pull/1548) + +### 11.18.4 + +- fix: reset t when keyPrefix is updated [1544](https://github.com/i18next/react-i18next/pull/1544) + +### 11.18.3 + +- types: bindI18n option for UseTranslationOptions + +### 11.18.2 + +- more html entities to unescape by default [1538](https://github.com/i18next/react-i18next/pull/1538) + +### 11.18.1 + +- types: allow iterable with objects as children [1531](https://github.com/i18next/react-i18next/pull/1531) + +### 11.18.0 + +- ability to add custom unescape function [1529](https://github.com/i18next/react-i18next/pull/1529) + +### 11.17.4 + +- fix: UMD build [1527](https://github.com/i18next/react-i18next/issues/1527) + +### 11.17.3 + +- style: explicit React imports [1525](https://github.com/i18next/react-i18next/pull/1525) + +### 11.17.2 + +- reset t if ns changes in useTranslation [1518](https://github.com/i18next/react-i18next/pull/1518) + +### 11.17.1 + +- Stricter typescript type for Trans components prop [1516](https://github.com/i18next/react-i18next/pull/1516) + +### 11.17.0 + +- Add support for keyPrefix in withTranslation [1512](https://github.com/i18next/react-i18next/pull/1512) + +### 11.16.11 + +- types: fix Translation component types regression [1511](https://github.com/i18next/react-i18next/pull/1511) + +### 11.16.10 + +- types: translation component types [1509](https://github.com/i18next/react-i18next/pull/1509) + +### 11.16.9 + +- types: fix missing generic type for HTMLAttributes [1499](https://github.com/i18next/react-i18next/pull/1499) + +### 11.16.8 + +- types: fix Trans component to support react 18 types, by introducing allowObjectInHTMLChildren TS option [1492](https://github.com/i18next/react-i18next/pull/1492) + +### 11.16.7 + +- types: Added objects explicitly to Trans children [1486](https://github.com/i18next/react-i18next/pull/1486) + +### 11.16.6 + +- fix: warn just once 'i18n.languages were undefined or empty' and return true, like before + +### 11.16.5 + +- types: ReactNode should be prefixed with React [1481](https://github.com/i18next/react-i18next/pull/1481) + +### 11.16.4 + +- fix type 'TFunctionResult' is not assignable to type 'ReactNode' on React 18 [1480](https://github.com/i18next/react-i18next/pull/1480) + +### 11.16.3 + +- types: children fix for React v18 [1478](https://github.com/i18next/react-i18next/pull/1478) +- fix: apply [same fix](https://github.com/i18next/i18next/commit/0dcf7fdede9d58e16f82179b41b09f10eda5aeea) for local hasLoadedNamespace function + +### 11.16.2 + +- update macro to wrap defaults in brackets when necessary [1472](https://github.com/i18next/react-i18next/pull/1472) + +### 11.16.1 + +- types: for context prop of Trans component + +### 11.16.0 + +- fix: transSupportBasicHtmlNodes for keepArray check [1470](https://github.com/i18next/react-i18next/pull/1470) +- feat: add context prop to Trans component [1464](https://github.com/i18next/react-i18next/issues/1464) + +### 11.15.7 + +- types: add nsSeparator to CustomTypeOptions [1471](https://github.com/i18next/react-i18next/pull/1471) + +### 11.15.6 + +- fix error for typescript 4.6 [1453](https://github.com/i18next/react-i18next/pull/1463) + +### 11.15.5 + +- types: fix never return type when using plurals [1453](https://github.com/i18next/react-i18next/pull/1453) + +### 11.15.4 + +- types: add values field to Plural component in macros [1446](https://github.com/i18next/react-i18next/pull/1446) + +### 11.15.3 + +- types: fix for issue introduced with type extension for react-native [1436](https://github.com/i18next/react-i18next/pull/1436) + +### 11.15.2 + +- types: TypeScript interface for the Trans component does now accept react-native props [1418](https://github.com/i18next/react-i18next/pull/1418) + +### 11.15.1 + +- add missing types for shouldUnescape and useTranslation [1429](https://github.com/i18next/react-i18next/pull/1429) + +### 11.15.0 + +- option to unescape html in Trans [1426](https://github.com/i18next/react-i18next/pull/1426) + +### 11.14.3 + +- types: remove undefined from conditional type [1410](https://github.com/i18next/react-i18next/pull/1410) + +### 11.14.2 + +- Add type-safe support to deep keyPrefix [1403](https://github.com/i18next/react-i18next/pull/1403) + +### 11.14.1 + +- Rollback [1402](https://github.com/i18next/react-i18next/pull/1402): Remove generics from Trans component to suppress warning issue [1400](https://github.com/i18next/react-i18next/pull/1400) + +### 11.14.0 + +- Remove generics from Trans component to suppress warning issue [1400](https://github.com/i18next/react-i18next/pull/1400) +- Add type support to plurals [1399](https://github.com/i18next/react-i18next/pull/1399) + +### 11.13.0 + +- feat(types): add type-safe support to keyPrefix option [1390](https://github.com/i18next/react-i18next/pull/1390) +- feat(types): allow key separator augmentation [1367](https://github.com/i18next/react-i18next/pull/1367) + +### 11.12.0 + +- feature: add key prefix support to useTranslation hook [1371](https://github.com/i18next/react-i18next/pull/1371) + +### 11.11.4 + +- typescript: add returnNull and returnEptyString options to TypeOptions interface [1341](https://github.com/i18next/react-i18next/pull/1341) + +### 11.11.3 + +- Trans: parse first, then interpolate [1345](https://github.com/i18next/react-i18next/pull/1345) + +### 11.11.2 + +- feat(typings): support readonly namespaces in TFuncKey [1340](https://github.com/i18next/react-i18next/pull/1340) + +### 11.11.1 + +- feat(types): allow readonly namespaces in useTranslation [1339](https://github.com/i18next/react-i18next/pull/1339) + +### 11.11.0 + +- introduce `CustomTypeOptions` type definition and deprecate the `Resources` type definition [1328](https://github.com/i18next/react-i18next/pull/1328) + +### 11.10.0 + +- add transWrapTextNodes option [1324](https://github.com/i18next/react-i18next/pull/1324) to prevent a well-known Google Translate issue with React apps [1323](https://github.com/i18next/react-i18next/issues/1323), thanks to [feross](https://github.com/feross) + +### 11.9.0 + +- typescript/icu macro: add new syntax for interpolation of complex types [1316](https://github.com/i18next/react-i18next/pull/1316) -> [docs for template usage](https://react.i18next.com/misc/using-with-icu-format#tagged-template-for-icu) + +### 11.8.15 + +- ignore null children in Trans component [1307](https://github.com/i18next/react-i18next/issues/1307) + +### 11.8.14 + +- update html-parse-stringify to fix uppercase elements in Trans component [1304](https://github.com/i18next/react-i18next/issues/1304) + +### 11.8.13 + +- Replace html-parse-stringify2 with html-parse-stringify [1283](https://github.com/i18next/react-i18next/pull/1283) to prevent [CVE-2021-23346](https://github.com/i18next/react-i18next/issues/1275) + +### 11.8.12 + +- refactor: remove unneeded object [1286](https://github.com/i18next/react-i18next/pull/1286) + +### 11.8.11 + +- typescript: Bug fixes [1284](https://github.com/i18next/react-i18next/pull/1284) + +### 11.8.10 + +- typescript: Move type definition files [1276](https://github.com/i18next/react-i18next/pull/1276) + +### 11.8.9 + +- Fix allow to replace i18n in provider with useTranslation hook [1273](https://github.com/i18next/react-i18next/pull/1273) + +### 11.8.8 + +- typescript: Allow `TFuncKey` to be used without specifying the namespace, in the same way TFunction and useTranslation work [1262](https://github.com/i18next/react-i18next/pull/1262) + +### 11.8.7 + +- warning for old wait usage + +### 11.8.6 + +- typescript: Updated TS definitions (adding useSuspense option in TranslationProps) [1247](https://github.com/i18next/react-i18next/pull/1247) + +### 11.8.5 + +- typescript: fix: Inference for specific keys ts 4.1 [1230](https://github.com/i18next/react-i18next/pull/1230) + +### 11.8.4 + +- typescript: Add workaround to suppress infinite instantiation warning [1227](https://github.com/i18next/react-i18next/pull/1227) +- typescript: withTranslation() typing fix for defaultProps [1226](https://github.com/i18next/react-i18next/pull/1226) +- typescript: Accept const components prop for Trans [1224](https://github.com/i18next/react-i18next/pull/1224) + +### 11.8.3 + +- Fix: Return type inference for t function (typescript 4.1) [1221](https://github.com/i18next/react-i18next/pull/1221) + +### 11.8.2 + +- fix: type definitions for typescript 4.1 [1220](https://github.com/i18next/react-i18next/pull/1220) + +### 11.8.1 + +- fix: typescript definitions for t function without namespaces [1214](https://github.com/i18next/react-i18next/pull/1214) + +### 11.8.0 + +- typescript: Make the translation function fully type-safe [1193](https://github.com/i18next/react-i18next/pull/1193) +- trans should work with misleading overloaded empty elements in components [1206](https://github.com/i18next/react-i18next/pull/1206) + +### 11.7.4 + +- fixes passing interpolations options via Trans components tOptions prop [1204](https://github.com/i18next/react-i18next/pull/1204) + +### 11.7.3 + +- Avoid redundant re-rendering in I18nextProvider [1174](https://github.com/i18next/react-i18next/pull/1174) + +### 11.7.2 + +- Avoid setState while react is rendering [1165](https://github.com/i18next/react-i18next/pull/1165) + +### 11.7.1 + +- typescript: fix: typescript definition of context object [1160](https://github.com/i18next/react-i18next/pull/1160) + +### 11.7.0 + +- Trans interpolating self-closing tags in components prop(object) [1140](https://github.com/i18next/react-i18next/pull/1140) + +### 11.6.0 + +- Trans allow components props to be an object containing named interpolation elements + +### 11.5.1 + +- providing filename when running babel.parse in icu.macro [1133](https://github.com/i18next/react-i18next/pull/1133) + +### 11.5.0 + +- Trans: merge option in mapAST [1120](https://github.com/i18next/react-i18next/pull/1120) + +### 11.4.0 + +- Add sideEffects false to package json to allow tree shaking [1097](https://github.com/i18next/react-i18next/pull/1097) + +### 11.3.5 + +- fix returning defaultValue for Trans component [1092](https://github.com/i18next/react-i18next/pull/1092) + +### 11.3.4 + +- [useTranslation] Avoid setting the new `t` function if the component is unmounted. (1051)[https://github.com/i18next/react-i18next/pull/1051] + +### 11.3.3 + +- fixes copying ns in useSSR + +### 11.3.2 + +- typescript: Add optional defaultN [1050](https://github.com/i18next/react-i18next/pull/1050) + +### 11.3.1 + +- typescript: Translation component's ready parameter is missing in TypeScript definition [1044](https://github.com/i18next/react-i18next/pull/1044) +- change hook condition in Trans to equal useTranslations implementation + +### 11.3.0 + +- useSSR: add namespaces to init options options.ns [1031](https://github.com/i18next/react-i18next/issues/1031) +- typescript: Fix the type of the components props of Trans [1036](https://github.com/i18next/react-i18next/pull/1036) + +### 11.2.7 + +- typescript: Allow ComponentType for Trans' `parent` type [1021](https://github.com/i18next/react-i18next/pull/1021) + +### 11.2.6 + +- typescript: Allow html props on Trans, fix `parent` prop type [1019](https://github.com/i18next/react-i18next/pull/1019) + +### 11.2.5 + +- handle array fallback on wrongly configured app ;) [1010](https://github.com/i18next/react-i18next/pull/1010) + +### 11.2.4 + +- typescript: Extend withTranslation tests to include optional props [1009](https://github.com/i18next/react-i18next/pull/1009) + +### 11.2.3 + +- Store should be initialized after useSSR [1008](https://github.com/i18next/react-i18next/pull/1008) + +### 11.2.2 + +- Only pass forwardedRef to children if options.withRef is false [999](https://github.com/i18next/react-i18next/pull/999) + +### 11.2.1 + +- remove used jsx in withTranslation to avoid issues while compiling [994](https://github.com/i18next/react-i18next/pull/994) + +### 11.2.0 + +- withTranslation allow not only passing a ref with option withRef but also passing a forwardedRef from outside as props (before forwardedRef was only added to wrapped component if the withRef option was set) [992](https://github.com/i18next/react-i18next/pull/992) + +### 11.1.0 + +- Update `rollup.config.js` for IE11 Transpilations [988](https://github.com/i18next/react-i18next/pull/988) + +### 11.0.1 + +- typescript: Use updated ts export default from i18next [984](https://github.com/i18next/react-i18next/pull/984) + +### 11.0.0 + +- **Breaking** based on i18next changes made in [v18.0.0](https://github.com/i18next/i18next/blob/master/CHANGELOG.md#1800) changing the language should not trigger a Suspense anylonger. The state will be ready and `t` bound to the previous language until `languageChanged` get triggered -> this results in a nicer experience for users (no flickering Suspense while changing the language). Based on issue "Suspence is fired during lang change when useTranslation called in between" [975](https://github.com/i18next/react-i18next/issues/975) +- the default bindI18n is now `languageChanged` and `languageChanging` was removed from that default +- Adding `languageChanging` to bindI18n will bring back old behaviour where a language change will trigger a Suspense / ready: false while loading those new translations +- You can now override the defaults in i18next.options.react for `bindI18n`, `bindI18nStore` and `useSuspense` in the hook by `useTranslation(ns, { bindI18n, bindI18nStore, useSuspense})` or in the HOC by passing those as props. + +### 10.13.2 + +- typescript: Add t function to TransProps types [969](https://github.com/i18next/react-i18next/pull/969) +- lint: Fix linter errors [966](https://github.com/i18next/react-i18next/pull/966) + +### 10.13.1 + +- avoid conditional hook call in edge case (was only issue in wrong setup useContext outside I18nextProvider) [951](https://github.com/i18next/react-i18next/pull/951) + +### 10.13.0 + +- also use count from `values` object passed to Trans if passed - else use the one on props [947](https://github.com/i18next/react-i18next/pull/947) + +### 10.12.5 + +- typescript: Update types for reportNamespaces [945](https://github.com/i18next/react-i18next/pull/945) +- typescript: Improve withSSR type definition [943](https://github.com/i18next/react-i18next/pull/943) + +### 10.12.4 + +- ICU: Fixes macro to support count prop and expressions better [939](https://github.com/i18next/react-i18next/pull/939) + +### 10.12.3 + +- avoid conditional hook call in edge case (wrong setup) [935](https://github.com/i18next/react-i18next/pull/935) + +### 10.12.2 + +- Trans: do not replace html tags in translation strings that are not in the transKeepBasicHtmlNodesFor array [919](https://github.com/i18next/react-i18next/issues/919) + +### 10.12.1 + +- Set ready flag to false when i18n instance has not been initialised [918](https://github.com/i18next/react-i18next/pull/918) + +### 10.12.0 + +- fix / extend icu.macro: ICU: Trans macro will parse defaults as alternate to children [917](https://github.com/i18next/react-i18next/pull/917) + +### 10.11.5 + +- typescript: fix types for use() [912](https://github.com/i18next/react-i18next/pull/912) + +### 10.11.4 + +- assert edge case trans component get set a key +- assert context get destructed of empty object if context gets used falsely on a component got pulled out of main tree from react-portal or similar + +### 10.11.3 + +- only apply initial values in useSSR, withSSR on i18next instances not being a clone (eg. created by express middleware on server) ==> don't apply on serverside + +### 10.11.2 + +- Reload translations whenever namespaces passed to useTranslation() change [878](https://github.com/i18next/react-i18next/pull/878) + +### 10.11.1 + +- fixes a regression in Trans component taking namespace from passed t function [867](https://github.com/i18next/react-i18next/issues/867#issuecomment-502395958) + +### 10.11.0 + +- Restore support passing the defaultNS via I18nextProvider prop [860](https://github.com/i18next/react-i18next/pull/860) + +### 10.10.0 + +- HOC: expose wrapped component as WrappedComponent property [853](https://github.com/i18next/react-i18next/pull/853) + +### 10.9.1 + +- Fix useEffect mount/unmount usage [852](https://github.com/i18next/react-i18next/pull/852) + +### 10.9.0 + +- trigger suspense on languageChanging by add listening to that event too (new in i18next@15.1.0) - if you do not like this behaviour of suspending during languageChange - remove it from bindI18n + +### 10.8.1 + +- expose context [829](https://github.com/i18next/react-i18next/pull/829) + +### 10.8.0 + +- Support taking values for interpolation not only from content but the props count, values too: Replace count prop from in translation string automatically [826](https://github.com/i18next/react-i18next/issues/826) + +### 10.7.0 + +- brings back nsMode=default|fallback [822](https://github.com/i18next/react-i18next/pull/822) +- typescript: Add missing type definition for withTranslation options [821](https://github.com/i18next/react-i18next/pull/821) + +### 10.6.2 + +- Fix Trans component ignore default options [818](https://github.com/i18next/react-i18next/pull/818) + +### 10.6.1 + +- useTranslation useEffect also guard against unmounted for bound events...seems unmount and actual call to useEffect cleanup are not in correct order (component is first unmounted and then unbound - should be vice versa) + +### 10.6.0 + +- use forwardRef for withTranslation [802](https://github.com/i18next/react-i18next/pull/802) +- fixes Translation reset after component is unmounted with useTranslation [801](https://github.com/i18next/react-i18next/issues/801) + +### 10.5.3 + +- Fix the displayName of HOC components [798](https://github.com/i18next/react-i18next/pull/798) + +### 10.5.2 + +- fixes: transSupportBasicHtmlNodes doesn't work with self-closing Trans [790](https://github.com/i18next/react-i18next/issues/790) + +### 10.5.1 + +- ReferenceError: setImmediate is not defined [787](https://github.com/i18next/react-i18next/issues/787) + +### 10.5.0 + +- Adding support for nested component inside Trans that are a list.map like `` [784](https://github.com/i18next/react-i18next/pull/784) (Adding `, + ]} + /> + ); + } + const { container } = render(); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Result should be a list: +
    +
  • + li1 +
  • +
  • + li2 +
  • +
+
+ `); + }); + + it('should render dynamic ul as components property when pass as a children', () => { + const list = ['li1', 'li2']; + + function TestComponent() { + return ( + + My list: +
    + {list.map((item) => ( +
  • {item}
  • + ))} +
+
+ ); + } + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Result should be a list: +
    +
  • + li1 +
  • +
  • + li2 +
  • +
+
+ `); + }); + + it('should render dynamic Elements correctly', () => { + const dynamicContent =
testing
; + + function TestComponent() { + return ( + + My dynamic content: + {dynamicContent} + + ); + } + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ My dynamic content: +
+ testing +
+
+ `); + }); + + it('should render dynamic strings correctly', () => { + const dynamicContent = 'testing'; + + function TestComponent() { + return ( + + My dynamic content: {dynamicContent} + + ); + } + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ My dynamic content: + testing +
+ `); + }); +}); diff --git a/test/trans.render.icu.spec.jsx b/test/trans.render.icu.spec.jsx new file mode 100644 index 000000000..da610e68b --- /dev/null +++ b/test/trans.render.icu.spec.jsx @@ -0,0 +1,133 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup } from '@testing-library/react'; +import './i18n'; +import { Trans } from '../src/Trans'; + +describe('trans using no children but props - icu case', () => { + afterEach(() => { + cleanup(); + }); + + function TestComponent() { + return ( + universe]} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + world + +
+ `); + }); +}); + +describe('trans using no children but props - nested case', () => { + function TestComponent() { + return ( + + placeholder +
+ , + ]} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+ + hello +
+ world +
+
+ `); + }); +}); + +describe('trans using no children but props - self closing case', () => { + function TestComponent() { + return ]} />; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello +
+ world +
+ `); + }); +}); + +describe('Trans should use value from translation', () => { + it('should use value from translation if no data provided in component', () => { + function TestComponent() { + return ( + ]} + /> + ); + } + + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Result should be rendered within tag + + dragonfly + +
+ `); + }); + + it('should use value from translation if dummy data provided in component', () => { + function TestComponent() { + return ( + test string]} + /> + ); + } + + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ Result should be rendered within tag + + dragonfly + +
+ `); + }); +}); diff --git a/test/trans.render.object.spec.jsx b/test/trans.render.object.spec.jsx new file mode 100644 index 000000000..a8010016d --- /dev/null +++ b/test/trans.render.object.spec.jsx @@ -0,0 +1,296 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup } from '@testing-library/react'; +import './i18n'; +import { Trans } from '../src/Trans'; + +describe('trans using no children but components (object) - base case using array not object', () => { + afterEach(() => { + cleanup(); + }); + + function TestComponent() { + return ( + just dummy
, univers]} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + beautiful + + + + world + +
+ `); + }); +}); + +describe('trans using no children but components (object) - using index', () => { + function TestComponent() { + return ( + just dummy, 2: univers }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + beautiful + + + + world + +
+ `); + }); +}); + +describe.skip('trans using no children but components (object) - using names', () => { + function TestComponent() { + return ( + just dummy, bold: univers }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + under ten + + <10 this text after the sign should be rendered + + world + +
+ `); + }); +}); + +describe('trans using no children but components (object) - using names', () => { + function TestComponent() { + return ( + just dummy, bold: univers }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + beautiful + + + + world + +
+ `); + }); +}); + +describe('trans using no children but components (object) - using names with no lowercase', () => { + function TestComponent() { + return ( + just dummy, BoldCase: univers }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
+ hello + + beautiful + + + + world + +
+ `); + }); +}); + +describe('trans using no children but components (object) - use more than once', () => { + function TestComponent() { + return ( + a, listitem:
  • b
  • }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello +
      +
    • + A +
    • + , +
    • + B +
    • + and +
    • + C +
    • +
    +
    + `); + }); +}); + +describe('trans using no children but components (object) - use more than once (empty)', () => { + function TestComponent() { + return ( + , listitem:
  • }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello +
      +
    • + A +
    • + , +
    • + B +
    • + and +
    • + C +
    • +
    +
    + `); + }); +}); + +describe('trans using no children but components (object) - using self closing tag', () => { + function Button() { + return ; + } + function TestComponent() { + return }} />; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello + +
    + `); + }); +}); + +describe('trans using no children but components (object) - empty content', () => { + function Button() { + return ; + } + function TestComponent() { + return }} />; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello + +
    + `); + }); +}); + +describe('trans using children but components (object) - self closing tag', () => { + function Button() { + return ; + } + function TestComponent() { + return }}>{'hello '}; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello + +
    + `); + }); +}); + +describe('trans using no children but components (object) - interpolated component with children', () => { + function Button({ children }) { + return ; + } + function TestComponent() { + return }} />; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + hello + +
    + `); + }); +}); diff --git a/test/trans.render.spec.js b/test/trans.render.spec.js deleted file mode 100644 index 5da7e4b44..000000000 --- a/test/trans.render.spec.js +++ /dev/null @@ -1,280 +0,0 @@ -import React from 'react'; -import { shallow, render, mount } from 'enzyme'; -import i18n from './i18n'; -import translate from '../src/translate'; -import Trans from '../src/Trans'; -import PropTypes from "prop-types"; - -const context = { - i18n -}; - -function Link({ to, children }) { - return {children}; -} - -describe('trans simple', () => { - const TestElement = ({ t, parent }) => { - const count = 10; - const name = "Jan"; - return ( - - Open here. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - Go there. -
    - )).toBe(true); - }); - - describe('trans simple - setting back default behaviour of no parent', () => { - // we set in ./i18n react.defaultTransParent so all tests run backwards compatible - // and this tests new default bahaviour of just returning children - const TestElement = ({ t, parent }) => { - const count = 10; - const name = "Jan"; - return ( - - Open here. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( - Go there. - )).toBe(true); - }); - }); - - it('can use a different parent element', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - expect(wrapper.contains( - - Go there. - - )).toBe(true); - }); - - it('uses the i18n.t function if t is not in the context nor specified using props', () => { - const wrapper = mount(, { context, childContextTypes: { i18n: PropTypes.object.isRequired } }); - - expect(wrapper.contains(( -
    - Go there. -
    ) - )).toBe(true); - }); -}); - -describe('trans simple using ns prop', () => { - const TestElement = ({ t, parent }) => { - return ( - - Open here. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - Another go there. -
    - )).toBe(true); - }); -}); - -describe('trans testTransKey1 singular', () => { - const TestElement = ({ t }) => { - const numOfItems = 1; - return ( - - {{numOfItems}} items matched. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - 1 item matched. -
    - )).toBe(true); - }); -}); - -describe('trans testTransKey1 plural', () => { - const TestElement = ({ t }) => { - const numOfItems = 10; - return ( - - {{numOfItems}} items matched. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - 10 items matched. -
    - )).toBe(true); - }); -}); - -describe('trans testTransKey2', () => { - const TestElement = ({ t }) => { - const numOfItems = 10; - return ( - - {{numOfItems}} items matched. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - 10 items matched. -
    - )).toBe(true); - }); -}); - -describe('trans testTransKey3', () => { - const TestElement = ({ t }) => { - const numOfItems = 10; - return ( - - Result: {{numOfItems}} items matched. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - Result: 10 items matched. -
    - )).toBe(true); - }); -}); - - -describe('trans complex', () => { - const TestElement = ({ t }) => { - const count = 10; - const name = "Jan"; - return ( - - Hello {{name}}, you have {{count}} message. Open here. - - ); - } - - it('should render correct content', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains( -
    - Hello Jan, you have 10 messages. Open here. -
    - )).toBe(true); - }); -}); - -describe('trans with t as prop', () => { - const TestElement = ({ t, cb }) => { - const customT = (...args) => { - if (cb) cb(); - return t(...args); - }; - return ( - - Open here. - - ); - }; - - it('should use props t', () => { - let usedCustomT = false; - const cb = () => { usedCustomT = true; }; - - const HocElement = translate(['translation'], {})(TestElement); - - mount(, { context }); - expect(usedCustomT).toBe(true); - }); - - it('should not pass t to HTML element', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - expect(wrapper.contains( -
    - Go there. -
    - )).toBe(true); - }); - -}); - -describe('trans with empty content', () => { - const TestElement = ({ t, cb }) => { - return {""}; - }; - it('should render an empty string', () => { - const HocElement = translate(['translation'], {})(TestElement); - const wrapper = mount(, { context }); - expect(wrapper.contains(
    )).toBe(true); - }); -}); - -describe('trans with only content from translation file - no children', () => { - const TestElement = ({ t, cb }) => { - return ; - }; - it('should render translated string', () => { - const HocElement = translate(['translation'], {})(TestElement); - const wrapper = mount(, { context }); - expect(wrapper.contains(
    test
    )).toBe(true); - }); -}); diff --git a/test/trans.render.spec.jsx b/test/trans.render.spec.jsx new file mode 100644 index 000000000..dfd573ee1 --- /dev/null +++ b/test/trans.render.spec.jsx @@ -0,0 +1,1082 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; +import React from 'react'; +import { render, cleanup } from '@testing-library/react'; +import i18n from './i18n'; +import { withTranslation } from '../src/withTranslation'; +import { Trans } from '../src/Trans'; + +function Link({ to, children }) { + return {children}; +} + +describe('trans simple', () => { + afterEach(() => { + cleanup(); + }); + + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + there + + . +
    + `); + }); + + it('can use a different parent element', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` + + Go + + there + + . + + `); + }); +}); + +describe('trans simple using ns prop', () => { + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Another go + + there + + . +
    + `); + }); +}); + +describe('trans using translation prop', () => { + function TestComponent({ parent }) { + return ( + + {/* eslint-disable-next-line jsx-a11y/anchor-has-content */} + + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` + + `); + }); +}); + +describe('trans overwrites translation prop', () => { + function TestComponent({ parent }) { + return ( + + {/* eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label */} + + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` + + `); + }); +}); + +describe('trans simple with custom html tag', () => { + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + function TestComponent2({ parent }) { + return ; + } + + function TestComponent3({ parent }) { + return ; + } + + it('should not skip custom html tags', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + Go + + +
    + + there + + . +
    + `); + }); + + it('should not skip custom html tags - empty node', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + Go + + +
    + there. +
    + `); + }); + + it('should skip custom html tags not listed in transKeepBasicHtmlNodesFor', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + Go + + <video /> + <script>console.warn("test")</script> + there. +
    + `); + }); +}); + +describe('trans bracketNotation', () => { + function TestComponent() { + const numOfItems = 4; + return ; + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + 4 +
    + `); + }); +}); + +describe('trans otherNotation', () => { + function TestComponent() { + const numOfItems = 4; + return ; + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + #$?count?$# +
    + `); + }); +}); + +describe('trans testTransKey1 singular', () => { + function TestComponent() { + const numOfItems = 1; + return ( + + {{ numOfItems }} items matched. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + 1 + item matched. +
    + `); + }); +}); + +describe('trans testTransKey1 plural', () => { + function TestComponent() { + const numOfItems = 10; + return ( + + {{ numOfItems }} items matched. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + 10 + items matched. +
    + `); + }); +}); + +describe('trans testTransKey2', () => { + function TestComponent() { + const numOfItems = 10; + return ( + + {{ numOfItems }} items matched. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + 10 + + items matched. +
    + `); + }); +}); + +describe('trans testTransKey3', () => { + function TestComponent() { + const numOfItems = 10; + return ( + + Result: {{ numOfItems }} items matched. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Result: + + 10 + + items matched. +
    + `); + }); +}); + +describe('trans complex', () => { + function TestComponent() { + const count = 10; + const name = 'Jan'; + // prettier-ignore + return ( + + Hello {{ name }}, you have {{ count }} message. Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Hello + + Jan + + , you have + 10 + messages. Open + + here + + . +
    + `); + }); +}); + +describe('trans complex - count only in props', () => { + function TestComponent() { + const count = 10; + const name = 'Jan'; + // prettier-ignore + return ( + + Hello {{ name }}, you have {{n: count}} message. Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Hello + + Jan + + , you have + 10 + messages. Open + + here + + . +
    + `); + }); +}); + +describe('trans complex v2 no extra pseudo elements for interpolation', () => { + function TestComponent() { + const count = 10; + const name = 'Jan'; + // prettier-ignore + return ( + + Hello {{ name }}, you have {{ count }} message. Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Hello + + Jan + + , you have 10 messages. Open + + here + + . +
    + `); + }); +}); + +describe('trans with t as prop', () => { + function TestComponent({ t, cb }) { + const customT = (...args) => { + if (cb) cb(); + return t(...args); + }; + return ( + + Open here. + + ); + } + + it('should use props t', () => { + let usedCustomT = false; + const cb = () => { + usedCustomT = true; + }; + + const HocElement = withTranslation(['translation'], {})(TestComponent); + + render(); + expect(usedCustomT).toBe(true); + }); + + it('should not pass t to HTML element', () => { + const HocElement = withTranslation(['translation'], {})(TestComponent); + + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + there + + . +
    + `); + }); +}); + +describe('trans with empty content', () => { + function TestComponent() { + return ; + } + it('should render an empty string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(`
    `); + }); +}); + +describe('trans with only content from translation file - no children', () => { + function TestComponent() { + return ; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + test +
    + `); + }); +}); + +describe('trans with only html content from translation file - no children', () => { + function TestComponent() { + return ; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + Go + + +
    + there. +
    + `); + }); +}); + +describe('trans should not break on invalid node from translations', () => { + function TestComponent() { + return ; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + <hello +
    + `); + }); +}); + +describe('trans should not break on invalid node from translations - part2', () => { + function TestComponent() { + return ; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + <hello> +
    + `); + }); +}); + +describe('trans should work with misleading overloaded empty elements in components', () => { + function TestComponent() { + return ( + , bold: }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Hi Fritz, +
    + and + + welcome + +
    + `); + }); +}); + +describe('trans should work with defaultVariables', () => { + function TestComponent() { + return ; + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + add first SECOND +
    + `); + }); +}); + +describe('trans should work with lowercase elements in components', () => { + function TestComponent() { + return ( + dummy }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + click + + here + + for more +
    + `); + }); +}); + +describe('trans should work with uppercase elements in components', () => { + function TestComponent() { + return ( + dummy }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + click + + here + + for more +
    + `); + }); +}); + +describe('trans should work with selfclosing elements in components', () => { + function TestComponent() { + return ( + These children will be preserved
    , + }} + /> + ); + } + it('should render translated string', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + interpolated component: +
    + These children will be preserved +
    +
    + `); + }); +}); + +describe('trans should work with self closing elements with react components', () => { + function Component({ children }) { + return
    {children}
    ; + } + it('should render component children', () => { + const { container } = render( + These children will be preserved, + }} + />, + ); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + interpolated component: +
    + These children will be preserved +
    +
    + `); + }); + it('should render Link component children', () => { + const { container } = render( + These children will be preserved, + }} + />, + ); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + interpolated component: + + These children will be preserved + +
    + `); + }); + it('should render anchor tag children', () => { + const { container } = render( + These children will be preserved, + }} + />, + ); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + interpolated component: + + These children will be preserved + +
    + `); + }); +}); + +describe('trans with null child', () => { + function TestComponent() { + return ( + + Open here.{null} + + ); + } + + it('should ignore the null child and render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + there + + . +
    + `); + }); +}); + +describe('trans with wrapTextNodes', () => { + let orgValue; + beforeAll(() => { + orgValue = i18n.options.react.transWrapTextNodes; + i18n.options.react.transWrapTextNodes = 'span'; + }); + afterAll(() => { + i18n.options.react.transWrapTextNodes = orgValue; + }); + + function TestComponent() { + return ( + + Open here. + + ); + } + + it('should wrap text nodes accordingly', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + + Go + + + + there + + + + . + +
    + `); + }); +}); + +describe('trans does ignore user defined values when parsing', () => { + function TestComponent({ value }) { + return ( + + This is just some {{ value }} text + + ); + } + + it('should escape value with angle brackets', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + This is + + just + + some <weird> text +
    + `); + }); +}); + +describe('trans should allow escaped html', () => { + function TestComponent() { + return ( + ]} shouldUnescape /> + ); + } + + it('should unescape <   & > to < SPACE & >', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Escaped html should unescape correctly + + < &> + + . +
    + `); + }); +}); + +describe('trans with custom unescape', () => { + let orgValue; + beforeAll(() => { + orgValue = i18n.options.react.unescape; + i18n.options.react.unescape = (text) => text.replace('­', '\u00AD'); + }); + + afterAll(() => { + i18n.options.react.unescape = orgValue; + }); + + it('should allow unescape override', () => { + function TestComponent() { + return ( + ]} + shouldUnescape + /> + ); + } + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Text should be passed through custom unescape + + \u00AD + +
    + `); + }); + + it('should allow unescape override again', () => { + function TestComponent() { + return ; + } + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Vertrauens\u00ADkennwert +
    + `); + }); +}); + +describe('trans with context via tOptions', () => { + const tOptions = { context: 'home' }; + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + home + + . +
    + `); + expect(tOptions).to.have.property('context', 'home'); + }); +}); + +describe('trans with context property', () => { + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + home + + . +
    + `); + }); +}); + +describe('trans with formatting', () => { + function TestComponent({ parent }) { + return ( + <> + + + + {{ foo: 1234 }} + + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.childNodes[0]).toMatchInlineSnapshot(` +
    + Value as is: 1234 +
    + `); + expect(container.childNodes[1]).toMatchInlineSnapshot(` +
    + Treat value as number: 1234 +
    + `); + expect(container.childNodes[2]).toMatchInlineSnapshot(` +
    + Treat value as number: 1234 +
    + `); + }); +}); + +describe('trans with formatting with alwaysFormat', () => { + let newI18n; + + beforeEach(() => { + newI18n = i18n.createInstance(); + newI18n.init({ + interpolation: { + alwaysFormat: true, + escapeValue: false, + format: (value) => `(formatted ${value})`, + }, + }); + }); + + function TestComponent({ parent }) { + const name = 'Fritz'; + return ( + + number: {'{{count, INTEGER}}'} +
    + name: {{ name }} +
    + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container).toMatchInlineSnapshot(` +
    + number: (formatted 3) +
    + name: (formatted Fritz) +
    + `); + }); +}); + +describe('trans with undefined context property', () => { + function TestComponent({ parent }) { + return ( + + Open here. + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go + + there + + . +
    + `); + }); +}); + +describe('trans with context property but no children', () => { + function TestComponent({ parent }) { + return ; + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go to Switzerland. +
    + `); + }); +}); + +describe('trans with undefined context property but no children', () => { + function TestComponent({ parent }) { + return ; + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go . +
    + `); + }); +}); + +describe('trans with different context property but no children', () => { + function TestComponent({ parent }) { + return ; + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + Go somewhere. +
    + `); + }); +}); + +describe('trans with defaults property and children', () => { + function TestComponent() { + return ( + + You have {{ count: 3 }} message + + ); + } + + it('should render correct content', () => { + const { container } = render(); + expect(container.firstChild).toMatchInlineSnapshot(` +
    + You have 3 messages +
    + `); + }); +}); diff --git a/test/trans.spec.js b/test/trans.spec.js deleted file mode 100644 index cc554201f..000000000 --- a/test/trans.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -jest.unmock('../src/Trans'); -import React from 'react'; -import PropTypes from 'prop-types'; -import Trans from '../src/Trans'; - -describe('trans', () => { - it('should have some stuff', () => { - expect(Trans.contextTypes.i18n) - .toBe(PropTypes.object.isRequired); - expect(Trans.propTypes.i18n) - .toBe(PropTypes.object); - expect(Trans.propTypes.t) - .toBe(PropTypes.func); - }); -}); diff --git a/test/translate.render.nsMode.spec.js b/test/translate.render.nsMode.spec.js deleted file mode 100644 index 73fc32e02..000000000 --- a/test/translate.render.nsMode.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import { shallow, render, mount } from 'enzyme'; -import i18n from './i18n'; -import translate from '../src/translate'; - -const newI18n = i18n.createInstance(); -newI18n - .init({ - lng: 'en', - fallbackLng: 'en', - - resources: { - en: { - a: { - key1: 'test a', - }, - b: { - key2: 'test b' - } - } - }, - - interpolation: { - escapeValue: false, // not needed for react!! - } - }) - -const context = { - i18n: newI18n -}; - -describe.only('translate wait', () => { - const TestElement = ({ t }) => { - return ( -
    {t('key2')}
    - ); - } - - it('should fallback for correct translation', () => { - const HocElement = translate(['a', 'b'], { nsMode: 'fallback' })(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains(
    test b
    )).toBe(true); - }); - - - it('should global wait for correct translation', () => { - // reinit global - newI18n.init({ react: { nsMode: 'fallback' }}); - - const HocElement = translate(['a', 'b'], { /*nsMode: fallback*/ })(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains(
    test b
    )).toBe(true); - }); -}); diff --git a/test/translate.render.spec.js b/test/translate.render.spec.js deleted file mode 100644 index 278cbcc11..000000000 --- a/test/translate.render.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { shallow, render, mount } from 'enzyme'; -import i18n from './i18n'; -import translate from '../src/translate'; - -const context = { - i18n -}; - -describe('translate', () => { - const TestElement = ({ t }) => { - return ( -
    {t('key1')}
    - ); - } - - it('should render correct translation', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains(
    test
    )).toBe(true); - }); - - it('should bind / unbind', () => { - const HocElement = translate(['translation'], {})(TestElement); - - const wrapper = mount(, context); - // console.log(wrapper.debug()); - - // has bound events - expect(i18n.observers.languageChanged.length).toBe(2) - expect(i18n.observers.loaded.length).toBe(2) - - // unbind after unmount - wrapper.unmount() - expect(i18n.observers.languageChanged.length).toBe(1) - expect(i18n.observers.loaded.length).toBe(1) - }); -}); diff --git a/test/translate.render.wait.spec.js b/test/translate.render.wait.spec.js deleted file mode 100644 index b194efbe5..000000000 --- a/test/translate.render.wait.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { shallow, render, mount } from 'enzyme'; -import i18n from './i18n'; -import BackendMock from './backendMock'; -import translate from '../src/translate'; - -const newI18n = i18n.createInstance(); -const backend = new BackendMock(); -newI18n - .use(backend) - .init({ - lng: 'en', - fallbackLng: 'en', - - interpolation: { - escapeValue: false, // not needed for react!! - } - }) - -const context = { - i18n: newI18n -}; - -describe('translate wait', () => { - const TestElement = ({ t }) => { - return ( -
    {t('key1')}
    - ); - } - - it('should wait for correct translation', () => { - const HocElement = translate(['common'], { wait: true })(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains(
    test
    )).toBe(false); - - backend.flush(); - wrapper.update(); - - setTimeout(() => { - expect(wrapper.contains(
    test
    )).toBe(true); - }, 50); - }); - - - it('should global wait for correct translation', () => { - // reinit global - newI18n.init({ react: { wait: true }}); - - const HocElement = translate(['common'], { /* not use this -> wait: true */ })(TestElement); - - const wrapper = mount(, { context }); - // console.log(wrapper.debug()); - expect(wrapper.contains(
    test
    )).toBe(false); - - backend.flush(); - wrapper.update(); - - setTimeout(() => { - expect(wrapper.contains(
    test
    )).toBe(true); - }, 50); - }); -}); diff --git a/test/translate.spec.js b/test/translate.spec.js deleted file mode 100644 index 8c30d9d4b..000000000 --- a/test/translate.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -jest.unmock('../src/translate'); -import React from 'react'; -import PropTypes from 'prop-types'; -import translate from '../src/translate'; - -describe('translate', () => { - it('should return a function', () => { - const wrap = translate('namespace', {}); - expect(typeof wrap).toBe('function'); - }); - it('that function should wrap my element and provide many things', () => { - const wrap = translate(['ns1', 'ns2'], {}); - const Elem = React.createFactory('Elem'); - Elem.displayName = 'Elem'; - Elem.NOT_KNOWN_REACT_STATIC = 'IS HOISTED ?'; - const wrapped = wrap(Elem); - expect(wrapped.WrappedComponent).toBe(Elem); - expect(wrapped.contextTypes.i18n).toBe(PropTypes.object); - expect(wrapped.displayName).toBe('Translate(Elem)'); - expect(wrapped.namespaces.length).toBe(2); - expect(wrapped.namespaces[0]).toBe('ns1'); - expect(wrapped.namespaces[1]).toBe('ns2'); - expect(wrapped.NOT_KNOWN_REACT_STATIC).toBe('IS HOISTED ?'); - }); - it('should do things', () => { - const wrap = translate(['ns1', 'ns2'], { withRef: true }); - const Elem = React.createFactory('Elem'); - Elem.displayName = 'Elem'; - const wrapped = wrap(Elem); - const props = { initialI18nStore: {}, initialLanguage: 'en' }; - const t = (message) => message; - const context = { - i18n: { - options: { - defaultNS: 'defaultNS' - }, - services: { - resourceStore: { - data: {} - } - }, - changeLanguage: () => {}, - getFixedT(p, ns) { - return t; - } - } - }; - const instance = new wrapped(props, context); - - expect(typeof instance.getWrappedInstance).toBe('function'); - }); - it('that we can set i18n', () => { - const t = (message) => message; - const i18n = { - options: { - defaultNS: 'defaultNS' - }, - services: { - resourceStore: { - data: {} - } - }, - changeLanguage: () => {}, - getFixedT(p, ns) { - return t; - } - }; - translate.setI18n(i18n); - const wrap = translate(['ns1', 'ns2'], {}); - const Elem = React.createFactory('Elem'); - const wrapped = wrap(Elem); - const instance = new wrapped({}, {}); - - expect(instance.i18n).toBe(i18n); - }); - -}); diff --git a/test/typescript/custom-types/Trans.test.tsx b/test/typescript/custom-types/Trans.test.tsx new file mode 100644 index 000000000..4a0ad36fe --- /dev/null +++ b/test/typescript/custom-types/Trans.test.tsx @@ -0,0 +1,120 @@ +import { describe, it, expectTypeOf } from 'vitest'; +import * as React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +describe('', () => { + describe('default namespace', () => { + it('standard usage', () => { + // foo + expectTypeOf(Trans).toBeCallableWith({ + i18nKey: 'foo', + }); + }); + + it('should trow if key do not exist', () => { + expectTypeOf>() + .toHaveProperty('i18nKey') + .extract<'Nope'>() + // @ts-expect-error + .toMatchTypeOf<'Nope'>(); + }); + }); + + describe('named namespace', () => { + it('standard usage', () => { + // foo + expectTypeOf(Trans).toBeCallableWith({ + ns: 'custom', + i18nKey: 'foo', + }); + }); + + it('should trow if namespace do not exist', () => { + expectTypeOf>() + .toHaveProperty('ns') + .extract<'Nope'>() + // @ts-expect-error + .toMatchTypeOf<'Nope'>(); + }); + }); + + describe('array namespace', () => { + it('should work with array namespace', () => { + // + expectTypeOf(Trans).toBeCallableWith({ + ns: ['alternate', 'custom'], + i18nKey: ['alternate:baz', 'custom:bar'], + }); + }); + + it('should throw error if key is not present inside namespaces', () => { + // + expectTypeOf(Trans).toBeCallableWith({ + ns: ['alternate', 'custom'], + // @ts-expect-error + i18nKey: ['alternate:baz', 'custom:bar2'], + }); + }); + }); + + describe('usage with `t` function', () => { + it('should work when providing `t` function', () => { + const { t } = useTranslation('alternate'); + + // foo + expectTypeOf(Trans).toBeCallableWith({ + t, + i18nKey: 'foobar.barfoo', + }); + }); + + it('should work when providing `t` function with a prefix', () => { + const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); + + // foo + expectTypeOf>().toBeCallableWith( + { + t, + i18nKey: 'deeper.deeeeeper', + }, + ); + }); + + it('should throw error with `t` function with key prefix and wrong `i18nKey`', () => { + const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); + + // foo + expectTypeOf>().toBeCallableWith( + { + t, + // @ts-expect-error + i18nKey: 'xxx', + }, + ); + }); + }); + + describe('interpolation', () => { + it('should work with text and interpolation', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: <>foo {{ var: '' }}, + }); + }); + + it('should work with Interpolation in HTMLElement', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: ( + <> + foo {{ var: '' }} + + ), + }); + }); + + it('should work with text and interpolation as children of an HTMLElement', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: foo {{ var: '' }}, + }); + }); + }); +}); diff --git a/test/typescript/custom-types/TransWithoutContext.test.tsx b/test/typescript/custom-types/TransWithoutContext.test.tsx new file mode 100644 index 000000000..707635d13 --- /dev/null +++ b/test/typescript/custom-types/TransWithoutContext.test.tsx @@ -0,0 +1,167 @@ +import { describe, it, expectTypeOf, assertType } from 'vitest'; +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Trans } from '../../../TransWithoutContext'; + +describe('', () => { + describe('default namespace', () => { + it('standard usage', () => { + // foo + expectTypeOf(Trans).toBeCallableWith({ + i18nKey: 'foo', + }); + }); + + it('should trow if key do not exist', () => { + expectTypeOf>() + .toHaveProperty('i18nKey') + .extract<'Nope'>() + // @ts-expect-error + .toMatchTypeOf<'Nope'>(); + }); + }); + + describe('named namespace', () => { + it('standard usage', () => { + // foo + expectTypeOf(Trans).toBeCallableWith({ + ns: 'custom', + i18nKey: 'foo', + }); + }); + + it('should trow if namespace do not exist', () => { + expectTypeOf>() + .toHaveProperty('ns') + .extract<'Nope'>() + // @ts-expect-error + .toMatchTypeOf<'Nope'>(); + }); + }); + + describe('array namespace', () => { + it('should work with array namespace', () => { + // + expectTypeOf(Trans).toBeCallableWith({ + ns: ['alternate', 'custom'], + i18nKey: ['alternate:baz', 'custom:bar'], + }); + }); + + it('should throw error if key is not present inside namespaces', () => { + // + expectTypeOf(Trans).toBeCallableWith({ + ns: ['alternate', 'custom'], + // @ts-expect-error + i18nKey: ['alternate:baz', 'custom:bar2'], + }); + }); + }); + + describe('usage with `t` function', () => { + it('should work when providing `t` function', () => { + const { t } = useTranslation('alternate'); + + // foo + expectTypeOf(Trans).toBeCallableWith({ + t, + i18nKey: 'foobar.barfoo', + }); + }); + + it('should work when providing `t` function with a prefix', () => { + const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); + + // foo + expectTypeOf>().toBeCallableWith( + { + t, + i18nKey: 'deeper.deeeeeper', + }, + ); + }); + + it('should throw error with `t` function with key prefix and wrong `i18nKey`', () => { + const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); + + // foo + expectTypeOf>().toBeCallableWith( + { + t, + // @ts-expect-error + i18nKey: 'xxx', + }, + ); + }); + }); + + describe('interpolation', () => { + it('should work with text and interpolation', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: <>foo {{ var: '' }}, + }); + }); + + it('should work with Interpolation in HTMLElement', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: ( + <> + foo {{ var: '' }} + + ), + }); + }); + + it('should work with text and interpolation as children of an HTMLElement', () => { + expectTypeOf(Trans).toBeCallableWith({ + children: foo {{ var: '' }}, + }); + }); + }); + + describe('usage with context', () => { + it('should work with default namespace', () => { + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with `ns` prop', () => { + assertType(); + + assertType( + // @ts-expect-error should throw error when context is not valid + , + ); + }); + + it('should work with default `t` function', () => { + const { t } = useTranslation(); + + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with custom `t` function', () => { + const { t } = useTranslation('context'); + + assertType(); + + // @ts-expect-error should throw error when context is not valid + assertType(); + }); + + it('should work with `ns` prop and `count` prop', () => { + const { t } = useTranslation('plurals'); + assertType(); + }); + + it('should work with custom `t` function and `count` prop', () => { + const { t } = useTranslation('plurals'); + assertType(); + }); + }); +}); diff --git a/test/typescript/custom-types/Translation.test.ts b/test/typescript/custom-types/Translation.test.ts new file mode 100644 index 000000000..701052d3d --- /dev/null +++ b/test/typescript/custom-types/Translation.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expectTypeOf } from 'vitest'; +import { Translation } from 'react-i18next'; + +describe('', () => { + it('should work with default namespace', () => { + expectTypeOf(Translation).toBeCallableWith({ children: (t) => t('foo') }); + }); + + it('should work with named default namespace', () => { + expectTypeOf(Translation<'custom'>).toBeCallableWith({ children: (t) => t('foo') }); + }); + + it('should work with named namespace', () => { + expectTypeOf(Translation<'alternate'>).toBeCallableWith({ children: (t) => t('baz') }); + }); + + it('should work with namespace array', () => { + expectTypeOf(Translation<['alternate', 'custom']>).toBeCallableWith({ + children: (t) => `${t('alternate:baz')} ${t('custom:foo')}`, + }); + }); + + it('should throw error when namespace does not exist', () => { + // @ts-expect-error + expectTypeOf(Translation<'fake'>).toBeFunction(); + }); + + it('should throw error when namespace does not contain given `t` key', () => { + expectTypeOf(Translation<'custom'>).toBeCallableWith({ + // @ts-expect-error + children: (t) => t('fake'), + }); + }); + + it('should throw error when namespace does not contain given `t` key (with namespace as prefix)', () => { + expectTypeOf(Translation<'custom'>).toBeCallableWith({ + // @ts-expect-error + children: (t) => t('custom:fake'), + }); + }); +}); diff --git a/test/typescript/custom-types/i18next.d.ts b/test/typescript/custom-types/i18next.d.ts new file mode 100644 index 000000000..4420198d2 --- /dev/null +++ b/test/typescript/custom-types/i18next.d.ts @@ -0,0 +1,46 @@ +import 'i18next'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'custom'; + allowObjectInHTMLChildren: true; + resources: { + custom: { + foo: 'foo'; + bar: 'bar'; + + some: 'some'; + some_me: 'some context'; + }; + + alternate: { + baz: 'baz'; + foobar: { + barfoo: 'barfoo'; + deep: { + deeper: { + deeeeeper: 'foobar'; + }; + }; + }; + }; + + plurals: { + foo_zero: 'foo'; + foo_one: 'foo'; + foo_two: 'foo'; + foo_many: 'foo'; + foo_other: 'foo'; + }; + + context: { + dessert_cake: 'a nice cake'; + dessert_muffin_one: 'a nice muffin'; + dessert_muffin_other: '{{count}} nice muffins'; + + beverage: 'beverage'; + beverage_beer: 'beer'; + }; + }; + } +} diff --git a/test/typescript/custom-types/tsconfig.json b/test/typescript/custom-types/tsconfig.json new file mode 100644 index 000000000..6c88073b9 --- /dev/null +++ b/test/typescript/custom-types/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./**/*"], + "exclude": [] +} diff --git a/test/typescript/custom-types/useTranslation.test.ts b/test/typescript/custom-types/useTranslation.test.ts new file mode 100644 index 000000000..6dac6277d --- /dev/null +++ b/test/typescript/custom-types/useTranslation.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expectTypeOf, assertType } from 'vitest'; +import { useTranslation } from 'react-i18next'; +import { TFunction, i18n } from 'i18next'; + +describe('useTranslation', () => { + it('should provide result with both object and array', () => { + const result = useTranslation(); + + expectTypeOf(result).toMatchTypeOf<[TFunction, i18n, boolean]>(); + expectTypeOf(result).toHaveProperty('ready').toBeBoolean(); + expectTypeOf(result).toHaveProperty('t').toBeFunction(); + expectTypeOf(result).toHaveProperty('i18n').toBeObject(); + }); + + describe('default namespace', () => { + it('should work with default namespace', () => { + const [t] = useTranslation(); + + expectTypeOf(t).toBeCallableWith('foo'); + }); + + it('should work with default named namespace', () => { + const [t] = useTranslation('custom'); + + expectTypeOf(t).toBeCallableWith('bar'); + }); + }); + + describe('named namespace', () => { + it('should work with named namespace', () => { + const [t] = useTranslation('alternate'); + + expectTypeOf(t).toBeCallableWith('baz'); + }); + + it('should throw error when namespace does not exist', () => { + // @ts-expect-error + const [t] = useTranslation('fake'); + }); + + it('should throw error when key does not exist inside namespace', () => { + const [t] = useTranslation('custom'); + // @ts-expect-error + assertType(t('fake')); + }); + }); + + describe('namespace as array', () => { + it('should work with const namespaces', () => { + const [t] = useTranslation(['alternate', 'custom']); + + expectTypeOf(t('alternate:baz')).toEqualTypeOf<'baz'>(); + expectTypeOf(t('baz', { ns: 'alternate' })).toEqualTypeOf<'baz'>(); + expectTypeOf(t('custom:foo')).toEqualTypeOf<'foo'>(); + expectTypeOf(t('foo', { ns: 'custom' })).toEqualTypeOf<'foo'>(); + }); + + it('should work with const namespaces', () => { + const namespaces = ['alternate', 'custom'] as const; + const [t] = useTranslation(namespaces); + + expectTypeOf(t('alternate:baz')).toEqualTypeOf<'baz'>(); + expectTypeOf(t('baz', { ns: 'alternate' })).toEqualTypeOf<'baz'>(); + expectTypeOf(t('custom:foo')).toEqualTypeOf<'foo'>(); + expectTypeOf(t('foo', { ns: 'custom' })).toEqualTypeOf<'foo'>(); + }); + + it('should throw error when using wrong key', () => { + const [t] = useTranslation(['custom']); + // @ts-expect-error + assertType(t('custom:fake')); + // @ts-expect-error + assertType(t('fake', { ns: 'custom' })); + }); + }); + + describe('with `keyPrefix`', () => { + it('should provide string keys', () => { + const [t] = useTranslation('alternate', { keyPrefix: 'foobar' }); + + expectTypeOf(t('barfoo')).toEqualTypeOf<'barfoo'>(); + }); + + it('should work with deeper objects', () => { + const [t] = useTranslation('alternate', { keyPrefix: 'foobar.deep' }); + + expectTypeOf(t('deeper', { returnObjects: true })).toEqualTypeOf<{ + deeeeeper: 'foobar'; + }>(); + expectTypeOf(t('deeper.deeeeeper')).toEqualTypeOf<'foobar'>(); + }); + + it('should throw error with an invalid keyPrefix', () => { + // @ts-expect-error + useTranslation('alternate', { keyPrefix: 'abc' }); + }); + + it('should throw error with an invalid key', () => { + const [t] = useTranslation('alternate', { keyPrefix: 'foobar' }); + // @ts-expect-error + assertType(t('abc')); + }); + }); + + it('should work with json format v4 plurals', () => { + const [t] = useTranslation('plurals'); + + expectTypeOf(t('foo')).toEqualTypeOf<'foo'>(); + expectTypeOf(t('foo_one')).toEqualTypeOf<'foo'>(); + }); +}); diff --git a/test/typescript/misc/I18nextProvider.test.tsx b/test/typescript/misc/I18nextProvider.test.tsx new file mode 100644 index 000000000..80ade73c5 --- /dev/null +++ b/test/typescript/misc/I18nextProvider.test.tsx @@ -0,0 +1,20 @@ +import { describe, expectTypeOf } from 'vitest'; +import i18next from 'i18next'; +import { I18nextProvider } from 'react-i18next'; + +describe('', () => { + it('basic usage', () => { + expectTypeOf(I18nextProvider).toBeCallableWith({ + i18n: i18next, + children: 'Foo', + }); + }); + + it('should accept `defaultNS`', () => { + expectTypeOf(I18nextProvider).toBeCallableWith({ + i18n: i18next, + defaultNS: 'defaultNS', + children: 'Foo', + }); + }); +}); diff --git a/test/typescript/misc/Trans.test.tsx b/test/typescript/misc/Trans.test.tsx new file mode 100644 index 000000000..ed8f4bc5b --- /dev/null +++ b/test/typescript/misc/Trans.test.tsx @@ -0,0 +1,134 @@ +import { assertType, describe, expectTypeOf, it } from 'vitest'; +import * as React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +describe('', () => { + it('should work with string as `children`', () => { + expectTypeOf(Trans).toBeCallableWith({ children: 'Foo' }); + }); + + it('should work with HTMLNode as children', () => { + expectTypeOf(Trans).toBeCallableWith({ children:
    }); + }); + + describe('should work with `components`', () => { + it('inline', () => { + expectTypeOf(Trans).toBeCallableWith({ + components: [
    ], + children: 'Foo', + }); + }); + + it('inline const variable', () => { + const components = [
    ] as const; + + expectTypeOf(Trans).toBeCallableWith({ + components, + children: 'Foo', + }); + }); + + it('JSX', () => { + expectTypeOf(Trans).toBeCallableWith({ + components: { + Btn: