Repository: Romanchuk/angular-i18next Branch: master Commit: be47b2b66485 Files: 137 Total size: 219.4 KB Directory structure: gitextract_w52vdt87/ ├── .cleandir.sh ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── static.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CHANGELOG.md ├── FUNDING.yml ├── LICENSE ├── README.md ├── README_DEPRECATED.md ├── apps/ │ └── angular-i18next-demo/ │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src/ │ │ ├── app/ │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.config.server.ts │ │ │ ├── app.config.ts │ │ │ ├── app.routes.server.ts │ │ │ ├── app.routes.ts │ │ │ ├── content/ │ │ │ │ ├── access-denied/ │ │ │ │ │ ├── access-denied.component.html │ │ │ │ │ └── access-denied.component.ts │ │ │ │ ├── simple-demo.component.html │ │ │ │ └── simple-demo.component.ts │ │ │ ├── features/ │ │ │ │ └── rich_form_feature/ │ │ │ │ ├── rich-form.component.html │ │ │ │ ├── rich-form.component.ts │ │ │ │ └── rich-form.model.ts │ │ │ ├── i18next.options.ts │ │ │ └── structure/ │ │ │ ├── app-error.component.html │ │ │ ├── app-error.component.ts │ │ │ ├── app-footer.component.html │ │ │ ├── app-footer.component.ts │ │ │ ├── app-header.component.html │ │ │ ├── app-header.component.ts │ │ │ └── header-controls/ │ │ │ ├── header.language.component.html │ │ │ └── header.language.component.ts │ │ ├── assets/ │ │ │ ├── .gitkeep │ │ │ └── ng-validation.css │ │ ├── environments/ │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.html │ │ ├── lib/ │ │ │ └── validation/ │ │ │ ├── services/ │ │ │ │ └── ValidationDirtyChecker.ts │ │ │ └── validators/ │ │ │ ├── ArrayValidators.js │ │ │ ├── ArrayValidators.ts │ │ │ ├── ConditionalValidator.js │ │ │ └── ConditionalValidator.ts │ │ ├── locales/ │ │ │ ├── en.error.json │ │ │ ├── en.feature.rich_form.json │ │ │ ├── en.translation.json │ │ │ ├── en.validation.json │ │ │ ├── ru.error.json │ │ │ ├── ru.feature.rich_form.json │ │ │ ├── ru.translation.json │ │ │ └── ru.validation.json │ │ ├── main.server.ts │ │ ├── main.ts │ │ ├── server.ts │ │ ├── styles.css │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ ├── tsconfig.server.json │ └── tsconfig.spec.json ├── decorate-angular-cli.js ├── ecosystem.config.js ├── jest.config.ts ├── jest.preset.js ├── libs/ │ └── angular-i18next/ │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── forms/ │ │ ├── ng-package.json │ │ └── src/ │ │ ├── components/ │ │ │ └── validation-message.component.ts │ │ ├── directives/ │ │ │ └── validation-message.directive.ts │ │ ├── models.ts │ │ └── public_api.ts │ ├── jest.config.ts │ ├── ng-package.json │ ├── package.json │ ├── postinstall.js │ ├── project.json │ ├── src/ │ │ ├── .eslintrc.json │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── I18NextErrorHandlingStrategies.ts │ │ │ ├── I18NextEvents.ts │ │ │ ├── I18NextLoadResult.ts │ │ │ ├── I18NextModuleParams.ts │ │ │ ├── index.ts │ │ │ ├── interpolation.ts │ │ │ ├── models.ts │ │ │ ├── module.ts │ │ │ ├── namespace.resolver.ts │ │ │ ├── namespaces.guard.ts │ │ │ ├── pipes/ │ │ │ │ ├── i18next-cap.pipe.ts │ │ │ │ ├── i18next-eager.pipe.ts │ │ │ │ ├── i18next-format.pipe.ts │ │ │ │ └── i18next.pipe.ts │ │ │ ├── provider.ts │ │ │ ├── provider.utils.ts │ │ │ ├── services/ │ │ │ │ ├── i18next-title.ts │ │ │ │ ├── i18next.service.ts │ │ │ │ ├── translation.events.ts │ │ │ │ └── translation.service.ts │ │ │ └── tokens.ts │ │ ├── test-setup.ts │ │ └── tests/ │ │ ├── module/ │ │ │ └── module.spec.ts │ │ ├── pipes/ │ │ │ ├── I18NextEagerPipe.spec.ts │ │ │ └── I18NextPipe.spec.ts │ │ ├── projectTests/ │ │ │ ├── project.component.ts │ │ │ └── projectTests.spec.ts │ │ ├── provider/ │ │ │ └── provider.spec.ts │ │ ├── service/ │ │ │ └── I18NextService.spec.ts │ │ └── setup.ts │ ├── ssr/ │ │ ├── ng-package.json │ │ └── src/ │ │ ├── provider.ssr.ts │ │ └── public_api.ts │ ├── testing/ │ │ ├── ng-package.json │ │ └── src/ │ │ ├── mock.service.ts │ │ ├── provider.ts │ │ └── public_api.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package.json ├── tools/ │ └── tsconfig.tools.json ├── tsconfig.base.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cleandir.sh ================================================ echo Cleaning up project directory... rm -rf node_modules dist build coverage src/**/*.d.ts src/**/*.js src/**/*.js.map src/**/*.metadata.json ================================================ FILE: .editorconfig ================================================ # Editor configuration, see http://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ node_modules ================================================ FILE: .eslintrc.json ================================================ { "root": true, "ignorePatterns": ["**/*"], "plugins": ["@nx"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { "@nx/enforce-module-boundaries": [ "error", { "enforceBuildableLibDependency": true, "allow": [], "depConstraints": [ { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } ] } ] } }, { "files": ["*.ts", "*.tsx"], "extends": ["plugin:@nx/typescript"], "rules": {} }, { "files": ["*.js", "*.jsx"], "extends": ["plugin:@nx/javascript"], "rules": {} } ] } ================================================ FILE: .github/FUNDING.yml ================================================ custom: ["https://www.paypal.com/paypalme2/sergeyromanchuk/10USD"] ================================================ FILE: .github/workflows/static.yml ================================================ # Simple workflow for deploying static content to GitHub Pages name: Deploy static content to Pages on: # Runs on pushes targeting the default branch push: branches: ["master"] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow one concurrent deployment concurrency: group: "pages" cancel-in-progress: true jobs: # Single deploy job since we're just deploying deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest strategy: matrix: node-version: [22.13.x] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run test - run: npm run build - run: npm run prepare:demo - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: './dist/angular-i18next-demo/browser' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ # See http://help.github.com/ignore-files/ for more about ignoring files. # compiled output dist /tmp /out-tsc # dependencies node_modules # IDEs and editors /.idea .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # IDE - VSCode .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # misc /.sass-cache /connect.lock /coverage /libpeerconnection.log npm-debug.log yarn-error.log testem.log /typings # System Files .DS_Store Thumbs.db .angular .nx ================================================ FILE: .prettierignore ================================================ # Add files here to ignore them from prettier formatting /dist /coverage /.nx/cache .angular ================================================ FILE: .prettierrc ================================================ { "singleQuote": true } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "angular.ng-template", "nrwl.angular-console", "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", "dbaeumer.vscode-eslint", "github.vscode-github-actions" ] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "1.0.0", "configurations": [ { "type": "node", "request": "launch", "name": "Jest: current file", //"env": { "NODE_ENV": "test" }, "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["${fileBasenameNoExtension}", "--config", "jest.config.ts"], "console": "integratedTerminal", "windows": { "program": "${workspaceFolder}/node_modules/jest/bin/jest" } } ] } ================================================ FILE: .vscode/settings.json ================================================ // Place your settings in this file to overwrite default and user settings. { "search.exclude": { "coverage": true, "dist": true, "build": true }, "typescript.tsdk": "node_modules\\typescript\\lib" } ================================================ FILE: CHANGELOG.md ================================================ # [10.3.0](https://github.com/Romanchuk/angular-i18next/compare/v10.3.0-0...v10.3.0) (2021-06-15) # [10.3.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.2.0...v10.3.0-0) (2021-06-15) # [10.2.0](https://github.com/Romanchuk/angular-i18next/compare/v10.2.0-0...v10.2.0) (2021-05-12) # [10.2.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.1.0...v10.2.0-0) (2021-05-12) ### Features * i18next v20+ support ([0327a7c](https://github.com/Romanchuk/angular-i18next/commit/0327a7c9f35140f0c8e098d9d1528b6e7303a8d0)) # [10.1.0](https://github.com/Romanchuk/angular-i18next/compare/v10.1.0-0...v10.1.0) (2021-03-01) # [10.1.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.0.1...v10.1.0-0) (2021-03-01) ### Bug Fixes * **I18NextEagerPipe:** ensure changing PipeOptions returns correct translated value a not cached one with different PipeOptions but same key ([4a6d375](https://github.com/Romanchuk/angular-i18next/commit/4a6d375181dda41399c58f7644b97d3755acf84f)) ## [10.0.1](https://github.com/Romanchuk/angular-i18next/compare/v10.0.1-beta...v10.0.1) (2020-12-21) ## [10.0.1-beta](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0...v10.0.1-beta) (2020-12-21) # [10.0.0](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-2...v10.0.0) (2020-07-06) # [10.0.0-2](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-1...v10.0.0-2) (2020-07-06) # [10.0.0-1](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-0...v10.0.0-1) (2020-07-06) # [10.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v9.0.1...v10.0.0-0) (2020-07-06) ## [9.0.1](https://github.com/Romanchuk/angular-i18next/compare/v9.0.0...v9.0.1) (2020-02-25) ### Bug Fixes * pass translate options ([4cfe42c](https://github.com/Romanchuk/angular-i18next/commit/4cfe42c)) # [9.0.0](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.3...v9.0.0) (2020-02-20) # [8.1.0-beta.3](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.2...v8.1.0-beta.3) (2020-02-20) # [8.1.0-beta.2](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.1...v8.1.0-beta.2) (2020-02-20) # [8.1.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta...v8.1.0-beta.1) (2020-02-20) ### Features * improved typings ([214e35d](https://github.com/Romanchuk/angular-i18next/commit/214e35d)) # [8.1.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.2.0-beta...v8.1.0-beta) (2020-02-20) ## [8.0.1](https://github.com/Romanchuk/angular-i18next/compare/v8.0.1-beta.0...v8.0.1) (2020-02-18) ## [8.0.1-beta.0](https://github.com/Romanchuk/angular-i18next/compare/v8.0.1-beta...v8.0.1-beta.0) (2020-02-18) ## [8.0.1-beta](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0...v8.0.1-beta) (2020-02-18) # [8.0.0](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0-beta.1...v8.0.0) (2020-02-14) # [8.0.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0-beta...v8.0.0-beta.1) (2020-02-13) # [8.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.2.0-beta...v8.0.0-beta) (2020-02-13) # [7.2.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.0.0...v7.2.0-beta) (2020-01-28) ### Bug Fixes * I18NextEagerPipe ([8dbefe1](https://github.com/Romanchuk/angular-i18next/commit/8dbefe1)) # [7.0.0](https://github.com/Romanchuk/angular-i18next/compare/v6.1.0...v7.0.0) (2019-06-05) # [6.1.0](https://github.com/Romanchuk/angular-i18next/compare/v6.1.0-beta...v6.1.0) (2019-05-27) # [6.1.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v6.0.1...v6.1.0-beta) (2019-05-25) ## [6.0.1](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0...v6.0.1) (2019-03-11) # [6.0.0](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0-beta.0...v6.0.0) (2019-02-10) # [6.0.0-beta.0](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0-beta...v6.0.0-beta.0) (2019-02-10) # [6.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v5.0.6...v6.0.0-beta) (2019-02-10) ## [5.0.6](https://github.com/Romanchuk/angular-i18next/compare/v5.0.5...v5.0.6) (2018-12-03) ## [5.0.5](https://github.com/Romanchuk/angular-i18next/compare/v5.0.4...v5.0.5) (2018-12-03) ## [5.0.4](https://github.com/Romanchuk/angular-i18next/compare/v5.0.3...v5.0.4) (2018-12-03) ## [5.0.3](https://github.com/Romanchuk/angular-i18next/compare/v5.0.2...v5.0.3) (2018-12-03) ## [5.0.2](https://github.com/Romanchuk/angular-i18next/compare/v5.0.1...v5.0.2) (2018-12-03) ### Bug Fixes * package.json ([54a8c37](https://github.com/Romanchuk/angular-i18next/commit/54a8c37)) ## [5.0.1](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0...v5.0.1) (2018-11-28) # [5.0.0](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0-beta2...v5.0.0) (2018-11-28) # [5.0.0-beta2](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0-beta...v5.0.0-beta2) (2018-11-28) # [5.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v4.0.0...v5.0.0-beta) (2018-11-28) ### Bug Fixes * docs ([220a0b8](https://github.com/Romanchuk/angular-i18next/commit/220a0b8)) # [4.0.0](https://github.com/Romanchuk/angular-i18next/compare/v4.0.0-beta...v4.0.0) (2018-06-25) In v4 passed through most of i18next api methods 1. Update angular to v6+ 2. Update rxjs to v6.2.0+ # [4.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v3.4.2...v4.0.0-beta) (2018-06-11) ## [3.4.2](https://github.com/Romanchuk/angular-i18next/compare/v3.4.1...v3.4.2) (2018-05-05) ## [3.4.1](https://github.com/Romanchuk/angular-i18next/compare/v3.4.0...v3.4.1) (2018-04-29) - default formater fixes # [3.4.0](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0...v3.4.0) (2018-04-29) - i18next v11 support - fix: [format pipe](https://github.com/Romanchuk/angular-i18next/issues/15) # [3.3.0](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0-beta.2...v3.3.0) (2018-03-12) - added umd bundle - comments cleanup - updated dev dependencies # [3.3.0-beta.2](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0-beta.1...v3.3.0-beta.2) (2018-03-12) # [3.3.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v3.2.0...v3.3.0-beta.1) (2018-02-04) # [3.2.0](https://github.com/Romanchuk/angular-i18next/compare/v3.1.1...v3.2.0) (2018-01-17) ### Bug Fixes * [aot build failed](Romanchuk/angular-i18next#10) ### Breaking changes Removed parameter 'localizeTitle' from forRoot method. You need to manually resolve Title as I18NextTitle for same behavior. ## [3.1.1](https://github.com/Romanchuk/angular-i18next/compare/v3.1.0...v3.1.1) (2018-01-01) ### Bug Fixes * bug namespace fallback ([a16b067](https://github.com/Romanchuk/angular-i18next/commit/a16b067)) * conventional-github-releaser run ([df3bb84](https://github.com/Romanchuk/angular-i18next/commit/df3bb84)) # [3.1.0](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0...v3.1.0) (2017-12-22) It is possible to pass array of namespaces (or scopes). [Key would fallback](https://www.i18next.com/api.html#t) to next namespace in array if the previous failed to resolve. `[feature.validators:key, validators:key]` ```typescript { provide: I18NEXT_NAMESPACE, useValue: ['feature.validators', 'validators'] } ``` # [3.0.0](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0-alpha.2...v3.0.0) (2017-12-15) # [3.0.0-alpha.2](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0-alpha...v3.0.0-alpha.2) (2017-12-05) # [3.0.0-alpha](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0...v3.0.0-alpha) (2017-11-27) # [2.0.0](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0-beta2...v2.0.0) (2017-11-14) # [2.0.0-beta2](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0-beta...v2.0.0-beta2) (2017-11-05) # [2.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v1.1.0...v2.0.0-beta) (2017-11-05) # [1.1.0](https://github.com/Romanchuk/angular-i18next/compare/v1.0.2...v1.1.0) (2017-11-04) ## [1.0.2](https://github.com/Romanchuk/angular-i18next/compare/v1.0.1...v1.0.2) (2017-09-22) ## [1.0.1](https://github.com/Romanchuk/angular-i18next/compare/v1.0.0...v1.0.1) (2017-09-21) # [1.0.0](https://github.com/Romanchuk/angular-i18next/compare/v0.2.4...v1.0.0) (2017-09-21) ## [0.2.4](https://github.com/Romanchuk/angular-i18next/compare/v0.2.3...v0.2.4) (2017-06-29) ## [0.2.3](https://github.com/Romanchuk/angular-i18next/compare/v0.2.2...v0.2.3) (2017-06-29) ## [0.2.2](https://github.com/Romanchuk/angular-i18next/compare/v0.2.1...v0.2.2) (2017-06-29) ### Bug Fixes * **I18NextService:** context-safe calls of i18next methods ([455a07d](https://github.com/Romanchuk/angular-i18next/commit/455a07d)) ## [0.2.1](https://github.com/Romanchuk/angular-i18next/compare/v0.2.0...v0.2.1) (2017-06-29) ### Bug Fixes * **package:** return back required exports ([fb7ead6](https://github.com/Romanchuk/angular-i18next/commit/fb7ead6)) # [0.2.0](https://github.com/Romanchuk/angular-i18next/compare/0.1.0...0.2.0) (2017-06-28) ### Features * **package:** AOT support added ([fc1f66d](https://github.com/Romanchuk/angular-i18next/commit/fc1f66d)) ================================================ FILE: FUNDING.yml ================================================ github: Romanchuk ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Sergey Romanchuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![npm version](https://badge.fury.io/js/angular-i18next.svg)](https://badge.fury.io/js/angular-i18next) [![Downloads](http://img.shields.io/npm/dm/angular-i18next.svg)](https://npmjs.org/package/angular-i18next) [![Build Status](https://github.com/romanchuk/angular-i18next/actions/workflows/static.yml/badge.svg)](https://github.com/Romanchuk/angular-i18next/actions/workflows/static.yml) [![Coverage Status](https://coveralls.io/repos/github/Romanchuk/angular-i18next/badge.svg?branch=master)](https://coveralls.io/github/Romanchuk/angular-i18next?branch=master) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![paypal](https://img.shields.io/badge/say_thanks-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) [![GitHub stars](https://img.shields.io/github/stars/romanchuk/angular-i18next?label=Please%20star%20repo%21&style=social)](https://github.com/romanchuk/angular-i18next) # angular-i18next Best [i18next](http://i18next.com/) integration with [angular](https://angular.io/) [Live DEMO](https://angular-i18next.onrender.com) - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Cookbook](#cookbook) - [In-project testing](#in-project-testing) - [Demo](#demo) - [Articles](#articles) - [Support project](#cheers) - [DEPRECATED DOCS](./README_DEPRECATED.md) # Features - Native i18next [options](https://www.i18next.com/configuration-options.html) - Promise initialization - [i18next plugin](https://www.i18next.com/plugins-and-utils.html#plugins) support - Events support - Namespaces lazy load - i18next native [format](https://www.i18next.com/api.html#format) support - document.title localization - Error handling strategies - i18next namespaces and scopes (prefixes) for angular modules and components - AOT support - SSR support - Providers for unit testing - Angular Package Format support - Zoneless compatible # Cheers Star this project Hey dude! Help me out for a couple of :beers:! Поддержи проект - угости автора кружечкой пива! [![paypal](https://img.shields.io/badge/paypal-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) ## Available Submodules (optional) - **`angular-i18next/ssr`**: Adds Server Side Rendering support. - **`angular-i18next/forms`**: Provides localization for `@angular/forms`. - **`angular-i18next/testing`**: Offers features for testing. # Installation **1.** Install package ```bash npm install i18next angular-i18next ``` **2.** Initialize i18next before angular application and provide Angular would not load until i18next initialize event fired ```typescript import { I18NEXT_SERVICE } from 'angular-i18next'; export function i18nAppInit() { return () { const i18next = inject(I18NEXT_SERVICE); return i18next.init(); } } ``` ```typescript providers: [ provideAppInitializer(i18nAppInit()), provideI18Next( withCustomErrorHandlingStrategy(StrictErrorHandlingStrategy) ) ] ``` # Usage ## Pipes Use "i18next" pipe to translate key: ```html
{{ 'test' | i18next }}
``` Passing ["t options"](https://www.i18next.com/api.html#t): ```html
{{ 'test' | i18next: { count: 5, nsSeparator: '#' } }}
``` Remember to import the Pipe into the Component: ```typescript @Component({ // ... imports: [I18NextPipe], }) export class SomeExampleComponent {} ``` Trigger native i18next [format method](https://www.i18next.com/formatting.html) by using I18NextFormatPipe or I18NextPipe with option 'format': `{{ 'any_key' | i18next | i18nextFormat }}` `{{ 'any_key' | i18next: { format: 'cap' } }}` `{{ 'any_key' | i18nextCap }}` **Note:** Using "i18nextCap" you will get the same result as `i18next: { format: 'cap' }` **REMEMBER** that format will not work until you set "interpolation.format" function in i18next options. `angular-i81next` has static method `static interpolationFormat(customFormat: Function = null): Function` that can be used as default interpolation format function (it provides 'upper', 'cap' and 'lower' formatters). You also can pass your custom function to be called after library formatters: ```typescript import { defaultInterpolationFormat, interpolationFormat } from "angular-i18next"; const i18nextOptions = { supportedLngs: ['en', 'ru'], ns: [ 'translation', 'validation', 'error', ], interpolation: { format: interpolationFormat((value, format, lng) => { // extend interpolation format if(value instanceof Date) return moment(value).format(format); return value; }); // format: interpolationFormat(defaultInterpolationFormat) } }; ``` **i18nextEager pipe** This is the impure analog of *i18next pipe* that is subscribed to language change, it will change string right away to choosen language (without reloading page). **Warning!**: Use i18nextEager only in combine with [OnPush change detection strategy](https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4), or else (default change detection) each pipe will retrigger more than one time (cause of performance issues). Subscribing to event observables: ```typescript this.i18NextService.events.languageChanged.subscribe(lang => { // do something }) ``` Add a provider to module/component if you want to prefix child i18next keys: ```typescript { provide: I18NEXT_NAMESPACE, useValue: 'feature' // set 'feature:' prefix } ``` ```typescript { provide: I18NEXT_SCOPE, useValue: 'person' // set 'person.' prefix } ``` Since v3.1.0+ it is possible to pass array of namespaces (or scopes). [Key would fallback](https://www.i18next.com/api.html#t) to next namespace in array if the previous failed to resolve. `[feature_validators:key, validators:key]` ```typescript { provide: I18NEXT_NAMESPACE, useValue: ['feature_validators', 'validators'] } ``` _NOTE:* **Do NOT** use default (or custom) i18next delimiters in namespace names. ### Document title If you want to turn on document title localization resolve Title as `I18NextTitle` imported from 'angular-i18next': ```typescript providers: [provideI18Next(withTitle())] ``` Routes example: ```typescript const appRoutes: Routes = [ { path: 'error', component: AppErrorComponent, data: { title: 'error:error_occured' } }, { path: 'denied', component: AccessDeniedComponent, data: { title: 'error:access_denied' } } ]; ``` Ways to use I18NextService in your code: > **Warning:** Injection of **I18NextService** is possible, but it would not consider I18NEXT_NAMESPACE and I18NEXT_SCOPE providers. There are 2 possible reasons to inject **I18NextService**: initialization and subscription to its events. In all other cases inject **I18NextPipe**. 1) **Recommended way:** Inject via **I18NEXT_SERVICE** token. By default it will inject instance of **I18NextService**. ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) ``` 2) Legacy way: Inject via type ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, private i18NextService: I18NextService) ``` ### Error handling Error handling is now configurable: 1) By default i18next promise will use NativeErrorHandlingStrategy. I18Next would be always resolve successfully. Error could be get from 'then' handler parameter. 2) Set StrictErrorHandlingStrategy to reject load promises (init, languageChange, loadNamespaces) on first load fail (this was default in v2 but changed to fit [native i18next behavior](https://github.com/Romanchuk/angular-i18next/issues/9): ```typescript providers: [ provideI18Next( withCustomErrorHandlingStrategy(StrictErrorHandlingStrategy) ) ] ``` ### Lazy loading Use `i18NextNamespacesGuard` in your routes to to load i18next namespace. Note: It is not necessary to register lazy loading namespaces in global i18next options. ``` { path: 'rich_form', loadComponent: () => RichFormComponent, providers: [ { provide: I18NEXT_NAMESPACE, // namespace to start in component useValue: 'feature.rich_form', }, ], canActivate: [i18NextNamespacesGuard('feature.rich_form')] } ``` Use I18NextService.loadNamespaces() method to load namespaces in code. # Cookbook ### i18next plugin support ```typescript import { I18NextModule, ITranslationService, I18NEXT_SERVICE } from 'angular-i18next'; // import Backend from 'i18next-xhr-backend'; //for i18next < 20.0.0 import HttpApi from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; ... i18next.use(HttpApi) .use(LanguageDetector) .init(i18nextOptions) ``` ### Server side rendereng (SSR) 1. Provide for server: ```typescript import { provideI18Next, withTitle } from 'angular-i18next'; import { withSSR } from 'angular-i18next/ssr'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(), provideServerRouting(serverRoutes), provideI18Next(withTitle(), withSSR()), ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ``` 2. Configure i18next in `server.ts` ([Example](./apps/angular-i18next-demo/src/server.ts)): ### Auto error message for `@angular/forms` Use `i18nextValidationMessage` directive with formControlName ```typescript import { I18NextValidationMessageDirective } from 'angular-i18next/forms' @Component({ imports: [I18NextValidationMessageDirective] }) ``` There is priority order for validation messages: 1. namespace + `control_specific` with form hierarchy 2. namespace + Common validation key(like `required`) 3. `control_specific` with form hierarchy 4. Common validation key like `required` Also you can interpolate `control.error` values. For example: For validator `Validators.min(1)` ```json "min": "Minimal {{min}}. Actual: {{actual}}." ``` `en.validation.json` ```json { "required": "Field is required.", "pattern": "$t(validation:_fill) valid value.", "_fill": "Please fill in", "control_specific": { "technicalContact": { "firstName": { "required": "$t(validation:_fill) technical specialist's first name." }, "lastName": { "required": "$t(validation:_fill) technical specialist's last name." }, "middleName": { "required": "$t(validation:_fill) technical specialist's patronymic." } } } } ``` ### Testing ```typescript import { withSSR } from 'angular-i18next/testing'; TestBed.configureTestingModule({ imports: [ProjectComponent], providers: [ provideI18NextMockAppInitializer(), provideI18Next(withMock()) ] }); ``` # What to do if... ? ## New angular version released, but angular-i18next is not released YET Angular releases mostly don't break angular-i18next, but we cannot tell ahead that current version of `angular-i18next` will work correctly with latest angular version. You can override an angular-i18next `peerDependencies` in your `package.json` on your **own risk**: ```json "overrides": { "angular-i18next": { "@angular/common": "*", "@angular/core": "*", "@angular/platform-browser": "*" } } ``` # In-project testing You might want to unit-test project components that are using i18next pipes Example tests setup: [libs/angular-i18next/src/tests/projectTests](https://github.com/Romanchuk/angular-i18next/tree/master/libs/angular-i18next/src/tests/projectTests) # Demo [Live DEMO](https://angular-i18next.onrender.com) Demo app source code available here: # Articles - [Angular L10n with I18next](https://phrase.com/blog/posts/angular-l10n-with-i18next/) - [Best Libraries for Angular I18n](https://phrase.com/blog/posts/best-libraries-for-angular-i18n/) ================================================ FILE: README_DEPRECATED.md ================================================ [![npm version](https://badge.fury.io/js/angular-i18next.svg)](https://badge.fury.io/js/angular-i18next) [![Downloads](http://img.shields.io/npm/dm/angular-i18next.svg)](https://npmjs.org/package/angular-i18next) [![Build Status](https://travis-ci.com/Romanchuk/angular-i18next.svg?branch=master)](https://travis-ci.com/Romanchuk/angular-i18next) [![Coverage Status](https://coveralls.io/repos/github/Romanchuk/angular-i18next/badge.svg?branch=master)](https://coveralls.io/github/Romanchuk/angular-i18next?branch=master) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![paypal](https://img.shields.io/badge/say_thanks-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) [![GitHub stars](https://img.shields.io/github/stars/romanchuk/angular-i18next?label=Please%20star%20repo%21&style=social)](https://github.com/romanchuk/angular-i18next) # angular-i18next [i18next](http://i18next.com/) v8.4+ integration with [angular](https://angular.io/) v2.0+ [Live DEMO](https://romanchuk.github.io/angular-i18next/) - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Cookbook](#cookbook) - [Deep integration](#deep-integration) - [In-project testing](#in-project-testing) - [Demo](#demo) - [Articles](#articles) - [Support project](#support-on-beerpay) # Features - Native i18next [options](https://www.i18next.com/configuration-options.html) - Promise initialization - [i18next plugin](https://www.i18next.com/plugins-and-utils.html#plugins) support - Events support - Namespaces lazy load - i18next native [format](https://www.i18next.com/api.html#format) support - document.title localization - Error handling strategies - i18next namespaces and scopes (prefixes) for angular modules and components - AOT support - [Angular Package Format](https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/preview) support [Related packages](#deep-integration) also has implementations for: - Reactive forms validators localization - Http error message localizer # Cheers! Hey dude! Help me out for a couple of :beers:! Поддержи проект - угости автора кружечкой пива! [![paypal](https://img.shields.io/badge/paypal-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) # Installation **1.** Install package ``` npm install i18next --save npm install angular-i18next --save ``` **2.** Import I18NextModule to AppModule ```typescript import { I18NextModule } from 'angular-i18next'; @NgModule({ bootstrap: [ AppComponent ], declarations: [ AppComponent ], import: [ I18NextModule.forRoot() ] }) export class AppModule {} ``` **3.** Import I18NextModule.forRoot() to AppModule and setup provider with "init" method (use native [options](https://www.i18next.com/overview/configuration-options)). Angular would not load until i18next initialize event fired > **Warning:**: options in example valid for i18next v20 (Always check latest API options of i18next) ```typescript export function appInit(i18next: ITranslationService) { return () => i18next.init({ supportedLngs: ['en', 'ru'], fallbackLng: 'en', debug: true, returnEmptyString: false, ns: [ 'translation', 'validation', 'error' ], }); } export function localeIdFactory(i18next: ITranslationService) { return i18next.language; } export const I18N_PROVIDERS = [ { provide: APP_INITIALIZER, useFactory: appInit, deps: [I18NEXT_SERVICE], multi: true }, { provide: LOCALE_ID, deps: [I18NEXT_SERVICE], useFactory: localeIdFactory }]; ``` ```typescript @NgModule({ imports: [ ... I18NextModule.forRoot() ], providers: [ ... I18N_PROVIDERS, ], bootstrap: [AppComponent] }) export class AppModule { } ``` # Usage ### Pipes Use "i18next" pipe to translate key:
{{ 'test' | i18next }}
Passing ["t options"](https://www.i18next.com/api.html#t):
{{ 'test' | i18next: { count: 5, nsSeparator: '#' } }}
Trigger native i18next [format method](https://www.i18next.com/formatting.html) by using I18NextFormatPipe or I18NextPipe with option 'format': `{{ 'any_key' | i18next | i18nextFormat }}` `{{ 'any_key' | i18next: { format: 'cap' } }}` `{{ 'any_key' | i18nextCap }}` **Note:** Using "i18nextCap" you will get the same result as `i18next: { format: 'cap' }` **REMEMBER** that format will not work until you set "interpolation.format" function in i18next options. I18NextModule has static method `static interpolationFormat(customFormat: Function = null): Function` that can be used as default interpolation format function (it provides 'upper', 'cap' and 'lower' formatters). You also can pass your custom function to be called after I18NextModule formatters: ```typescript const i18nextOptions = { supportedLngs: ['en', 'ru'], ns: [ 'translation', 'validation', 'error', ], interpolation: { format: I18NextModule.interpolationFormat((value, format, lng) => { if(value instanceof Date) return moment(value).format(format); return value; }); // format: I18NextModule.interpolationFormat() } }; ``` **i18nextEager pipe** This is the impure analog of *i18next pipe* that is subscribed to language change, it will change string right away to choosen language (without reloading page). **Warning!**: Use i18nextEager only in combine with [OnPush change detection strategy](https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4), or else (default change detection) each pipe will retrigger more than one time (cause of performance issues). Subscribing to event observables: ```typescript this.i18NextService.events.languageChanged.subscribe(lang => { // do something }) ``` Add a provider to module/component if you want to prefix child i18next keys: ```typescript { provide: I18NEXT_NAMESPACE, useValue: 'feature' // set 'feature:' prefix } ``` ```typescript { provide: I18NEXT_SCOPE, useValue: 'person' // set 'person.' prefix } ``` Since v3.1.0+ it is possible to pass array of namespaces (or scopes). [Key would fallback](https://www.i18next.com/api.html#t) to next namespace in array if the previous failed to resolve. `[feature_validators:key, validators:key]` ```typescript { provide: I18NEXT_NAMESPACE, useValue: ['feature_validators', 'validators'] } ``` _NOTE:_ **Do NOT** use default (or custom) i18next delimiters in namespace names. ### Document title If you want to turn on document title localization resolve Title as `I18NextTitle` imported from 'angular-i18next': ```typescript { provide: Title, useClass: I18NextTitle } ``` Also you can implement your own Title service with specific behavior. Inject `I18NextPipe` (or `I18NextService`) to service/component: ```typescript import { Injectable, Inject } from '@angular/core'; import { Title, DOCUMENT } from '@angular/platform-browser'; import { I18NextPipe } from 'angular-i18next'; @Injectable() export class I18NextTitle extends Title { constructor(private i18nextPipe: I18NextPipe, @Inject(DOCUMENT) doc) { super(doc); } setTitle(value: string) { return super.setTitle(this.translate(value)); } private translate(text: string) { return this.i18nextPipe.transform(text, { format: 'cap'}); } } ``` Ways to use I18NextService in your code: > **Warning:** Injection of **I18NextService** is possible, but it would not consider I18NEXT_NAMESPACE and I18NEXT_SCOPE providers. There are 2 possible reasons to inject **I18NextService**: initialization and subscription to its events. In all other cases inject **I18NextPipe**. 1) **Recommended way:** Inject via **I18NEXT_SERVICE** token. By default it will inject instance of **I18NextService**. ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) ``` 2) Legacy way: Inject via type ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, private i18NextService: I18NextService) ``` ### Error handling Error handling is now configurable: 1) By default i18next promise will use NativeErrorHandlingStrategy. I18Next would be always resolve succesfully. Error could be get from 'then' handler parameter. 2) Set StrictErrorHandlingStrategy to reject load promises (init, languageChange, loadNamespaces) on first load fail (this was default in v2 but changed to fit [native i18next behavior](https://github.com/Romanchuk/angular-i18next/issues/9): `I18NextModule.forRoot({ errorHandlingStrategy: StrictErrorHandlingStrategy })` ### Lazy loading Use I18NEXT_NAMESPACE_RESOLVER in your routes to to load i18next namespace. Note: It is not neccesary to register lazy loading namespaces in global i18next options. ``` { path: 'rich_form', loadChildren: 'app/features/rich_form_feature/RichFormFeatureModule#RichFormFeatureModule', data: { i18nextNamespaces: ['feature.rich_form'] }, resolve: { i18next: I18NEXT_NAMESPACE_RESOLVER } }, ``` Use I18NextService.loadNamespaces() method to load namespaces in code. # Cookbook ### i18next plugin support ```typescript import { I18NextModule, ITranslationService, I18NEXT_SERVICE } from 'angular-i18next'; // import Backend from 'i18next-xhr-backend'; //for i18next < 20.0.0 import HttpApi from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; ... i18next.use(HttpApi) .use(LanguageDetector) .init(i18nextOptions) ``` ### Initialize i18next before angular application Angular would not load until i18next initialize event fired ```typescript export function appInit(i18next: ITranslationService) { return () => i18next.init(); } export function localeIdFactory(i18next: ITranslationService) { return i18next.language; } export const I18N_PROVIDERS = [ { provide: APP_INITIALIZER, useFactory: appInit, deps: [I18NEXT_SERVICE], multi: true }, { provide: LOCALE_ID, deps: [I18NEXT_SERVICE], useFactory: localeIdFactory }]; ``` ### Document title update on language or route change ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) { // page title subscription // https://toddmotto.com/dynamic-page-titles-angular-2-router-events#final-code this.router.events .filter(event => event instanceof NavigationEnd) .map(() => this.router.routerState.root) .map(route => { while (route.firstChild) route = route.firstChild; return route; }) .filter(route => route.outlet === 'primary') .mergeMap(route => route.data) .subscribe((event) => this.updatePageTitle(event['title'])); } ngOnInit() { this.i18NextService.events.languageChanged.subscribe(lang => { let root = this.router.routerState.root; if (root != null && root.firstChild != null) { let data: any = root.firstChild.data; this.updatePageTitle(data && data.value && data.value.title); } }); } updatePageTitle(title: string): void { let newTitle = title || 'application_title'; this.title.setTitle(newTitle); } } ``` Routes example: ```typescript const appRoutes: Routes = [ { path: 'error', component: AppErrorComponent, data: { title: 'error:error_occured' } }, { path: 'denied', component: AccessDeniedComponent, data: { title: 'error:access_denied' } } ]; ``` # What to do if... ? ## New angular version released, but angular-i18next is not released YET!!! Angular releases mostly don't break angular-i18next, but we cannot tell ahead that current version of `angular-i18next` will work correctly with latest angular version. You can override an angular-i18next `peerDependencies` in your `package.json` on your **own risk**: ```json "overrides": { "angular-i18next": { "@angular/common": "*", "@angular/core": "*", "@angular/platform-browser": "*" } } ``` # Deep integration List of packages to integrate angular and i18next more deeply: - [angular-validation-message](https://github.com/Romanchuk/angular-validation-message) - angular [reactive form validators](https://angular.io/guide/reactive-forms#step-2-making-a-field-required) integration (and [angular-validation-message-i18next ](https://github.com/Romanchuk/angular-validation-message-i18next) is i18next bridge to it). It gives you possibility to localize form validators and it automatically puts localized validator error message to markup (if there is one). - [angular-i18next-error-interceptor](https://github.com/LCGroupIT/angular-i18next-error-interceptor) - allows you to set default errot messages for non-200 http status responses. So if the back-end didn't specify { message: 'some error' } in a response (sort of contract with our backend) interceptor will check response status code and will fill { message: 'Server is not available. Please try again.' }. Also package includes pipe where you can pass HttpErrorResponse and it will return error message whenever it's back-end message or our localized message. # In-project testing You might want to unit-test project components that are using i18next pipes Example tests setup: [libs/angular-i18next/src/tests/projectTests](https://github.com/Romanchuk/angular-i18next/tree/master/libs/angular-i18next/src/tests/projectTests) # Demo [Live DEMO](https://romanchuk.github.io/angular-i18next-demo/) Demo app source code available here: https://github.com/Romanchuk/angular-i18next-demo # Articles - [Angular L10n with I18next](https://phrase.com/blog/posts/angular-l10n-with-i18next/) - [Best Libraries for Angular I18n](https://phrase.com/blog/posts/best-libraries-for-angular-i18n/) ================================================ FILE: apps/angular-i18next-demo/.browserslistrc ================================================ # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries # For the full list of supported browsers by the Angular framework, please see: # https://angular.io/guide/browser-support # You can see what browsers were selected by your queries by running: # npx browserslist last 1 Chrome version last 1 Firefox version last 2 Edge major versions last 2 Safari major versions last 2 iOS major versions Firefox ESR ================================================ FILE: apps/angular-i18next-demo/.eslintrc.json ================================================ { "extends": ["../../.eslintrc.json"], "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts"], "extends": [ "plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates" ], "rules": {} }, { "files": ["*.html"], "extends": ["plugin:@nx/angular-template"], "rules": {} } ] } ================================================ FILE: apps/angular-i18next-demo/jest.config.ts ================================================ /* eslint-disable */ export default { displayName: 'angular-i18next-demo', preset: '../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: {}, coverageDirectory: '../../coverage/apps/angular-i18next-demo', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', { tsconfig: '/tsconfig.spec.json', stringifyContentPathRegex: '\\.(html|svg)$', }, ], }, transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], snapshotSerializers: [ 'jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment', ], }; ================================================ FILE: apps/angular-i18next-demo/project.json ================================================ { "name": "angular-i18next-demo", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "sourceRoot": "apps/angular-i18next-demo/src", "prefix": "angular-i18next", "tags": [], "targets": { "build": { "executor": "@nx/angular:application", "outputs": ["{options.outputPath}"], "options": { "browser": "apps/angular-i18next-demo/src/main.ts", "outputPath": "dist/angular-i18next-demo", "index": "apps/angular-i18next-demo/src/index.html", "tsConfig": "apps/angular-i18next-demo/tsconfig.app.json", "assets": [ "apps/angular-i18next-demo/src/assets/favicon.png", "apps/angular-i18next-demo/src/assets", "apps/angular-i18next-demo/src/locales" ], "styles": ["apps/angular-i18next-demo/src/styles.css"], "scripts": [], "server": "apps/angular-i18next-demo/src/main.server.ts", "ssr": { "entry": "apps/angular-i18next-demo/src/server.ts" }, "outputMode": "server" }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "apps/angular-i18next-demo/src/environments/environment.ts", "with": "apps/angular-i18next-demo/src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "executor": "@nx/angular:dev-server", "configurations": { "production": { "buildTarget": "angular-i18next-demo:build:production" }, "development": { "buildTarget": "angular-i18next-demo:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "executor": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "angular-i18next-demo:build" } }, "lint": { "executor": "@nx/eslint:lint", "options": { "lintFilePatterns": [ "apps/angular-i18next-demo/**/*.ts", "apps/angular-i18next-demo/**/*.html" ] } }, "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/apps/angular-i18next-demo"], "options": { "jestConfig": "apps/angular-i18next-demo/jest.config.ts", "passWithNoTests": true } } } } ================================================ FILE: apps/angular-i18next-demo/src/app/app.component.css ================================================ ================================================ FILE: apps/angular-i18next-demo/src/app/app.component.html ================================================
@if (loading) {
}

{{ 'intro' | i18next }}


================================================ FILE: apps/angular-i18next-demo/src/app/app.component.ts ================================================ import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { Event as RouterEvent, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RouterOutlet } from '@angular/router'; import { I18NEXT_SERVICE, I18NextPipe, ITranslationService } from 'angular-i18next'; import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { AppHeaderComponent } from "./structure/app-header.component"; import { AppFooterComponent } from "./structure/app-footer.component"; @Component({ selector: 'app', encapsulation: ViewEncapsulation.None, templateUrl: './app.component.html', standalone: true, imports: [I18NextPipe, AppHeaderComponent, AppFooterComponent, RouterOutlet], }) export class AppComponent implements OnInit { loading = true; start = 0; get title() { return this._title.getTitle(); } constructor(private router: Router, private _title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) { // spinner/loader subscription router.events .subscribe((event: RouterEvent) => { this.navigationInterceptor(event); }); // page title subscription // https://toddmotto.com/dynamic-page-titles-angular-2-router-events#final-code this.router.events .pipe( filter(event => event instanceof NavigationEnd), map(() => this.router.routerState.root), map(route => { while (route.firstChild) route = route.firstChild; return route; }), filter(route => route.outlet === 'primary'), mergeMap(route => route.data) ) .subscribe((event) => this.updatePageTitle(event['title'])); } ngOnInit() { this.i18NextService.events.languageChanged.subscribe(() => { const root = this.router.routerState.root; if (root != null && root.firstChild != null) { const data = root.firstChild.data; data .pipe( tap((data) => { this.updatePageTitle(data && data['value'] && data['value'].title); }) ) .subscribe(); } }); } // http://stackoverflow.com/questions/37069609/show-loading-screen-when-navigating-between-routes-in-angular-2 navigationInterceptor(event: RouterEvent): void { if (event instanceof NavigationStart) { this.loading = true; } if (event instanceof NavigationEnd || event instanceof NavigationCancel || event instanceof NavigationError) { this.loading = false; } } updatePageTitle(title: string): void { const newTitle = title || 'application_title'; console.log('Setting page title:', newTitle); this._title.setTitle(newTitle); console.log('Setting page title end:', newTitle); } } ================================================ FILE: apps/angular-i18next-demo/src/app/app.config.server.ts ================================================ import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; import { provideServerRendering, withRoutes } from '@angular/ssr'; import { provideI18Next, withTitle } from 'angular-i18next'; import { withSSR } from 'angular-i18next/ssr'; import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), provideI18Next(withTitle(), withSSR()), ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); ================================================ FILE: apps/angular-i18next-demo/src/app/app.config.ts ================================================ import { isPlatformBrowser } from '@angular/common'; import { ApplicationConfig, importProvidersFrom, inject, PLATFORM_ID, provideAppInitializer, provideZonelessChangeDetection, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserModule, provideClientHydration, withEventReplay, } from '@angular/platform-browser'; import { I18NEXT_SERVICE, I18NextLoadResult, provideI18Next, withTitle } from 'angular-i18next'; import HttpApi from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; import i18nextOptions from './i18next.options'; import { provideRouter } from '@angular/router'; import { appRoutes } from './app.routes'; export function appInit() { return () => { const i18next = inject(I18NEXT_SERVICE); const platformId = inject(PLATFORM_ID); if (!isPlatformBrowser(platformId)) { return Promise.resolve(); } const promise: Promise = i18next .use(HttpApi) .use(LanguageDetector) .init(i18nextOptions); return promise; }; } export const appConfig: ApplicationConfig = { providers: [ provideClientHydration(withEventReplay()), provideZonelessChangeDetection(), provideRouter(appRoutes), importProvidersFrom(BrowserModule, FormsModule), provideAppInitializer(appInit()), provideI18Next(withTitle()), ], }; ================================================ FILE: apps/angular-i18next-demo/src/app/app.routes.server.ts ================================================ import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { path: '**', renderMode: RenderMode.Server, }, ]; ================================================ FILE: apps/angular-i18next-demo/src/app/app.routes.ts ================================================ import { Routes } from "@angular/router"; import { I18NEXT_NAMESPACE, i18NextNamespacesGuard } from "angular-i18next"; import { AccessDeniedComponent } from "./content/access-denied/access-denied.component"; import { SimpleDemoComponent } from "./content/simple-demo.component"; import { RichFormComponent } from "./features/rich_form_feature/rich-form.component"; export const appRoutes: Routes = [ { path: '', component: SimpleDemoComponent }, { path: 'rich_form', loadComponent: () => RichFormComponent, data: { title: 'feature.rich_form:title' }, providers: [ { provide: I18NEXT_NAMESPACE, useValue: 'feature.rich_form', }, ], canActivate: [i18NextNamespacesGuard('feature.rich_form')] }, { path: 'denied', component: AccessDeniedComponent, data: { title: 'error:access_denied' }} ]; ================================================ FILE: apps/angular-i18next-demo/src/app/content/access-denied/access-denied.component.html ================================================

{{ 'error:access_denied' | i18next: { case: 'cap' } }}

================================================ FILE: apps/angular-i18next-demo/src/app/content/access-denied/access-denied.component.ts ================================================ import { Component } from '@angular/core'; import { I18NextPipe } from "angular-i18next"; @Component({ selector: 'access-denied', templateUrl: './access-denied.component.html', standalone: true, imports: [I18NextPipe] }) export class AccessDeniedComponent { } ================================================ FILE: apps/angular-i18next-demo/src/app/content/simple-demo.component.html ================================================ 

{{ 'simple_demo' | i18nextCap }}



{{ 'parametrized_string_title' | i18nextCap }}

{{ 'parametrized_string' | i18next: { value: value, str: str } }}


{{ 'case_demo_title' | i18nextCap }}

original capitalize lowercase uppercase
{{ 'case_demo' | i18next }} {{ 'case_demo' | i18next: { format: 'capitalize' } }} {{ 'case_demo' | i18next: { format: 'lowercase' } }} {{ 'case_demo' | i18next | i18nextFormat: 'uppercase' }}
================================================ FILE: apps/angular-i18next-demo/src/app/content/simple-demo.component.ts ================================================  import { Component, ViewEncapsulation } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { I18NextCapPipe, I18NextFormatPipe, I18NextPipe } from "angular-i18next"; @Component({ selector: 'simple-demo', encapsulation: ViewEncapsulation.None, templateUrl: './simple-demo.component.html', standalone: true, imports: [I18NextCapPipe, I18NextPipe, I18NextFormatPipe, FormsModule] }) export class SimpleDemoComponent { value = 15; str = 'Hello'; } ================================================ FILE: apps/angular-i18next-demo/src/app/features/rich_form_feature/rich-form.component.html ================================================ 

{{ 'title' | i18next: { case: 'cap' } }}


{{ 'technical_contact' | i18nextEager }}
================================================ FILE: apps/angular-i18next-demo/src/app/features/rich_form_feature/rich-form.component.ts ================================================  import { Component, ViewEncapsulation } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { I18NextCapPipe, I18NextEagerPipe, I18NextPipe } from "angular-i18next"; import { I18NextValidationMessageDirective } from 'angular-i18next/forms'; import { ValidationDirtyChecker } from '../../../lib/validation/services/ValidationDirtyChecker'; import { RichFormModel } from './rich-form.model'; @Component({ selector: 'rich-form', encapsulation: ViewEncapsulation.None, templateUrl: './rich-form.component.html', standalone: true, imports: [I18NextCapPipe, I18NextPipe, I18NextEagerPipe, ReactiveFormsModule, I18NextValidationMessageDirective], providers: [ValidationDirtyChecker] }) export class RichFormComponent { form: FormGroup; model: RichFormModel = new RichFormModel(); constructor(private fb: FormBuilder, private readonly validationDirtyChecker: ValidationDirtyChecker) { this.form = this.fb.group({ 'count': [this.model.count, [Validators.min(1), Validators.max(3)]], 'email': [this.model.email, [Validators.email]], 'technicalContact': this.fb.group({ 'firstName': [this.model.technicalContact.firstName, [Validators.required]], 'lastName': [this.model.technicalContact.lastName, [Validators.required]], 'middleName': [this.model.technicalContact.middleName, [Validators.required]], }) }); } onSubmit(e: Event) { if (!this.form.valid) { this.validationDirtyChecker.markControlsDirty(this.form); return; } } } ================================================ FILE: apps/angular-i18next-demo/src/app/features/rich_form_feature/rich-form.model.ts ================================================ export class Contact { lastName: string | undefined; firstName: string | undefined; middleName: string | undefined; } export class RichFormModel { email: string | undefined; count = 0; technicalContact: Contact = new Contact(); } ================================================ FILE: apps/angular-i18next-demo/src/app/i18next.options.ts ================================================ import { defaultInterpolationFormat, interpolationFormat } from "angular-i18next"; import type * as i18n from 'i18next'; import type { HttpBackendOptions } from "i18next-http-backend"; export const i18nextOptions: i18n.InitOptions & { backend: HttpBackendOptions} = { supportedLngs:['en', 'ru'], fallbackLng: 'en', debug: true, returnEmptyString: false, ns: [ 'translation', 'validation', 'error' ], interpolation: { format: interpolationFormat(defaultInterpolationFormat) }, //backend plugin options backend: { loadPath: 'locales/{{lng}}.{{ns}}.json', }, // lang detection plugin options detection: { // order and from where user language should be detected order: ['cookie', 'header'], // keys or params to lookup language from lookupCookie: 'lang', // lookupHeader: 'accept-language', // cache user language on caches: ['cookie'], // optional expire and domain for set cookie cookieMinutes: 10080, // 7 days } }; export default i18nextOptions; ================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-error.component.html ================================================
{{ 'error:oops' | i18next }}
{{ 'error:error_occured_onload' | i18next }}
  • {{ 'error:contact_administrator_or_try_to_clear_browser_chache_and_restart_application' | i18next }}

    @if (showed) {
    {{ 'error:cookies.how_to' | i18next }}
    {{ 'error:cookies.clear_chrome' | i18next }}
    }
  • {{ 'error:need_help_write_to_us' | i18next }}

    {{ 'error:write' | i18next }}
================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-error.component.ts ================================================ import { Component, ViewEncapsulation } from '@angular/core'; import { I18NextPipe } from 'angular-i18next'; @Component({ selector: 'app-error', encapsulation: ViewEncapsulation.None, templateUrl: './app-error.component.html', standalone: true, imports: [I18NextPipe] }) export class AppErrorComponent { public showed = false; public toggle(){ this.showed = !this.showed; } public close(){ this.showed = false; } public reload(){ document.location.href = document.location.protocol + '//' + document.location.host; } } ================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-footer.component.html ================================================  ================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-footer.component.ts ================================================  import { Component, VERSION, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'app-footer', encapsulation: ViewEncapsulation.None, templateUrl: './app-footer.component.html', standalone: true }) export class AppFooterComponent { angularVersion = '0.0.0'; i18nextVersion = '0.0.0'; constructor(){ this.angularVersion = VERSION.full; this.i18nextVersion = '24.2.1'; } } ================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-header.component.html ================================================ 
================================================ FILE: apps/angular-i18next-demo/src/app/structure/app-header.component.ts ================================================ import { Component, ViewEncapsulation } from '@angular/core'; import { I18NextCapPipe, I18NextPipe } from 'angular-i18next'; import { HeaderLanguageComponent } from "./header-controls/header.language.component"; import { RouterLink } from '@angular/router'; @Component({ selector: 'app-header', encapsulation: ViewEncapsulation.None, templateUrl: './app-header.component.html', standalone: true, imports: [I18NextPipe, I18NextCapPipe, HeaderLanguageComponent, RouterLink], }) export class AppHeaderComponent {} ================================================ FILE: apps/angular-i18next-demo/src/app/structure/header-controls/header.language.component.html ================================================
@for (lang of languages; track lang) { @if (currentLanguage() !== lang) { {{ '_languages.' + lang | i18nextCap }} } @else { {{ '_languages.' + lang | i18nextCap }} } }
================================================ FILE: apps/angular-i18next-demo/src/app/structure/header-controls/header.language.component.ts ================================================ import { Component, Inject, ViewEncapsulation, OnInit, signal } from '@angular/core'; import { I18NEXT_SERVICE, I18NextCapPipe, ITranslationService } from 'angular-i18next'; @Component({ selector: 'header-language', encapsulation: ViewEncapsulation.None, templateUrl: './header.language.component.html', standalone: true, imports: [I18NextCapPipe] }) export class HeaderLanguageComponent implements OnInit { currentLanguage = signal('ru'); languages: string[] = ['ru', 'en']; constructor( @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService ) {} ngOnInit() { this.i18NextService.events.initialized.subscribe((e) => { if (e) { this.updateState(this.i18NextService.language); } }); } changeLanguage(lang: string){ if (lang !== this.i18NextService.language) { this.i18NextService.changeLanguage(lang).then(() => { this.updateState(lang); document.location.reload(); }); } } private updateState(lang: string) { this.currentLanguage.set(lang); } } ================================================ FILE: apps/angular-i18next-demo/src/assets/.gitkeep ================================================ ================================================ FILE: apps/angular-i18next-demo/src/assets/ng-validation.css ================================================ input.ng-dirty.ng-invalid, textarea.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid, .ng-dirty.ng-invalid:focus { background-color: #FDEDED; border-color: #D22630; -webkit-box-shadow: none; box-shadow: none; } /* s7 ui kit fix */ .form-group .error-container { display: block; } .error-container { color: #D22630; padding-top: 2px; } /* end fix */ checkbox.ng-invalid .custom-control-indicator, multiplecheckbox.ng-invalid.ng-dirty .custom-control-indicator, flatpickr.ng-invalid.ng-dirty .form-control, datepicker.ng-invalid.ng-dirty .select2-container .select2-selection, radio-button.ng-dirty.ng-invalid .custom-control-indicator, div.ng-invalid.ng-dirty.form-group-valid .custom-control-indicator, div.ng-invalid.ng-dirty.form-group-valid .select2-container .select2-selection { border-color: #D22630; background-color: #FDEDED; } radio-button + radio-button, checkbox + checkbox { margin-left: 15px; } ================================================ FILE: apps/angular-i18next-demo/src/environments/environment.prod.ts ================================================ export const environment = { production: true }; ================================================ FILE: apps/angular-i18next-demo/src/environments/environment.ts ================================================ // This file can be replaced during build by using the `fileReplacements` array. // `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { production: false }; ================================================ FILE: apps/angular-i18next-demo/src/index.html ================================================ DEMO angular-i18next
================================================ FILE: apps/angular-i18next-demo/src/lib/validation/services/ValidationDirtyChecker.ts ================================================ import { Injectable } from '@angular/core'; import { FormArray, FormGroup } from '@angular/forms'; @Injectable() export class ValidationDirtyChecker { markControlsDirty(group: FormGroup | FormArray) { const controls = group.controls; for (const ck in controls) { // eslint-disable-next-line no-prototype-builtins if (controls.hasOwnProperty(ck)) { const c = (controls)[ck]; c.markAsDirty({ onlySelf: true }); if (c instanceof FormGroup || c instanceof FormArray) this.markControlsDirty(c); } } } } ================================================ FILE: apps/angular-i18next-demo/src/lib/validation/validators/ArrayValidators.js ================================================ var ArrayValidators = (function () { function ArrayValidators() { } ArrayValidators.minLength = function (minLength, ignoreNullAndUndefined) { if (ignoreNullAndUndefined === void 0) { ignoreNullAndUndefined = false; } return function (control) { if (control) { var isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); var val = control.value; var isValid = false; if (!ignoreNullAndUndefined) isValid = val.length >= minLength; else isValid = val.filter(function (v) { return v != null; }).length >= minLength; if (isValid) return null; return { 'arrayMinLength': minLength }; } }; }; ArrayValidators.maxLength = function (maxLength, ignoreNullAndUndefined) { if (ignoreNullAndUndefined === void 0) { ignoreNullAndUndefined = false; } return function (control) { if (control) { var isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); var val = control.value; var isValid = false; if (!ignoreNullAndUndefined) isValid = val.length <= maxLength; else isValid = val.filter(function (v) { return v != null; }).length <= maxLength; if (isValid) return null; return { 'arrayMaxLength': maxLength }; } }; }; ArrayValidators.eqLength = function (length, ignoreNullAndUndefined) { if (ignoreNullAndUndefined === void 0) { ignoreNullAndUndefined = false; } return function (control) { if (control) { var isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); var val = control.value; var isValid = false; if (!ignoreNullAndUndefined) isValid = val.length === length; else isValid = val.filter(function (v) { return v != null; }).length === length; if (isValid) return null; return { 'arrayMaxLength': length }; } }; }; return ArrayValidators; }()); export { ArrayValidators }; //# sourceMappingURL=ArrayValidators.js.map ================================================ FILE: apps/angular-i18next-demo/src/lib/validation/validators/ArrayValidators.ts ================================================ import { OnChanges, SimpleChanges, Component, Directive, forwardRef } from '@angular/core'; import { Validator, AsyncValidatorFn, ValidatorFn, FormControl, FormGroup, FormArray, AbstractControl, NG_VALIDATORS } from '@angular/forms'; export class ArrayValidators { static minLength(minLength: Number, ignoreNullAndUndefined: Boolean = false): ValidatorFn { return (control: FormControl) => { if (control) { let isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); let val: Array = control.value; let isValid: Boolean = false; if (!ignoreNullAndUndefined) isValid = val.length >= minLength; else isValid = val.filter(v => v != null).length >= minLength; if (isValid) return null; return { 'arrayMinLength': minLength }; } }; } static maxLength(maxLength: Number, ignoreNullAndUndefined: Boolean = false): ValidatorFn { return (control: FormControl) => { if (control) { let isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); let val: Array = control.value; let isValid: Boolean = false; if (!ignoreNullAndUndefined) isValid = val.length <= maxLength; else isValid = val.filter(v => v != null).length <= maxLength; if (isValid) return null; return { 'arrayMaxLength': maxLength }; } }; } static eqLength(length: Number, ignoreNullAndUndefined: Boolean = false): ValidatorFn { return (control: FormControl) => { if (control) { let isArray = control.value instanceof Array; if (!isArray) throw new Error('Control value must be array!'); let val: Array = control.value; let isValid: Boolean = false; if (!ignoreNullAndUndefined) isValid = val.length === length; else isValid = val.filter(v => v != null).length === length; if (isValid) return null; return { 'arrayMaxLength': length }; } }; } } ================================================ FILE: apps/angular-i18next-demo/src/lib/validation/validators/ConditionalValidator.js ================================================ import 'rxjs/add/operator/distinctUntilChanged'; // fn distinctUntilChanged var ConditionalValidator = (function () { function ConditionalValidator() { } /** * Валидатор, который применяет валидатор при некотором заданом условии. * @param {ConditionalFunc} conditional Условие для применения валидатора * @param {ValidatorFn} validator Валидатор, который будет применен * @param {Boolean} trackParentOnly Подписка только на изменение значения родителя (По-умолчанию подписка на root) */ ConditionalValidator.set = function (conditional, validator, trackParentOnly) { if (trackParentOnly === void 0) { trackParentOnly = null; } var revalidateSub; return function (control) { if (control && control.parent) { if (!revalidateSub) { revalidateOnChanges(control, trackParentOnly); revalidateSub = true; } if (conditional(control.root)) { return validator(control); } } return null; }; }; /* Не реализован */ ConditionalValidator.setAsync = function (conditional, validator) { throw new Error('Not implemented'); // todo: implement }; ConditionalValidator.equivalent = function (controlKey, expectedValue) { return function (rootGroup) { var control = rootGroup.get(controlKey); if (!control) return expectedValue === undefined; return expectedValue === control.value; }; }; return ConditionalValidator; }()); export { ConditionalValidator }; function revalidateOnChanges(control, trackParentOnly) { if (trackParentOnly === void 0) { trackParentOnly = null; } var parentControl = trackParentOnly ? control.parent : control.root; parentControl.valueChanges .distinctUntilChanged(function (a, b) { // These will always be plain objects coming from the form, do a simple comparison if (a && !b || !a && b) { return false; } else if (a && b && Object.keys(a).length !== Object.keys(b).length) { return false; } else if (a && b) { for (var i in a) { if (a[i] !== b[i]) { return false; } } } return true; }) .subscribe(function () { control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); }); } //# sourceMappingURL=ConditionalValidator.js.map ================================================ FILE: apps/angular-i18next-demo/src/lib/validation/validators/ConditionalValidator.ts ================================================ import { Validator, AsyncValidatorFn, ValidatorFn, FormControl, FormGroup, FormArray, AbstractControl } from '@angular/forms'; import { distinctUntilChanged } from 'rxjs/operators'; // todo: доработать ConditionalValidator, чтобы он работал в связке с асинхронным валидатором (сейчас валится) /* usage this.formBuilder.group({ vehicleType: ['', Validators.required], licencePlate: [ '', ConditionalValidator.apply( group => group.controls.vehicleType.value === 'car', Validators.compose([ Validators.required, Validators.minLength(6) ]) ), ] }); this.formBuilder.group({ country: ['', Validators.required], vehicleType: ['', Validators.required], licencePlate: [ '', Validators.compose([ ConditionalValidator.apply( group => group.controls.vehicleType.value === 'car', Validators.required ), ConditionalValidator.apply( group => group.controls.country.value === 'sweden', Validators.minLength(6) ), ]) ] }); */ interface ConditionalFunc { (rootGroup: FormGroup | FormArray): Boolean; } export class ConditionalValidator { /** * Валидатор, который применяет валидатор при некотором заданом условии. * @param {ConditionalFunc} conditional Условие для применения валидатора * @param {ValidatorFn} validator Валидатор, который будет применен * @param {Boolean} trackParentOnly Подписка только на изменение значения родителя (По-умолчанию подписка на root) */ static set(conditional: ConditionalFunc, validator: ValidatorFn, trackParentOnly: Boolean = null): ValidatorFn { let revalidateSub: Boolean; return (control: FormControl) => { if (control && control.parent) { if (!revalidateSub) { revalidateOnChanges(control, trackParentOnly); revalidateSub = true; } if (conditional(control.root)) { return validator(control); } } return null; }; } /* Не реализован */ static setAsync(conditional: Function, validator: AsyncValidatorFn): AsyncValidatorFn { throw new Error('Not implemented'); // todo: implement } static equivalent(controlKey: string, expectedValue: any): ConditionalFunc { return (rootGroup: FormGroup|FormArray) => { let control = rootGroup.get(controlKey); if (!control) return expectedValue === undefined; return expectedValue === control.value; } } } function revalidateOnChanges(control: AbstractControl, trackParentOnly: Boolean = null): void { let parentControl = trackParentOnly ? control.parent : control.root; parentControl.valueChanges .pipe( distinctUntilChanged((a, b) => { // These will always be plain objects coming from the form, do a simple comparison if (a && !b || !a && b) { return false; } else if (a && b && Object.keys(a).length !== Object.keys(b).length) { return false; } else if (a && b) { for (let i in a) { if (a[i] !== b[i]) { return false; } } } return true; }) ) .subscribe(() => { control.updateValueAndValidity({ onlySelf: true, emitEvent: false }); }); } ================================================ FILE: apps/angular-i18next-demo/src/locales/en.error.json ================================================ { "oops": "Oops!", "error_occured": "Error has occured", "error_occured_onload": "$t(error:error_occured)", "access_denied": "Access is denied", "reload": "Reload", "restart": "Restart", "contact_administrator_or_try_to_clear_browser_chache_and_restart_application": "Contact your administrator or clear browser cache and restart page.", "need_help_write_to_us": "Need help? Contact us.", "write": "Contact", "cookies": { "how_to": "How to clear browser cache and cookies", "chrome_clear": "Если у вас Google Chrome, то:
  1. Запустите Chrome.
  2. Нажмите на значок \"Настройка на панели инструментов.
  3. В меню Дополнительные инструменты нажмите Удаление данных о просмотренных страницах.
  4. В окне \"Очистить историю\" выберите пункты Файлы cookie, а также другие данные сайтов и плагинов и Изображения и другие файлы, сохраненные в кеше.
  5. В раскрывающемся меню в верхней части страницы выберите период, данные за который нужно удалить. Выберите вариант за все время, если вы хотите удалить все сведения.
  6. Нажмите кнопку Очистить историю.
" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/en.feature.rich_form.json ================================================ { "title": "Rich form with validation", "technical_contact": "technical contact", "count": "count", "person": { "first_name": "first name", "last_name": "last name", "middle_name": "middle name" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/en.translation.json ================================================ { "application_title": "Demo: angular-i18next", "intro": "This application is demonstrating itegration of i18next library with angular. You can switch language in the navbar.", "simple_demo": "Simple demo", "rich_form_title": "Rich form with validation", "parametrized_string_title": "parametrized string demo", "case_demo_title": "i18next pipe 'format' option demo", "case_demo": "rise and shine, Mr.Freeman", "parametrized_string": "I am parametrized sting with a value: {{value}} and a string: '{{str}}'", "email": "email", "_languages": { "ru": "Русский", "en": "English" }, "buttons": { "send": "send" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/en.validation.json ================================================ { "required": "Field is required.", "error": "Error occured.", "min": "Minimum value is {{min}}. Was {{actual}}.", "max": "Maximum value is {{max}}. Was {{actual}}.", "email": "$t(validation:_fill) valid e-mail.", "pattern": "$t(validation:_fill) valid value.", "maxlength": "Maximum length {{requiredLength}}.", "_fill": "Please fill in", "control_specific": { "technicalContact": { "firstName": { "required": "$t(validation:_fill) technical specialist's first name." }, "lastName": { "required": "$t(validation:_fill) technical specialist's last name." }, "middleName": { "required": "$t(validation:_fill) technical specialist's patronymic." } } } } ================================================ FILE: apps/angular-i18next-demo/src/locales/ru.error.json ================================================ { "oops": "Упс!", "error_occured": "произошла ошибка", "error_occured_onload": "При загрузке приложения $t(error:error_occured)", "access_denied": "Недостаточно прав", "reload": "Перезагрузить", "restart": "Перезапустить", "contact_administrator_or_try_to_clear_browser_chache_and_restart_application": "Обратитесь к администратору либо попробуйте очистить кэш и перезапустить приложение", "need_help_write_to_us": "Нужна помощь? Пишите нам.", "write": "Написать", "cookies": { "how_to": "Как очистить кэш и удалить файлы cookie", "chrome_clear": "Если у вас Google Chrome, то:
  1. Запустите Chrome.
  2. Нажмите на значок \"Настройка на панели инструментов.
  3. В меню Дополнительные инструменты нажмите Удаление данных о просмотренных страницах.
  4. В окне \"Очистить историю\" выберите пункты Файлы cookie, а также другие данные сайтов и плагинов и Изображения и другие файлы, сохраненные в кеше.
  5. В раскрывающемся меню в верхней части страницы выберите период, данные за который нужно удалить. Выберите вариант за все время, если вы хотите удалить все сведения.
  6. Нажмите кнопку Очистить историю.
" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/ru.feature.rich_form.json ================================================ { "title": "Форма с валидацией", "technical_contact": "технический специалист", "count": "кол-во", "person": { "first_name": "имя", "last_name": "фамилия", "middle_name": "отчество" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/ru.translation.json ================================================ { "application_title": "Демо: angular-i18next", "intro": "Данное приложение демонстрирует интеграцию библиотеки i18next с angular. Вы можете сменить язык в шапке.", "simple_demo": "простое демо", "rich_form_title": "Форма с валидацией", "parametrized_string_title": "демонстрация параметризованной строки", "case_demo_title": "демонстрация опции 'format'", "case_demo": "проснись и пой, мистер Фримэн", "parametrized_string": "Я параметризованная строка значением: {{value}} и строкой: '{{str}}'", "email": "email адрес", "_languages": { "ru": "Русский", "en": "English" }, "buttons": { "send": "отправить" } } ================================================ FILE: apps/angular-i18next-demo/src/locales/ru.validation.json ================================================ { "required": "Заполните это поле.", "error": "Возникла ошибка.", "minValue": "Минимальное значение.", "maxValue": "Максимальное значение.", "min": "Минимальное значение: {{min}}. Текущее: {{actual}}.", "max": "Максимальное значение: {{max}}. Текущее: {{actual}}.", "email": "Введите валидный email.", "pattern": "Введите валидное значение.", "maxlength": "Максимальная длина {{requiredLength}}.", "control_specific": { "technicalContact": { "firstName": { "required": "Заполните имя технического специалиста.", "pattern": "Имя технического специалиста содержит недопустимые символы." }, "lastName": { "required": "Заполните фамилию технического специалиста.", "pattern": "Фамилия технического специалиста содержит недопустимые символы." }, "middleName": { "required": "Заполните отчество технического специалиста.", "pattern": "Отчество технического специалиста содержит недопустимые символы." } } } } ================================================ FILE: apps/angular-i18next-demo/src/main.server.ts ================================================ import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { config } from './app/app.config.server'; const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context); export default bootstrap; ================================================ FILE: apps/angular-i18next-demo/src/main.ts ================================================ import { enableProdMode } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { appConfig } from './app/app.config'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } function bootstrap() { bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); } if (document.readyState === 'complete') { bootstrap(); } else { document.addEventListener('DOMContentLoaded', bootstrap); } ================================================ FILE: apps/angular-i18next-demo/src/server.ts ================================================ import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; import type { NextFunction, Request, Response } from 'express'; import express from 'express'; import i18next from 'i18next'; import ChainedBackend, { ChainedBackendOptions } from 'i18next-chained-backend'; import * as i18nextHttpMiddleware from 'i18next-http-middleware'; import resourcesToBackend from "i18next-resources-to-backend"; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import i18nextOptions from './app/i18next.options'; const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const app = express(); const angularApp = new AngularNodeAppEngine(); await i18next .use(ChainedBackend) .use(i18nextHttpMiddleware.LanguageDetector) .init({ ...i18nextOptions, backend: { backends: [ resourcesToBackend((lng, ns, clb) => { import(`./locales/${lng}.${ns}.json`) .then((resources) => clb(null, resources)) .catch((r)=> clb(r,null)) }) ], backendOptions: [{ loadPath: '/locales/{{lng}}.{{ns}}.json' }] } }); const i18nextHandler = i18nextHttpMiddleware.handle(i18next) as any; app.use(i18nextHandler); /** * Example Express Rest API endpoints can be defined here. * Uncomment and define endpoints as necessary. * * Example: * ```ts * app.get('/api/**', (req, res) => { * // Handle API request * }); * ``` */ /** * Serve static files from /browser */ app.use( express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false, }), ); /** * Handle all other requests by rendering the Angular application. */ app.use('/**', (req: Request & i18nextHttpMiddleware.I18NextRequest, res: Response, next: NextFunction) => { angularApp .handle(req, { i18n: req.i18n, }) .then((response) => response ? writeResponseToNodeResponse(response, res) : next(), ) .catch(next); }); /** * Start the server if this module is the main entry point. * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. */ if (isMainModule(import.meta.url) || process.env['PM2'] === 'true') { const port = process.env['PORT'] || 4000; const server = app.listen(port, () => { process.send?.('ready'); console.log(`Node Express server listening on http://localhost:${port}`); }); // Graceful shutdown process.on('SIGINT', () => { const cleanUp = () => { // Clean up other resources like DB connections } console.log('Closing server...') server.close(() => { console.log('Server closed !!! ') cleanUp() process.exit() }) // Force close server after 5secs setTimeout((e: any) => { console.log('Forcing server close !!!', e) cleanUp() process.exit(1) }, 5000) }) } /** * The request handler used by the Angular CLI (dev-server and during build). */ export const reqHandler = createNodeRequestHandler(app); ================================================ FILE: apps/angular-i18next-demo/src/styles.css ================================================ /* You can add global styles to this file, and also import other style files */ input.ng-dirty.ng-invalid, textarea.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid, .ng-dirty.ng-invalid:focus { background-color: #FDEDED; border-color: #D22630; -webkit-box-shadow: none; box-shadow: none; } /* s7 ui kit fix */ .form-group .error-container { display: block; } .error-container { color: #D22630; padding-top: 2px; } /* end fix */ checkbox.ng-invalid .custom-control-indicator, multiplecheckbox.ng-invalid.ng-dirty .custom-control-indicator, flatpickr.ng-invalid.ng-dirty .form-control, datepicker.ng-invalid.ng-dirty .select2-container .select2-selection, radio-button.ng-dirty.ng-invalid .custom-control-indicator, div.ng-invalid.ng-dirty.form-group-valid .custom-control-indicator, div.ng-invalid.ng-dirty.form-group-valid .select2-container .select2-selection { border-color: #D22630; background-color: #FDEDED; } radio-button + radio-button, checkbox + checkbox { margin-left: 15px; } ================================================ FILE: apps/angular-i18next-demo/src/test-setup.ts ================================================ import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; setupZonelessTestEnv(); ================================================ FILE: apps/angular-i18next-demo/tsconfig.app.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "types": ["node"] }, "files": [ "src/main.ts", "src/main.server.ts", "src/server.ts" ], "include": ["src/**/*.d.ts"], "exclude": ["**/*.test.ts", "**/*.spec.ts"] } ================================================ FILE: apps/angular-i18next-demo/tsconfig.editor.json ================================================ { "extends": "./tsconfig.json", "include": ["**/*.ts"], "compilerOptions": { "types": ["jest", "node"] } } ================================================ FILE: apps/angular-i18next-demo/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "files": [], "include": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.spec.json" }, { "path": "./tsconfig.editor.json" } ], "compilerOptions": { "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: apps/angular-i18next-demo/tsconfig.server.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.app.json", "compilerOptions": { "outDir": "../../out-tsc/server", "target": "es2019", "types": [ "node" ], "allowSyntheticDefaultImports": true, }, "files": [ "src/main.server.ts", "server.ts" ], "angularCompilerOptions": { "entryModule": "./src/app/app.server.module#AppServerModule" } } ================================================ FILE: apps/angular-i18next-demo/tsconfig.spec.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["jest", "node"], "allowSyntheticDefaultImports": true, // Typing support for this case "esModuleInterop": true, }, "files": ["src/test-setup.ts"], "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] } ================================================ FILE: decorate-angular-cli.js ================================================ /** * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching * and faster execution of tasks. * * It does this by: * * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. * - Symlinking the ng to nx command, so all commands run through the Nx CLI * - Updating the package.json postinstall script to give you control over this script * * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. * Every command you run should work the same when using the Nx CLI, except faster. * * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. * The Nx CLI simply does some optimizations before invoking the Angular CLI. * * To opt out of this patch: * - Replace occurrences of nx with ng in your package.json * - Remove the script from your postinstall script in your package.json * - Delete and reinstall your node_modules */ const fs = require('fs'); const os = require('os'); const cp = require('child_process'); const isWindows = os.platform() === 'win32'; let output; try { output = require('@nx/workspace').output; } catch (e) { console.warn( 'Angular CLI could not be decorated to enable computation caching. Please ensure @nx/workspace is installed.' ); process.exit(0); } /** * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still * invoke the Nx CLI and get the benefits of computation caching. */ function symlinkNgCLItoNxCLI() { try { const ngPath = './node_modules/.bin/ng'; const nxPath = './node_modules/.bin/nx'; if (isWindows) { /** * This is the most reliable way to create symlink-like behavior on Windows. * Such that it works in all shells and works with npx. */ ['', '.cmd', '.ps1'].forEach((ext) => { if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); }); } else { // If unix-based, symlink cp.execSync(`ln -sf ./nx ${ngPath}`); } } catch (e) { output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message, }); throw e; } } try { symlinkNgCLItoNxCLI(); require('@nrwl/cli/lib/decorate-cli').decorateCli(); output.log({ title: 'Angular CLI has been decorated to enable computation caching.', }); } catch (e) { output.error({ title: 'Decoration of the Angular CLI did not complete successfully', }); } ================================================ FILE: ecosystem.config.js ================================================ const { cwd } = require("process"); module.exports = { apps: [ { name: 'angular-i18next-demo', script: 'server.mjs', cwd: './dist/angular-i18next-demo/server', max_memory_restart: '100M', env: { PM2: true, NODE_ENV: "development" }, env_production: { PM2: true, NODE_ENV: "production", } }, ], }; ================================================ FILE: jest.config.ts ================================================ export default { projects: ['./libs/**/jest.config.ts'], }; ================================================ FILE: jest.preset.js ================================================ const nxPreset = require('@nx/jest/preset').default; module.exports = { ...nxPreset, /* TODO: Update to latest Jest snapshotFormat * By default Nx has kept the older style of Jest Snapshot formats * to prevent breaking of any existing tests with snapshots. * It's recommend you update to the latest format. * You can do this by removing snapshotFormat property * and running tests with --update-snapshot flag. * Example: "nx affected --targets=test --update-snapshot" * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format */ snapshotFormat: { escapeString: true, printBasicPrototype: true }, }; ================================================ FILE: libs/angular-i18next/.eslintrc.json ================================================ { "extends": ["../../.eslintrc.json"], "ignorePatterns": ["!**/*"] } ================================================ FILE: libs/angular-i18next/CHANGELOG.md ================================================ # [21.0.0-3](https://github.com/Romanchuk/angular-i18next/compare/v20.0.1...v21.0.0-3) (2026-01-23) ### Bug Fixes * build ([4f8d746](https://github.com/Romanchuk/angular-i18next/commit/4f8d746e88401bc339250af3383097f6837823cb)) * jest test setup ([253a5eb](https://github.com/Romanchuk/angular-i18next/commit/253a5eb47f55428e81ec43c0c48fff2de3036205)) * units ([2a46eda](https://github.com/Romanchuk/angular-i18next/commit/2a46eda0df9e117357965e7499573a88992fef5b)) # [21.0.0-2](https://github.com/Romanchuk/angular-i18next/compare/v20.0.1...v21.0.0-2) (2026-01-23) ### Bug Fixes * build ([4f8d746](https://github.com/Romanchuk/angular-i18next/commit/4f8d746e88401bc339250af3383097f6837823cb)) * jest test setup ([253a5eb](https://github.com/Romanchuk/angular-i18next/commit/253a5eb47f55428e81ec43c0c48fff2de3036205)) * units ([2a46eda](https://github.com/Romanchuk/angular-i18next/commit/2a46eda0df9e117357965e7499573a88992fef5b)) # [21.0.0-1](https://github.com/Romanchuk/angular-i18next/compare/v20.0.1...v21.0.0-1) (2026-01-23) ### Bug Fixes * build ([4f8d746](https://github.com/Romanchuk/angular-i18next/commit/4f8d746e88401bc339250af3383097f6837823cb)) * jest test setup ([253a5eb](https://github.com/Romanchuk/angular-i18next/commit/253a5eb47f55428e81ec43c0c48fff2de3036205)) * units ([2a46eda](https://github.com/Romanchuk/angular-i18next/commit/2a46eda0df9e117357965e7499573a88992fef5b)) ## [20.0.1](https://github.com/Romanchuk/angular-i18next/compare/v20.0.1-0...v20.0.1) (2025-11-14) ## [20.0.1-0](https://github.com/Romanchuk/angular-i18next/compare/v20.0.0...v20.0.1-0) (2025-11-14) ### Bug Fixes * i18next typings ([9dabeb9](https://github.com/Romanchuk/angular-i18next/commit/9dabeb9ab832413bb488151eb6f74bd65c9a48e8)) # [20.0.0](https://github.com/Romanchuk/angular-i18next/compare/v19.1.1...v20.0.0) (2025-08-22) ### Features * **up version i18next 25:** up version i18next 25 ([4a8d23a](https://github.com/Romanchuk/angular-i18next/commit/4a8d23a016ca4daa5bf06255b24ec10faa004948)) ### BREAKING CHANGES * **up version i18next 25:** up version i18next 25 ## [19.1.1](https://github.com/Romanchuk/angular-i18next/compare/v19.1.1-0...v19.1.1) (2025-08-22) ## [19.1.1-0](https://github.com/Romanchuk/angular-i18next/compare/v19.1.0...v19.1.1-0) (2025-06-22) ### Bug Fixes * ci ([f5c5339](https://github.com/Romanchuk/angular-i18next/commit/f5c5339598c340c2e7f973f90f06dd73c85c621b)) * ci ([563993b](https://github.com/Romanchuk/angular-i18next/commit/563993b507873903a6caf4374508686d8b1b5fd0)) * ci ([443db4e](https://github.com/Romanchuk/angular-i18next/commit/443db4e748522519bd1057947651e724187b1392)) * prepare demo ([a80d2bc](https://github.com/Romanchuk/angular-i18next/commit/a80d2bc19cfed95c5c5a7c4cf40a560f720353af)) * tsconfig.spec ([cea853b](https://github.com/Romanchuk/angular-i18next/commit/cea853bdab70b7199f05896a43deed9c26e26088)) # [19.1.0](https://github.com/Romanchuk/angular-i18next/compare/v19.0.1...v19.1.0) (2025-03-03) ### Bug Fixes * browser build and serve ([d9b73f2](https://github.com/Romanchuk/angular-i18next/commit/d9b73f20b82f5ba1d0692c40787e47032f30367e)) * build ([300e975](https://github.com/Romanchuk/angular-i18next/commit/300e975a1c6072e0564c5fff50e4bf1ddb7f4751)) * build ([ed46b54](https://github.com/Romanchuk/angular-i18next/commit/ed46b542564070d462b8d5b3c0b92b1e4a04ac55)) * forms ([3781a6b](https://github.com/Romanchuk/angular-i18next/commit/3781a6b7786b4666a77402cefec13860dacc4b05)) * new provide ([f17a4f8](https://github.com/Romanchuk/angular-i18next/commit/f17a4f863b422c667d76457bc4c996760a3c1ca0)) * specs ([5e12691](https://github.com/Romanchuk/angular-i18next/commit/5e12691a4dc091f251d0e254f36d8fa568a22158)) * ssr ([822b4d1](https://github.com/Romanchuk/angular-i18next/commit/822b4d1950acdadc2a5dabcbdd3a9983d9b15acd)) * tests ([7bba2bc](https://github.com/Romanchuk/angular-i18next/commit/7bba2bc353eab68a867c8bef62e7da28b9557f58)) # [19.1.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v19.0.1...v19.1.0-beta) (2025-03-01) ### Bug Fixes * browser build and serve ([d9b73f2](https://github.com/Romanchuk/angular-i18next/commit/d9b73f20b82f5ba1d0692c40787e47032f30367e)) * build ([300e975](https://github.com/Romanchuk/angular-i18next/commit/300e975a1c6072e0564c5fff50e4bf1ddb7f4751)) * build ([ed46b54](https://github.com/Romanchuk/angular-i18next/commit/ed46b542564070d462b8d5b3c0b92b1e4a04ac55)) * forms ([3781a6b](https://github.com/Romanchuk/angular-i18next/commit/3781a6b7786b4666a77402cefec13860dacc4b05)) * new provide ([f17a4f8](https://github.com/Romanchuk/angular-i18next/commit/f17a4f863b422c667d76457bc4c996760a3c1ca0)) * specs ([5e12691](https://github.com/Romanchuk/angular-i18next/commit/5e12691a4dc091f251d0e254f36d8fa568a22158)) * ssr ([822b4d1](https://github.com/Romanchuk/angular-i18next/commit/822b4d1950acdadc2a5dabcbdd3a9983d9b15acd)) * tests ([7bba2bc](https://github.com/Romanchuk/angular-i18next/commit/7bba2bc353eab68a867c8bef62e7da28b9557f58)) ## [19.0.1](https://github.com/Romanchuk/angular-i18next/compare/v19.0.0...v19.0.1) (2025-01-11) # [19.0.0](https://github.com/Romanchuk/angular-i18next/compare/v19.0.0-0...v19.0.0) (2025-01-11) ### Bug Fixes * app ([b634439](https://github.com/Romanchuk/angular-i18next/commit/b63443967f5da8bb470c09871562d431a2af2cf3)) # [19.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v18.0.0...v19.0.0-0) (2025-01-11) ### Bug Fixes * pages ([27d6e56](https://github.com/Romanchuk/angular-i18next/commit/27d6e5644d9c9d50d88fc7ab7bdc3d23f94285ef)) * tests ([29abe20](https://github.com/Romanchuk/angular-i18next/commit/29abe20a002ec9912af0388abf4a3f5eb0a97d90)) # [18.0.0](https://github.com/Romanchuk/angular-i18next/compare/v18.0.0-0...v18.0.0) (2024-06-03) # [18.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v17.0.2...v18.0.0-0) (2024-06-03) ## [17.0.2](https://github.com/Romanchuk/angular-i18next/compare/v17.0.1...v17.0.2) (2024-06-03) ### Bug Fixes * tests ([50c5f38](https://github.com/Romanchuk/angular-i18next/commit/50c5f38b122755d1a33fa0db0a59f7be201f26d3)) ## [17.0.1](https://github.com/Romanchuk/angular-i18next/compare/v17.0.0...v17.0.1) (2023-12-07) ### Bug Fixes * nx and jest setup ([0e6a61d](https://github.com/Romanchuk/angular-i18next/commit/0e6a61dd882f103b40bce38577fd7e7bcf44309a)) # [17.0.0](https://github.com/Romanchuk/angular-i18next/compare/v17.0.0-1...v17.0.0) (2023-12-01) # [17.0.0-1](https://github.com/Romanchuk/angular-i18next/compare/v17.0.0-0...v17.0.0-1) (2023-11-28) # [17.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v16.0.0...v17.0.0-0) (2023-11-27) # [16.0.0](https://github.com/Romanchuk/angular-i18next/compare/v16.0.0-0...v16.0.0) (2023-06-09) # [16.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v15.0.5...v16.0.0-0) (2023-06-09) ### Bug Fixes * package.json ([4f3d909](https://github.com/Romanchuk/angular-i18next/commit/4f3d909321651faa4f6406cf300582ce8a8001ce)) * test ([991dbca](https://github.com/Romanchuk/angular-i18next/commit/991dbca3c5ad9807b4884727065effe456f56b61)) * test script ([b075485](https://github.com/Romanchuk/angular-i18next/commit/b0754858d865a5e4aaa80ac18fb264d276f26787)) ## [15.0.5](https://github.com/Romanchuk/angular-i18next/compare/v15.0.4...v15.0.5) (2023-01-24) ### Bug Fixes * [#101](https://github.com/Romanchuk/angular-i18next/issues/101) ([e7d095a](https://github.com/Romanchuk/angular-i18next/commit/e7d095a2336b663f95e08ddadbe65de6cf8b191c)) ## [15.0.4](https://github.com/Romanchuk/angular-i18next/compare/v15.0.3...v15.0.4) (2023-01-16) ## [15.0.3](https://github.com/Romanchuk/angular-i18next/compare/v15.0.1...v15.0.3) (2023-01-16) ## [15.0.2](https://github.com/Romanchuk/angular-i18next/compare/v15.0.1...v15.0.2) (2023-01-16) ## [15.0.1](https://github.com/Romanchuk/angular-i18next/compare/v15.0.0...v15.0.1) (2023-01-16) ### Bug Fixes * [#97](https://github.com/Romanchuk/angular-i18next/issues/97) "strictNullChecks": true ([92d8205](https://github.com/Romanchuk/angular-i18next/commit/92d8205003908cb89587a99268983184fa4d6316)) * t signature ([029478a](https://github.com/Romanchuk/angular-i18next/commit/029478a5582afe626d892b8e1ee59c7d08e544f5)) # [15.0.0](https://github.com/Romanchuk/angular-i18next/compare/v15.0.0-1...v15.0.0) (2023-01-16) # [15.0.0-1](https://github.com/Romanchuk/angular-i18next/compare/v15.0.0-0...v15.0.0-1) (2023-01-16) # [15.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v14.2.0...v15.0.0-0) (2022-12-22) ### Bug Fixes * pages deploy ([df17d58](https://github.com/Romanchuk/angular-i18next/commit/df17d58fcfd3a18c9862064d43e33e799effb84e)) # [14.2.0](https://github.com/Romanchuk/angular-i18next/compare/v14.2.0-1...v14.2.0) (2022-11-22) # [14.2.0-1](https://github.com/Romanchuk/angular-i18next/compare/v14.2.0-0...v14.2.0-1) (2022-11-21) ### Bug Fixes * factory type ([7a0c62c](https://github.com/Romanchuk/angular-i18next/commit/7a0c62cac779149c9b26b8cbd502538457d7159e)) * missed import ([e793bc5](https://github.com/Romanchuk/angular-i18next/commit/e793bc511227f887d677bd2f45f3b2b173977a8e)) * specs default import ([532dd94](https://github.com/Romanchuk/angular-i18next/commit/532dd94bc23f57f0e3e0256a3534f0c48381f12f)) # [14.2.0-0](https://github.com/Romanchuk/angular-i18next/compare/v14.1.0...v14.2.0-0) (2022-11-17) ### Bug Fixes * i18next instance ([93e48b6](https://github.com/Romanchuk/angular-i18next/commit/93e48b646c486d9e157447a704d88925e05957c6)) * jest default import ([cea6677](https://github.com/Romanchuk/angular-i18next/commit/cea66776dd6af912dfdfff500f14bbe8e17c81e7)) * link to global i18next ([4439d3a](https://github.com/Romanchuk/angular-i18next/commit/4439d3a19ef3d2bec7fcb296b8f3eaebbd1af6a8)) * tests ([78bc41e](https://github.com/Romanchuk/angular-i18next/commit/78bc41e3a4eca2b63f28513120f0ef9863d2a21a)) # [14.1.0](https://github.com/Romanchuk/angular-i18next/compare/v14.0.5-6...v14.1.0) (2022-11-09) ### Bug Fixes * messages ([1cbebea](https://github.com/Romanchuk/angular-i18next/commit/1cbebea76d188057070442d6ffbd62bbf44ccf09)) * setup ([86de471](https://github.com/Romanchuk/angular-i18next/commit/86de471f898068274355d87f1c084ce76bb72b91)) ## [14.0.5-6](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.5-6) (2022-11-07) ### Bug Fixes * async funcs ([25e2c24](https://github.com/Romanchuk/angular-i18next/commit/25e2c24ba8c0cce1d4b235d1449fb554937b6fe4)) * build ([aafe356](https://github.com/Romanchuk/angular-i18next/commit/aafe356437287a4a2e30668a726c18e5a118cc04)) * cpy ([2a2460e](https://github.com/Romanchuk/angular-i18next/commit/2a2460e8e53f5a18185bf979318e0be77019f82e)) * Fixes i18next format call with options undefined ([ccfc6e1](https://github.com/Romanchuk/angular-i18next/commit/ccfc6e1583fcb4a5a8b591ac0e3e8cf95ce675eb)) * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) * test run ([cc2ca1c](https://github.com/Romanchuk/angular-i18next/commit/cc2ca1c6dbef4fe1126de9ffb7c73f0dc2be9062)) * tests ([0b868fc](https://github.com/Romanchuk/angular-i18next/commit/0b868fc25b9c7ff4902308425e066835d117596d)) ## [14.0.5-3](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.5-3) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.5-2](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.5-2) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.5-1](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.5-1) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.5-0](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.5-0) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.4-0](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.4-0) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.3](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.3) (2022-07-05) ### Bug Fixes * package ([9b03de0](https://github.com/Romanchuk/angular-i18next/commit/9b03de01029432a679b84f0b535c8c0986a2ce49)) ## [14.0.2](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.2) (2022-07-05) ## [14.0.1](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0...v14.0.1) (2022-07-05) # [14.0.0](https://github.com/Romanchuk/angular-i18next/compare/v14.0.0-0...v14.0.0) (2022-07-04) # [14.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v11.0.0...v14.0.0-0) (2022-06-14) ### Bug Fixes * [#81](https://github.com/Romanchuk/angular-i18next/issues/81) ([820a9e8](https://github.com/Romanchuk/angular-i18next/commit/820a9e8ab0d1b3f0c4757c4f7096dce4e1f844ed)) # [11.0.0](https://github.com/Romanchuk/angular-i18next/compare/v11.0.0-0...v11.0.0) (2022-01-28) # [11.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.3.0...v11.0.0-0) (2022-01-04) ### Bug Fixes * np dist ([670004f](https://github.com/Romanchuk/angular-i18next/commit/670004f2b1c41e0de88708767563b960bd88a3e6)) # [10.3.0](https://github.com/Romanchuk/angular-i18next/compare/v10.3.0-0...v10.3.0) (2021-06-15) # [10.3.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.2.0...v10.3.0-0) (2021-06-15) # [10.2.0](https://github.com/Romanchuk/angular-i18next/compare/v10.2.0-0...v10.2.0) (2021-05-12) # [10.2.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.1.0...v10.2.0-0) (2021-05-12) ### Features * i18next v20+ support ([0327a7c](https://github.com/Romanchuk/angular-i18next/commit/0327a7c9f35140f0c8e098d9d1528b6e7303a8d0)) # [10.1.0](https://github.com/Romanchuk/angular-i18next/compare/v10.1.0-0...v10.1.0) (2021-03-01) # [10.1.0-0](https://github.com/Romanchuk/angular-i18next/compare/v10.0.1...v10.1.0-0) (2021-03-01) ### Bug Fixes * **I18NextEagerPipe:** ensure changing PipeOptions returns correct translated value a not cached one with different PipeOptions but same key ([4a6d375](https://github.com/Romanchuk/angular-i18next/commit/4a6d375181dda41399c58f7644b97d3755acf84f)) ## [10.0.1](https://github.com/Romanchuk/angular-i18next/compare/v10.0.1-beta...v10.0.1) (2020-12-21) ## [10.0.1-beta](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0...v10.0.1-beta) (2020-12-21) # [10.0.0](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-2...v10.0.0) (2020-07-06) # [10.0.0-2](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-1...v10.0.0-2) (2020-07-06) # [10.0.0-1](https://github.com/Romanchuk/angular-i18next/compare/v10.0.0-0...v10.0.0-1) (2020-07-06) # [10.0.0-0](https://github.com/Romanchuk/angular-i18next/compare/v9.0.1...v10.0.0-0) (2020-07-06) ## [9.0.1](https://github.com/Romanchuk/angular-i18next/compare/v9.0.0...v9.0.1) (2020-02-25) ### Bug Fixes * pass translate options ([4cfe42c](https://github.com/Romanchuk/angular-i18next/commit/4cfe42c)) # [9.0.0](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.3...v9.0.0) (2020-02-20) # [8.1.0-beta.3](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.2...v8.1.0-beta.3) (2020-02-20) # [8.1.0-beta.2](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta.1...v8.1.0-beta.2) (2020-02-20) # [8.1.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v8.1.0-beta...v8.1.0-beta.1) (2020-02-20) ### Features * improved typings ([214e35d](https://github.com/Romanchuk/angular-i18next/commit/214e35d)) # [8.1.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.2.0-beta...v8.1.0-beta) (2020-02-20) ## [8.0.1](https://github.com/Romanchuk/angular-i18next/compare/v8.0.1-beta.0...v8.0.1) (2020-02-18) ## [8.0.1-beta.0](https://github.com/Romanchuk/angular-i18next/compare/v8.0.1-beta...v8.0.1-beta.0) (2020-02-18) ## [8.0.1-beta](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0...v8.0.1-beta) (2020-02-18) # [8.0.0](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0-beta.1...v8.0.0) (2020-02-14) # [8.0.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v8.0.0-beta...v8.0.0-beta.1) (2020-02-13) # [8.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.2.0-beta...v8.0.0-beta) (2020-02-13) # [7.2.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v7.0.0...v7.2.0-beta) (2020-01-28) ### Bug Fixes * I18NextEagerPipe ([8dbefe1](https://github.com/Romanchuk/angular-i18next/commit/8dbefe1)) # [7.0.0](https://github.com/Romanchuk/angular-i18next/compare/v6.1.0...v7.0.0) (2019-06-05) # [6.1.0](https://github.com/Romanchuk/angular-i18next/compare/v6.1.0-beta...v6.1.0) (2019-05-27) # [6.1.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v6.0.1...v6.1.0-beta) (2019-05-25) ## [6.0.1](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0...v6.0.1) (2019-03-11) # [6.0.0](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0-beta.0...v6.0.0) (2019-02-10) # [6.0.0-beta.0](https://github.com/Romanchuk/angular-i18next/compare/v6.0.0-beta...v6.0.0-beta.0) (2019-02-10) # [6.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v5.0.6...v6.0.0-beta) (2019-02-10) ## [5.0.6](https://github.com/Romanchuk/angular-i18next/compare/v5.0.5...v5.0.6) (2018-12-03) ## [5.0.5](https://github.com/Romanchuk/angular-i18next/compare/v5.0.4...v5.0.5) (2018-12-03) ## [5.0.4](https://github.com/Romanchuk/angular-i18next/compare/v5.0.3...v5.0.4) (2018-12-03) ## [5.0.3](https://github.com/Romanchuk/angular-i18next/compare/v5.0.2...v5.0.3) (2018-12-03) ## [5.0.2](https://github.com/Romanchuk/angular-i18next/compare/v5.0.1...v5.0.2) (2018-12-03) ### Bug Fixes * package.json ([54a8c37](https://github.com/Romanchuk/angular-i18next/commit/54a8c37)) ## [5.0.1](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0...v5.0.1) (2018-11-28) # [5.0.0](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0-beta2...v5.0.0) (2018-11-28) # [5.0.0-beta2](https://github.com/Romanchuk/angular-i18next/compare/v5.0.0-beta...v5.0.0-beta2) (2018-11-28) # [5.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v4.0.0...v5.0.0-beta) (2018-11-28) ### Bug Fixes * docs ([220a0b8](https://github.com/Romanchuk/angular-i18next/commit/220a0b8)) # [4.0.0](https://github.com/Romanchuk/angular-i18next/compare/v4.0.0-beta...v4.0.0) (2018-06-25) In v4 passed through most of i18next api methods 1. Update angular to v6+ 2. Update rxjs to v6.2.0+ # [4.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v3.4.2...v4.0.0-beta) (2018-06-11) ## [3.4.2](https://github.com/Romanchuk/angular-i18next/compare/v3.4.1...v3.4.2) (2018-05-05) ## [3.4.1](https://github.com/Romanchuk/angular-i18next/compare/v3.4.0...v3.4.1) (2018-04-29) - default formater fixes # [3.4.0](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0...v3.4.0) (2018-04-29) - i18next v11 support - fix: [format pipe](https://github.com/Romanchuk/angular-i18next/issues/15) # [3.3.0](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0-beta.2...v3.3.0) (2018-03-12) - added umd bundle - comments cleanup - updated dev dependencies # [3.3.0-beta.2](https://github.com/Romanchuk/angular-i18next/compare/v3.3.0-beta.1...v3.3.0-beta.2) (2018-03-12) # [3.3.0-beta.1](https://github.com/Romanchuk/angular-i18next/compare/v3.2.0...v3.3.0-beta.1) (2018-02-04) # [3.2.0](https://github.com/Romanchuk/angular-i18next/compare/v3.1.1...v3.2.0) (2018-01-17) ### Bug Fixes * [aot build failed](Romanchuk/angular-i18next#10) ### Breaking changes Removed parameter 'localizeTitle' from forRoot method. You need to manually resolve Title as I18NextTitle for same behavior. ## [3.1.1](https://github.com/Romanchuk/angular-i18next/compare/v3.1.0...v3.1.1) (2018-01-01) ### Bug Fixes * bug namespace fallback ([a16b067](https://github.com/Romanchuk/angular-i18next/commit/a16b067)) * conventional-github-releaser run ([df3bb84](https://github.com/Romanchuk/angular-i18next/commit/df3bb84)) # [3.1.0](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0...v3.1.0) (2017-12-22) It is possible to pass array of namespaces (or scopes). [Key would fallback](https://www.i18next.com/api.html#t) to next namespace in array if the previous failed to resolve. `[feature.validators:key, validators:key]` ```typescript { provide: I18NEXT_NAMESPACE, useValue: ['feature.validators', 'validators'] } ``` # [3.0.0](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0-alpha.2...v3.0.0) (2017-12-15) # [3.0.0-alpha.2](https://github.com/Romanchuk/angular-i18next/compare/v3.0.0-alpha...v3.0.0-alpha.2) (2017-12-05) # [3.0.0-alpha](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0...v3.0.0-alpha) (2017-11-27) # [2.0.0](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0-beta2...v2.0.0) (2017-11-14) # [2.0.0-beta2](https://github.com/Romanchuk/angular-i18next/compare/v2.0.0-beta...v2.0.0-beta2) (2017-11-05) # [2.0.0-beta](https://github.com/Romanchuk/angular-i18next/compare/v1.1.0...v2.0.0-beta) (2017-11-05) # [1.1.0](https://github.com/Romanchuk/angular-i18next/compare/v1.0.2...v1.1.0) (2017-11-04) ## [1.0.2](https://github.com/Romanchuk/angular-i18next/compare/v1.0.1...v1.0.2) (2017-09-22) ## [1.0.1](https://github.com/Romanchuk/angular-i18next/compare/v1.0.0...v1.0.1) (2017-09-21) # [1.0.0](https://github.com/Romanchuk/angular-i18next/compare/v0.2.4...v1.0.0) (2017-09-21) ## [0.2.4](https://github.com/Romanchuk/angular-i18next/compare/v0.2.3...v0.2.4) (2017-06-29) ## [0.2.3](https://github.com/Romanchuk/angular-i18next/compare/v0.2.2...v0.2.3) (2017-06-29) ## [0.2.2](https://github.com/Romanchuk/angular-i18next/compare/v0.2.1...v0.2.2) (2017-06-29) ### Bug Fixes * **I18NextService:** context-safe calls of i18next methods ([455a07d](https://github.com/Romanchuk/angular-i18next/commit/455a07d)) ## [0.2.1](https://github.com/Romanchuk/angular-i18next/compare/v0.2.0...v0.2.1) (2017-06-29) ### Bug Fixes * **package:** return back required exports ([fb7ead6](https://github.com/Romanchuk/angular-i18next/commit/fb7ead6)) # [0.2.0](https://github.com/Romanchuk/angular-i18next/compare/0.1.0...0.2.0) (2017-06-28) ### Features * **package:** AOT support added ([fc1f66d](https://github.com/Romanchuk/angular-i18next/commit/fc1f66d)) ================================================ FILE: libs/angular-i18next/README.md ================================================ [![npm version](https://badge.fury.io/js/angular-i18next.svg)](https://badge.fury.io/js/angular-i18next) [![Downloads](http://img.shields.io/npm/dm/angular-i18next.svg)](https://npmjs.org/package/angular-i18next) [![Build Status](https://travis-ci.com/Romanchuk/angular-i18next.svg?branch=master)](https://travis-ci.com/Romanchuk/angular-i18next) [![Coverage Status](https://coveralls.io/repos/github/Romanchuk/angular-i18next/badge.svg?branch=master)](https://coveralls.io/github/Romanchuk/angular-i18next?branch=master) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![Dependency Status](https://david-dm.org/Romanchuk/angular-i18next.svg)](https://david-dm.org/Romanchuk/angular-i18next) [![devDependency Status](https://david-dm.org/Romanchuk/angular-i18next/dev-status.svg)](https://david-dm.org/Romanchuk/angular-i18next?type=dev) [![paypal](https://img.shields.io/badge/say_thanks-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) [![GitHub stars](https://img.shields.io/github/stars/romanchuk/angular-i18next?label=Please%20star%20repo%21&style=social)](https://github.com/romanchuk/angular-i18next) # angular-i18next [i18next](http://i18next.com/) v8.4+ integration with [angular](https://angular.io/) v2.0+ [Live DEMO](https://romanchuk.github.io/angular-i18next-demo/) - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Cookbook](#cookbook) - [Deep integration](#deep-integration) - [In-project testing](#in-project-testing) - [Demo](#demo) - [Articles](#articles) - [Support project](#support-on-beerpay) # Features - Native i18next [options](https://www.i18next.com/configuration-options.html) - Promise initialization - [i18next plugin](https://www.i18next.com/plugins-and-utils.html#plugins) support - Events support - Namespaces lazy load - i18next native [format](https://www.i18next.com/api.html#format) support - document.title localization - Error handling strategies - i18next namespaces and scopes (prefixes) for angular modules and components - AOT support - [Angular Package Format](https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/preview) support [Related packages](#deep-integration) also has implementations for: - Reactive forms validators localization - Http error message localizer # Cheers! Hey dude! Help me out for a couple of :beers:! Поддержи проект - угости автора кружечкой пива! [![paypal](https://img.shields.io/badge/paypal-%2410-green)](https://www.paypal.com/paypalme2/sergeyromanchuk/10USD) # Installation **1.** Install package ``` npm install i18next --save npm install angular-i18next --save ``` **2.** Import I18NextModule to AppModule ```typescript import { I18NextModule } from 'angular-i18next'; @NgModule({ bootstrap: [ AppComponent ], declarations: [ AppComponent ], import: [ I18NextModule.forRoot() ] }) export class AppModule {} ``` **3.** Import I18NextModule.forRoot() to AppModule and setup provider with "init" method (use native [options](https://www.i18next.com/configuration-options.html)). Angular would not load until i18next initialize event fired ```typescript export function appInit(i18next: ITranslationService) { return () => i18next.init({ whitelist: ['en', 'ru'], fallbackLng: 'en', debug: true, returnEmptyString: false, ns: [ 'translation', 'validation', 'error' ], }); } export function localeIdFactory(i18next: ITranslationService) { return i18next.language; } export const I18N_PROVIDERS = [ { provide: APP_INITIALIZER, useFactory: appInit, deps: [I18NEXT_SERVICE], multi: true }, { provide: LOCALE_ID, deps: [I18NEXT_SERVICE], useFactory: localeIdFactory }]; ``` ```typescript @NgModule({ imports: [ ... I18NextModule.forRoot() ], providers: [ ... I18N_PROVIDERS, ], bootstrap: [AppComponent] }) export class AppModule { } ``` # Usage ### Pipes Use "i18next" pipe to translate key:
{{ 'test' | i18next }}
Passing ["t options"](https://www.i18next.com/api.html#t):
{{ 'test' | i18next: { count: 5, nsSeparator: '#' } }}
Trigger native i18next [format method](https://www.i18next.com/formatting.html) by using I18NextFormatPipe or I18NextPipe with option 'format': `{{ 'any_key' | i18next | i18nextFormat }}` `{{ 'any_key' | i18next: { format: 'cap' } }}` `{{ 'any_key' | i18nextCap }}` **Note:** Using "i18nextCap" you will get the same result as `i18next: { format: 'cap' }` **REMEMBER** that format will not work until you set "interpolation.format" function in i18next options. I18NextModule has static method `static interpolationFormat(customFormat: Function = null): Function` that can be used as default interpolation format function (it provides 'upper', 'cap' and 'lower' formatters). You also can pass your custom function to be called after I18NextModule formatters: ```typescript const i18nextOptions = { whitelist: ['en', 'ru'], ns: [ 'translation', 'validation', 'error', ], interpolation: { format: I18NextModule.interpolationFormat((value, format, lng) => { if(value instanceof Date) return moment(value).format(format); return value; }); // format: I18NextModule.interpolationFormat() } }; ``` **i18nextEager pipe** This is the impure analog of *i18next pipe* that is subscribed to language change, it will change string right away to choosen language (without reloading page). **Warning!**: Use i18nextEager only in combine with [OnPush change detection strategy](https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4), or else (default change detection) each pipe will retrigger more than one time (cause of performance issues). Subscribing to event observables: ```typescript this.i18NextService.events.languageChanged.subscribe(lang => { // do something }) ``` Add a provider to module/component if you want to prefix child i18next keys: ```typescript { provide: I18NEXT_NAMESPACE, useValue: 'feature' // set 'feature:' prefix } ``` ```typescript { provide: I18NEXT_SCOPE, useValue: 'person' // set 'person.' prefix } ``` Since v3.1.0+ it is possible to pass array of namespaces (or scopes). [Key would fallback](https://www.i18next.com/api.html#t) to next namespace in array if the previous failed to resolve. `[feature_validators:key, validators:key]` ```typescript { provide: I18NEXT_NAMESPACE, useValue: ['feature_validators', 'validators'] } ``` _NOTE:_ **Do NOT** use default (or custom) i18next delimiters in namespace names. ### Document title If you want to turn on document title localization resolve Title as `I18NextTitle` imported from 'angular-i18next': ```typescript { provide: Title, useClass: I18NextTitle } ``` Also you can implement your own Title service with specific behavior. Inject `I18NextPipe` (or `I18NextService`) to service/component: ```typescript import { Injectable, Inject } from '@angular/core'; import { Title, DOCUMENT } from '@angular/platform-browser'; import { I18NextPipe } from 'angular-i18next'; @Injectable() export class I18NextTitle extends Title { constructor(private i18nextPipe: I18NextPipe, @Inject(DOCUMENT) doc) { super(doc); } setTitle(value: string) { return super.setTitle(this.translate(value)); } private translate(text: string) { return this.i18nextPipe.transform(text, { format: 'cap'}); } } ``` Ways to use I18NextService in your code: > **Warning:** Injection of **I18NextService** is possible, but it would not consider I18NEXT_NAMESPACE and I18NEXT_SCOPE providers. There are 2 possible reasons to inject **I18NextService**: initialization and subscription to its events. In all other cases inject **I18NextPipe**. 1) **Recommended way:** Inject via **I18NEXT_SERVICE** token. By default it will inject instance of **I18NextService**. ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) ``` 2) Legacy way: Inject via type ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, private i18NextService: I18NextService) ``` ### Error handling Error handling is now configurable: 1) By default i18next promise will use NativeErrorHandlingStrategy. I18Next would be always resolve succesfully. Error could be get from 'then' handler parameter. 2) Set StrictErrorHandlingStrategy to reject load promises (init, languageChange, loadNamespaces) on first load fail (this was default in v2 but changed to fit [native i18next behavior](https://github.com/Romanchuk/angular-i18next/issues/9): `I18NextModule.forRoot({ errorHandlingStrategy: StrictErrorHandlingStrategy })` ### Lazy loading Use I18NEXT_NAMESPACE_RESOLVER in your routes to to load i18next namespace. Note: It is not neccesary to register lazy loading namespaces in global i18next options. ``` { path: 'rich_form', loadChildren: 'app/features/rich_form_feature/RichFormFeatureModule#RichFormFeatureModule', data: { i18nextNamespaces: ['feature.rich_form'] }, resolve: { i18next: I18NEXT_NAMESPACE_RESOLVER } }, ``` Use I18NextService.loadNamespaces() method to load namespaces in code. # Cookbook ### i18next plugin support ```typescript import { I18NextModule, ITranslationService, I18NEXT_SERVICE } from 'angular-i18next'; // import Backend from 'i18next-xhr-backend'; //for i18next < 20.0.0 import HttpApi from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; ... i18next.use(HttpApi) .use(LanguageDetector) .init(i18nextOptions) ``` ### Initialize i18next before angular application Angular would not load until i18next initialize event fired ```typescript export function appInit(i18next: ITranslationService) { return () => i18next.init(); } export function localeIdFactory(i18next: ITranslationService) { return i18next.language; } export const I18N_PROVIDERS = [ { provide: APP_INITIALIZER, useFactory: appInit, deps: [I18NEXT_SERVICE], multi: true }, { provide: LOCALE_ID, deps: [I18NEXT_SERVICE], useFactory: localeIdFactory }]; ``` ### Document title update on language or route change ```typescript export class AppComponent implements OnInit { constructor(private router: Router, private title: Title, @Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService) { // page title subscription // https://toddmotto.com/dynamic-page-titles-angular-2-router-events#final-code this.router.events .filter(event => event instanceof NavigationEnd) .map(() => this.router.routerState.root) .map(route => { while (route.firstChild) route = route.firstChild; return route; }) .filter(route => route.outlet === 'primary') .mergeMap(route => route.data) .subscribe((event) => this.updatePageTitle(event['title'])); } ngOnInit() { this.i18NextService.events.languageChanged.subscribe(lang => { let root = this.router.routerState.root; if (root != null && root.firstChild != null) { let data: any = root.firstChild.data; this.updatePageTitle(data && data.value && data.value.title); } }); } updatePageTitle(title: string): void { let newTitle = title || 'application_title'; this.title.setTitle(newTitle); } } ``` Routes example: ```typescript const appRoutes: Routes = [ { path: 'error', component: AppErrorComponent, data: { title: 'error:error_occured' } }, { path: 'denied', component: AccessDeniedComponent, data: { title: 'error:access_denied' } } ]; ``` # New angular version released, but angular-i18next is not released YET!!! Angular releases mostly don't break angular-i18next, but we cannot tell ahead that current version of `angular-i18next` will work correctly with latest angular version. You can override an angular-i18next `peerDependencies` in your `package.json` on your **own risk**: ```json "overrides": { "angular-i18next": { "@angular/common": "*", "@angular/core": "*", "@angular/platform-browser": "*" } } ``` # Deep integration List of packages to integrate angular and i18next more deeply: - [angular-validation-message](https://github.com/Romanchuk/angular-validation-message) - angular [reactive form validators](https://angular.io/guide/reactive-forms#step-2-making-a-field-required) integration (and [angular-validation-message-i18next ](https://github.com/Romanchuk/angular-validation-message-i18next) is i18next bridge to it). It gives you possibility to localize form validators and it automatically puts localized validator error message to markup (if there is one). - [angular-i18next-error-interceptor](https://github.com/LCGroupIT/angular-i18next-error-interceptor) - allows you to set default errot messages for non-200 http status responses. So if the back-end didn't specify { message: 'some error' } in a response (sort of contract with our backend) interceptor will check response status code and will fill { message: 'Server is not available. Please try again.' }. Also package includes pipe where you can pass HttpErrorResponse and it will return error message whenever it's back-end message or our localized message. # In-project testing You might want to unit-test project components that are using i18next pipes Example tests setup: [/tests/projectTests/projectTests.spec.ts](https://github.com/Romanchuk/angular-i18next/blob/master/tests/projectTests/projectTests.spec.ts) # Demo [Live DEMO](https://romanchuk.github.io/angular-i18next-demo/) Demo app source code available here: https://github.com/Romanchuk/angular-i18next-demo # Articles - [Angular L10n with I18next](https://phrase.com/blog/posts/angular-l10n-with-i18next/) - [Best Libraries for Angular I18n](https://phrase.com/blog/posts/best-libraries-for-angular-i18n/) ================================================ FILE: libs/angular-i18next/forms/ng-package.json ================================================ {} ================================================ FILE: libs/angular-i18next/forms/src/components/validation-message.component.ts ================================================ import { Component, ViewEncapsulation, computed, effect, inject, input, signal } from "@angular/core"; import { AbstractControl, NgControl } from "@angular/forms"; import { I18NEXT_NAMESPACE, I18NextCapPipe } from "angular-i18next"; import { ValidationMessage } from "../models"; import { combineLatest, startWith, Subscription, tap } from "rxjs"; @Component({ selector: 'i18next-validation-message', template: `
{{ i18nextKey() | i18nextCap: firstMessage().params }}
`, styles: [` .i18next-validation-message { display: none; width: 100%; position: relative; } .i18next-validation-message.standalone, .ng-dirty.ng-invalid + .i18next-validation-message { display: block; } `], encapsulation: ViewEncapsulation.None, standalone: true, imports: [I18NextCapPipe], host: { 'class': 'i18next-validation-message' } }) export class I18NextValidationMessageComponent { private readonly i18nextNamespace = inject(I18NEXT_NAMESPACE); private readonly validationString = 'validation'; private readonly manualSettedFor = signal(null); private readonly messages = signal([]); private controlChangesSub: Subscription | null = null; for = input(null); setFor(control: NgControl) { this.manualSettedFor.set(control); } constructor() { effect(() => { this.controlChangesSub?.unsubscribe(); this.messages.set([]); const control = this.control(); if (!control?.valueChanges) { return; } control.statusChanges?.pipe(tap((s) => console.log(s))).subscribe(); this.controlChangesSub = combineLatest([control.valueChanges, control.statusChanges]).pipe( startWith([control.value, control.status]), tap(() => { this.messages.set(this.getErrorMessages(control)) }) ).subscribe(); }); } protected readonly control = computed(() => this.for() ?? this.manualSettedFor()); protected readonly firstMessage = computed(() => this.messages()[0] ?? new ValidationMessage() ); protected readonly controlPath = computed(() => this.control()?.path?.join('.') ?? '' ); protected readonly i18nextKey = computed(() => { if (!this.firstMessage().key) return ''; const specificKey = [ this.validationString, ['control_specific', this.controlPath(), this.firstMessage().key].join('.') ].join(':'); const commonKey = [this.validationString, this.firstMessage().key].join(':'); const i18nextKeys: string[] = []; if (this.i18nextNamespace && this.i18nextNamespace !== this.validationString) { i18nextKeys.push([this.i18nextNamespace, specificKey].join('.')); i18nextKeys.push([this.i18nextNamespace, commonKey].join('.')); } i18nextKeys.push(specificKey); i18nextKeys.push(commonKey); return i18nextKeys; }); private getErrorMessages(control: NgControl) { const errors = control.errors; if (!errors) return []; return Object.entries(errors ?? {}).map(([key, value]) => { let params = null; if (value instanceof Object) { params = value; } else if (value !== true) { params = { [key]: value }; } return new ValidationMessage(key, params); }); } } ================================================ FILE: libs/angular-i18next/forms/src/directives/validation-message.directive.ts ================================================ import { AfterViewInit, ComponentRef, Directive, inject, OnDestroy, ViewContainerRef } from "@angular/core"; import { FormControlName, NgControl } from "@angular/forms"; import { I18NextValidationMessageComponent } from "../components/validation-message.component"; @Directive({ selector: '[formControlName][i18nextValidationMessage],[formGroupName][i18nextValidationMessage],[formArrayName][i18nextValidationMessage]', standalone: true }) export class I18NextValidationMessageDirective implements AfterViewInit, OnDestroy { private readonly viewContainer = inject(ViewContainerRef); private readonly formControlName = inject(FormControlName, { optional: true }); private validationMessageComponent: ComponentRef | null = null; ngAfterViewInit(): void { this.detach(); this.validationMessageComponent = this.viewContainer.createComponent(I18NextValidationMessageComponent); const control: NgControl = this.formControlName!; this.validationMessageComponent.instance.setFor(control); this.validationMessageComponent.changeDetectorRef.detectChanges(); } ngOnDestroy(): void { this.detach(); } private detach(): void { if (this.validationMessageComponent?.changeDetectorRef) { this.validationMessageComponent.changeDetectorRef.detach(); this.validationMessageComponent = null; } } } ================================================ FILE: libs/angular-i18next/forms/src/models.ts ================================================ /** * Represents a validation message with translation key and parameters */ export interface ValidationMessageParams { [key: string]: unknown; } export class ValidationMessage { constructor( public readonly key = '', public readonly params?: ValidationMessageParams ) {} } ================================================ FILE: libs/angular-i18next/forms/src/public_api.ts ================================================ export { ValidationMessage, ValidationMessageParams } from './models'; export { I18NextValidationMessageComponent } from './components/validation-message.component'; export { I18NextValidationMessageDirective } from './directives/validation-message.directive'; ================================================ FILE: libs/angular-i18next/jest.config.ts ================================================ /* eslint-disable */ export default { displayName: 'angular-i18next', preset: '../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], globals: {}, coverageDirectory: '../../libs/angular-i18next/coverage', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', { tsconfig: '/tsconfig.spec.json', stringifyContentPathRegex: '\\.(html|svg)$', }, ], }, transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], snapshotSerializers: [ 'jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment', ], useESM: true, }; ================================================ FILE: libs/angular-i18next/ng-package.json ================================================ { "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "assets": [ "CHANGELOG.md", "postinstall.js" ], "lib": { "entryFile": "src/index.ts" } } ================================================ FILE: libs/angular-i18next/package.json ================================================ { "name": "angular-i18next", "version": "21.0.0-3", "publishConfig": { "access": "public" }, "np": { "contents": "./dist" }, "author": { "name": "Sergey Romanchuk" }, "homepage": "https://github.com/Romanchuk/angular-i18next#readme", "repository": { "type": "git", "url": "git+https://github.com/Romanchuk/angular-i18next.git" }, "engines": { "node": ">=22.0.0" }, "license": "MIT", "description": "i18next module for Angular", "keywords": [ "i18n", "i18next", "angular" ], "bugs": { "url": "https://github.com/Romanchuk/angular-i18next/issues" }, "maintainers": [ { "email": "rezety@gmail.com", "name": "Sergey Romanchuk" } ], "scripts": { "clean": "npm run clean:build && npm run clean:tests", "clean:build": "npx rimraf dist", "clean:tests": "npx rimraf coverage && npx rimraf tests/**/*.+{js,js.map,d.ts,metadata.json}", "copy:assets": "npx cpy CHANGELOG.md dist", "conventional-changelog": "npx conventional-changelog", "changelog": "npm run conventional-changelog -- -p angular -i CHANGELOG.md -s", "changelog:add": "git add --force CHANGELOG.md package.json", "changelog:commit": "git commit -m \"Updated CHANGELOG.md\"", "version": "npx conventional-changelog -p angular -i CHANGELOG.md -s -r && npx nx run angular-i18next:build && npm run copy:assets && npm run changelog:add && npm run changelog:commit", "release": "npx np", "test": "npx nx run angular-i18next:test" }, "dependencies": { "tslib": "^2.8.1" }, "peerDependencies": { "@angular/common": "^21.1.1", "@angular/core": "^21.1.1", "@angular/platform-browser": "^21.1.1", "i18next": "^25.4.0", "rxjs": "^7.8.2" }, "optionalDependencies": { "@angular/ssr": "^21.1.1", "@angular/platform-server": "^21.1.1" } } ================================================ FILE: libs/angular-i18next/postinstall.js ================================================ var BANNER = '\u001B[96mThank you for using angular-i18next (https://github.com/Romanchuk/angular-i18next). Please star the repo!\u001B[0m\n\n' + '\u001B[96mThe project needs your help! Please consider supporting of angular-i18next \u001B[0m\n' + '\u001B[96m>\u001B[94m Say thanks via donation: https://www.paypal.com/paypalme2/sergeyromanchuk/10USD \u001B[0m\n'; console.log(BANNER); ================================================ FILE: libs/angular-i18next/project.json ================================================ { "name": "angular-i18next", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "libs/angular-i18next/src", "targets": { "build": { "dependsOn": ["clean"], "executor": "@nx/angular:package", "options": { "project": "libs/angular-i18next/ng-package.json", "tsConfig": "libs/angular-i18next/tsconfig.lib.json" } }, "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/libs/angular-i18next"], "options": { "jestConfig": "libs/angular-i18next/jest.config.ts", "passWithNoTests": true } }, "lint": { "executor": "@nx/eslint:lint", "options": { "lintFilePatterns": [ "libs/angular-i18next/src/**/*.ts", "libs/angular-i18next/src/**/*.html" ] } } }, "tags": [] } ================================================ FILE: libs/angular-i18next/src/.eslintrc.json ================================================ { "extends": ["../../.eslintrc.json"], "ignorePatterns": ["!**/*"] } ================================================ FILE: libs/angular-i18next/src/index.ts ================================================ export * from './lib/index'; ================================================ FILE: libs/angular-i18next/src/lib/I18NextErrorHandlingStrategies.ts ================================================ import { I18NextLoadResult } from './I18NextLoadResult'; import * as i18n from 'i18next'; export interface I18NextErrorHandlingStrategy { handle( resolve: (thenableOrResult?: any) => void, reject: (error: any) => void ): i18n.Callback; } export class NativeErrorHandlingStrategy implements I18NextErrorHandlingStrategy { handle( resolve: (thenableOrResult?: I18NextLoadResult) => void, reject: (error: any) => void ) { return (err: any, t?: Function) => { let result: I18NextLoadResult = { err: err, t: t, }; resolve(result); }; } } export class StrictErrorHandlingStrategy implements I18NextErrorHandlingStrategy { handle( resolve: (thenableOrResult?: I18NextLoadResult) => void, reject: (error: any) => void ) { return (err: any, t?: any) => { let result: I18NextLoadResult = { err: err, t: t, }; if (!err) { resolve(result); return; } reject(err); }; } } ================================================ FILE: libs/angular-i18next/src/lib/I18NextEvents.ts ================================================ import { BehaviorSubject, Subject } from 'rxjs'; import { ITranslationEvents, MissingKeyEvent, ResourceEvent, } from './services/translation.events'; import * as i18n from 'i18next'; export class I18NextEvents implements ITranslationEvents { initialized = new BehaviorSubject(undefined); loaded = new BehaviorSubject(false); failedLoading = new Subject(); missingKey = new Subject(); added = new Subject(); removed = new Subject(); languageChanged = new BehaviorSubject(null); } ================================================ FILE: libs/angular-i18next/src/lib/I18NextLoadResult.ts ================================================ export interface I18NextLoadResult { err: any; t?: Function; } ================================================ FILE: libs/angular-i18next/src/lib/I18NextModuleParams.ts ================================================ import { Type } from '@angular/core'; import { I18NextErrorHandlingStrategy } from './I18NextErrorHandlingStrategies'; export interface I18NextModuleParams { errorHandlingStrategy?: Type; } ================================================ FILE: libs/angular-i18next/src/lib/index.ts ================================================ export * from './I18NextErrorHandlingStrategies'; export * from './I18NextEvents'; export * from './I18NextLoadResult'; export * from './I18NextModuleParams'; export * from './interpolation'; export * from './models'; export * from './module'; export * from './namespaces.guard'; export * from './pipes/i18next-cap.pipe'; export * from './pipes/i18next-eager.pipe'; export * from './pipes/i18next-format.pipe'; export * from './pipes/i18next.pipe'; export * from './provider'; export * from './provider.utils'; export * from './services/i18next-title'; export * from './services/i18next.service'; export * from './services/translation.events'; export * from './services/translation.service'; export * from './tokens'; ================================================ FILE: libs/angular-i18next/src/lib/interpolation.ts ================================================ import type { FormatFunction, InterpolationOptions } from "i18next"; export function defaultInterpolationFormat( value: any, format?: string, lng?: string ): string { if (!value) return value; switch (format) { case 'upper': case 'uppercase': return value.toUpperCase(); case 'lower': case 'lowercase': return value.toLowerCase(); case 'cap': case 'capitalize': return value.charAt(0).toUpperCase() + value.slice(1); case null: case undefined: case 'none': default: return value; } } export function interpolationFormat(customFormat: Function | null = null): FormatFunction { function formatDelegate(value: any, format?: string, lng?: string, options?: InterpolationOptions & { [key: string]: any } ): string { let formatedValue: string = defaultInterpolationFormat( value, format, lng ); if (customFormat === null) return formatedValue; return customFormat(formatedValue, format, lng); } return formatDelegate; } ================================================ FILE: libs/angular-i18next/src/lib/models.ts ================================================ import type * as i18n from 'i18next'; export type FormatPipeOptions = { format?: string; lng?: string; case?: string; [key: string]: any }; export type PrependPipeOptions = { prependScope?: boolean; prependNamespace?: boolean; }; export type PipeOptions = i18n.TOptions & FormatPipeOptions & PrependPipeOptions; export type NamespaceResolver = ( activatedRouteSnapshot: any, routerStateSnapshot?: any ) => Promise ================================================ FILE: libs/angular-i18next/src/lib/module.ts ================================================ import { NgModule, ModuleWithProviders } from "@angular/core"; import { FormatFunction, InterpolationOptions } from "i18next"; import { I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_INSTANCE, I18NEXT_SERVICE, I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_NAMESPACE_RESOLVER } from "./tokens"; import { I18NextCapPipe } from "./pipes/i18next-cap.pipe"; import { I18NextEagerPipe } from "./pipes/i18next-eager.pipe"; import { I18NextErrorHandlingStrategy, NativeErrorHandlingStrategy } from "./I18NextErrorHandlingStrategies"; import { I18NextFormatPipe } from "./pipes/i18next-format.pipe"; import { I18NextModuleParams } from "./I18NextModuleParams"; import { I18NextPipe } from "./pipes/i18next.pipe"; import { I18NextService } from "./services/i18next.service"; import { I18NextTitle } from "./services/i18next-title"; import { defaultInterpolationFormat } from "./interpolation"; import * as i18n from 'i18next'; import { i18nextNamespaceResolverFactory } from "./namespace.resolver"; const i18nextGlobal: i18n.i18n = i18n.default; /** * @deprecated Use provideI18Next() instead. This module-based approach will be removed in a future version. * Example: * ```typescript * // Instead of * imports: [I18NextModule.forRoot()] * * // Use * providers: [provideI18Next()] * ``` */ @NgModule({ imports: [I18NextPipe, I18NextEagerPipe, I18NextCapPipe, I18NextFormatPipe], exports: [I18NextPipe, I18NextEagerPipe, I18NextCapPipe, I18NextFormatPipe], providers: [ { provide: I18NEXT_NAMESPACE, useValue: '', }, { provide: I18NEXT_SCOPE, useValue: '', }, I18NextTitle, I18NextFormatPipe ], }) export class I18NextModule { /** * @deprecated Use provideI18Next() instead. This module-based approach will be removed in a future version. * Example: * ```typescript * // Instead of * imports: [I18NextModule.forRoot()] * * // Use * providers: [provideI18Next()] * ``` */ static forRoot( params: I18NextModuleParams = {} ): ModuleWithProviders { return { ngModule: I18NextModule, providers: [{ provide: I18NEXT_INSTANCE, useValue: i18nextGlobal, }, { provide: I18NEXT_SERVICE, useFactory: (errHandle: I18NextErrorHandlingStrategy, i18nextInstance: i18n.i18n) => new I18NextService(errHandle, i18nextInstance), deps: [ I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_INSTANCE ] }, { provide: I18NEXT_ERROR_HANDLING_STRATEGY, useClass: params.errorHandlingStrategy || NativeErrorHandlingStrategy, }, I18NextService, I18NextPipe, I18NextEagerPipe, I18NextCapPipe, I18NextFormatPipe, I18NextTitle, { provide: I18NEXT_NAMESPACE_RESOLVER, useFactory: i18nextNamespaceResolverFactory, deps: [I18NEXT_SERVICE], }, ], }; } static interpolationFormat(customFormat: Function | null = null): FormatFunction { function formatDelegate(value: any, format?: string, lng?: string, options?: InterpolationOptions & { [key: string]: any } ): string { let formatedValue: string = defaultInterpolationFormat( value, format, lng ); if (customFormat === null) return formatedValue; return customFormat(formatedValue, format, lng); } return formatDelegate; } } ================================================ FILE: libs/angular-i18next/src/lib/namespace.resolver.ts ================================================ import { inject } from "@angular/core"; import { I18NEXT_SERVICE } from "./tokens"; import { ITranslationService } from "./services/translation.service"; import { NamespaceResolver } from "./models"; export function resolver( activatedRouteSnapshot: any, routerStateSnapshot: any ): NamespaceResolver { const i18next: ITranslationService = inject(I18NEXT_SERVICE); let namespaces: string[] = activatedRouteSnapshot.data?.i18nextNamespaces ?? []; // @ts-ignore return i18next.loadNamespaces(namespaces.filter((n) => n)); } export function i18nextNamespaceResolverFactory() { return resolver; } ================================================ FILE: libs/angular-i18next/src/lib/namespaces.guard.ts ================================================ import { inject } from "@angular/core"; import { I18NEXT_SERVICE } from "./tokens"; /** * This function can trigger the loading of I18Next namespaces and block route activation to ensure namespaces are loaded before navigation continues. * * @param i18nextNamespaces I18Next namespaces to load * @returns A functional guard that will load the I18Next Namespaces, and continue navigation when loaded. */ export const i18NextNamespacesGuard = (...i18nextNamespaces: string[]) => () => inject(I18NEXT_SERVICE) .loadNamespaces(i18nextNamespaces.filter(Boolean)) .then(() => true) .catch(() => false); ================================================ FILE: libs/angular-i18next/src/lib/pipes/i18next-cap.pipe.ts ================================================ import { Inject, Injectable, Pipe, PipeTransform } from '@angular/core'; import { I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE, } from '../tokens'; import { I18NextPipe } from './i18next.pipe'; import { ITranslationService } from '../services/translation.service'; import { PipeOptions } from '../models'; @Injectable() @Pipe({ name: 'i18nextCap', standalone: true }) export class I18NextCapPipe extends I18NextPipe implements PipeTransform { constructor( @Inject(I18NEXT_SERVICE) translateI18Next: ITranslationService, @Inject(I18NEXT_NAMESPACE) ns: string | string[], @Inject(I18NEXT_SCOPE) scope: string | string[] ) { super(translateI18Next, ns, scope); } public override transform(key: string | string[], options?: PipeOptions): string { options = options || {}; options.format = 'cap'; return super.transform(key, options); } } ================================================ FILE: libs/angular-i18next/src/lib/pipes/i18next-eager.pipe.ts ================================================ import { ChangeDetectorRef, Inject, Pipe, PipeTransform } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { PipeOptions } from '../models'; import { ITranslationService } from '../services/translation.service'; import { I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE, } from '../tokens'; import { I18NextPipe } from './i18next.pipe'; @Pipe({ name: 'i18nextEager', pure: false, standalone: true, }) export class I18NextEagerPipe extends I18NextPipe implements PipeTransform { private lastKey: string | undefined; private lastOptions: PipeOptions | undefined; private lastValue: string = ''; constructor( @Inject(I18NEXT_SERVICE) protected override translateI18Next: ITranslationService, @Inject(I18NEXT_NAMESPACE) protected override ns: string | string[], @Inject(I18NEXT_SCOPE) protected override scope: string | string[], private cd: ChangeDetectorRef ) { super(translateI18Next, ns, scope); translateI18Next.events.languageChanged .pipe(takeUntilDestroyed()) .subscribe(() => { this.cd.markForCheck(); }); } private hasKeyChanged(key: string | string[]): boolean { return !this.lastKey || this.lastKey !== key; } private hasOptionsChanged(options?: PipeOptions): boolean { return this.lastOptions !== options; } public override transform(key: string | string[], options?: PipeOptions): string { const newKey = this.translateI18Next.language + '|' + JSON.stringify(key); if (this.hasKeyChanged(newKey) || this.hasOptionsChanged(options)) { this.lastKey = newKey; this.lastOptions = options; this.lastValue = super.transform(key, options); } return this.lastValue; } } ================================================ FILE: libs/angular-i18next/src/lib/pipes/i18next-format.pipe.ts ================================================ import { Inject, Injectable, Pipe, PipeTransform } from '@angular/core'; import { FormatPipeOptions } from '../models'; import { ITranslationService } from '../services/translation.service'; import { I18NEXT_SERVICE } from '../tokens'; @Injectable() @Pipe({ name: 'i18nextFormat', standalone: true }) export class I18NextFormatPipe implements PipeTransform { constructor( @Inject(I18NEXT_SERVICE) private translateI18Next: ITranslationService ) {} public transform(value: any, options: FormatPipeOptions | string): string { let opts: FormatPipeOptions = typeof options === 'string' ? { format: options } : options; return this.translateI18Next.format(value, opts.format, opts.lng); } } ================================================ FILE: libs/angular-i18next/src/lib/pipes/i18next.pipe.ts ================================================ import { Inject, Injectable, Pipe, PipeTransform } from '@angular/core'; import { PipeOptions } from '../models'; import { ITranslationService } from '../services/translation.service'; import { I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE } from '../tokens'; @Injectable() @Pipe({ name: 'i18next', standalone: true }) export class I18NextPipe implements PipeTransform { constructor( @Inject(I18NEXT_SERVICE) protected translateI18Next: ITranslationService, @Inject(I18NEXT_NAMESPACE) protected ns: string | string[], @Inject(I18NEXT_SCOPE) protected scope: string | string[] ) {} public transform(key: string | string[], options?: PipeOptions): string { options = this.prepareOptions(options); let i18nOpts = this.translateI18Next.options; if (options.prependScope === undefined || options.prependScope === true) { if (this.scope) { key = this.prependScope( key, this.scope, i18nOpts.keySeparator, i18nOpts.nsSeparator ); } } if ( options.prependNamespace === undefined || options.prependNamespace === true ) { if (this.ns) { key = this.prependNamespace(key, this.ns, i18nOpts.nsSeparator); } } let result = this.translateI18Next.t(key, options); if (options.format) { if (result) { result = this.translateI18Next.format( result, options.format, this.translateI18Next.language ); } } return result ?? ''; } private prependScope( key: string | string[], scope: string | string[], keySeparator: string | false | undefined, nsSeparator: string | false | undefined ): string[] { const nsSep = nsSeparator || ''; const keySep = keySeparator || ''; if (typeof key === 'string') { key = [key]; } if (typeof scope === 'string') { scope = [scope]; } let keysWithScope = []; for (let i = 0; i < key.length; i++) { const k = key[i]; if (!this.keyContainsNsSeparator(k, nsSep)) { // Do not set scope, if key contains a namespace keysWithScope.push( ...scope.map((sc) => this.joinStrings(keySep, sc, k)) ); } keysWithScope.push(k); } return keysWithScope; } private prependNamespace( key: string | string[], ns: string | string[], nsSeparator: string | false | undefined ): string[] { const nsSep = nsSeparator || ''; if (typeof key === 'string') { key = [key]; } if (typeof ns === 'string') { ns = [ns]; } let keysWithNamespace = []; for (let i = 0; i < key.length; i++) { const k = key[i]; if (!this.keyContainsNsSeparator(k, nsSep)) { // Do not set namespace, if key contains a namespace keysWithNamespace.push(...ns.map((n) => this.joinStrings(nsSep, n, k))); } keysWithNamespace.push(k); } return keysWithNamespace; } private joinStrings(separator: string, ...str: string[]) { return [...str].join(separator); } private keyContainsNsSeparator(key: string, nsSeparator: string) { return key.indexOf(nsSeparator) !== -1; } private prepareOptions(options?: PipeOptions): PipeOptions { options = options || {}; if (options.context != null) options.context = options.context.toString(); return options; } } ================================================ FILE: libs/angular-i18next/src/lib/provider.ts ================================================ import { ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, EnvironmentProviders, inject, LOCALE_ID, makeEnvironmentProviders, Provider, Type } from '@angular/core'; import { Title } from '@angular/platform-browser'; import * as i18n from 'i18next'; import { I18NextErrorHandlingStrategy, NativeErrorHandlingStrategy, } from './I18NextErrorHandlingStrategies'; import { I18NextCapPipe } from './pipes/i18next-cap.pipe'; import { I18NextEagerPipe } from './pipes/i18next-eager.pipe'; import { I18NextFormatPipe } from './pipes/i18next-format.pipe'; import { I18NextPipe } from './pipes/i18next.pipe'; import { I18NextFeature, I18NextFeatureKind, makeI18NextFeature } from './provider.utils'; import { I18NextTitle } from './services/i18next-title'; import { I18NextService } from './services/i18next.service'; import { I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_INSTANCE, I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE } from './tokens'; const i18nextGlobal: i18n.i18n = i18n.default; export function localeIdFactory() { const i18next = inject(I18NEXT_SERVICE); return i18next.language ?? DEFAULT_LOCALE_ID; } /** * Provides the necessary dependencies for using i18next with Angular. * * @param features An array of features to enable. See {@link I18NextFeature} for available features. * @returns An array of providers that can be added to the root providers. * * @example * import { provideI18Next } from '@angular-i18next/core'; * * providers: [ * provideI18Next(), * ], * */ export function provideI18Next( ...features: I18NextFeature[] ): EnvironmentProviders { const providers: Provider[] = [ { provide: I18NEXT_INSTANCE, useValue: i18nextGlobal, }, { provide: I18NEXT_SERVICE, useFactory: ( errHandle: I18NextErrorHandlingStrategy, i18nextInstance: i18n.i18n, ) => new I18NextService(errHandle, i18nextInstance), deps: [I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_INSTANCE], }, { provide: I18NEXT_NAMESPACE, useValue: '', }, { provide: I18NEXT_SCOPE, useValue: '', }, { provide: I18NEXT_ERROR_HANDLING_STRATEGY, useClass: NativeErrorHandlingStrategy, }, { provide: LOCALE_ID, useFactory: localeIdFactory, }, I18NextService, I18NextPipe, I18NextEagerPipe, I18NextCapPipe, I18NextFormatPipe, ]; for (const feature of features) { providers.push(...feature.ɵproviders); } return makeEnvironmentProviders(providers); } /** * Configures a custom error handling strategy for i18next. * * @param errorHandlingStrategy - A class implementing the I18NextErrorHandlingStrategy interface. * @returns An I18NextFeature for the specified custom error handling strategy. * * This feature allows the integration of a custom error handling mechanism * into the i18next setup, replacing the default error handling strategy. * * * Example: * ```typescript * providers: [ * provideI18Next(withCustomErrorHandlingStrategy(StrictErrorHandlingStrategy)()) * ] * ``` */ export function withCustomErrorHandlingStrategy( errorHandlingStrategy: Type, ): I18NextFeature { return makeI18NextFeature(I18NextFeatureKind.CustomErrorHandlingStrategy, [ { provide: I18NEXT_ERROR_HANDLING_STRATEGY, useClass: errorHandlingStrategy, }, ]); } /** * Provides I18NextTitle service for document title translation support. * * @returns An I18NextFeature that configures the I18NextTitle service * * Example: * ```typescript * providers: [ * provideI18Next(withTitle()) * ] * ``` */ export function withTitle(): I18NextFeature { return makeI18NextFeature(I18NextFeatureKind.Title, [ { provide: Title, useClass: I18NextTitle } ]); } ================================================ FILE: libs/angular-i18next/src/lib/provider.utils.ts ================================================ import { Provider } from "@angular/core"; /** * A feature for use when configuring `provideI18Next`. * * @publicApi */ export interface I18NextFeature { ɵkind: KindT; ɵproviders: Provider[]; } export function makeI18NextFeature( kind: KindT, providers: Provider[], ): I18NextFeature { return { ɵkind: kind, ɵproviders: providers, }; } /** * Identifies a particular kind of `HttpFeature`. * * @publicApi */ export enum I18NextFeatureKind { CustomErrorHandlingStrategy, Mock, Title, AppInitialize, SSR, Forms } ================================================ FILE: libs/angular-i18next/src/lib/services/i18next-title.ts ================================================ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { I18NextPipe } from '../pipes/i18next.pipe'; @Injectable() export class I18NextTitle extends Title { constructor(private i18nextPipe: I18NextPipe, @Inject(DOCUMENT) doc: any) { super(doc); } override setTitle(value: string) { return super.setTitle(this.translate(value)); } private translate(text: string) { return this.i18nextPipe.transform(text, { format: 'cap' }); } } ================================================ FILE: libs/angular-i18next/src/lib/services/i18next.service.ts ================================================ import { Inject, Injectable, Optional } from '@angular/core'; import * as i18n from 'i18next'; import { I18NextErrorHandlingStrategy } from '../I18NextErrorHandlingStrategies'; import { I18NextEvents } from '../I18NextEvents'; import { I18NextLoadResult } from '../I18NextLoadResult'; import { I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_INSTANCE } from '../tokens'; import { ITranslationEvents } from './translation.events'; import { ITranslationOptions, ITranslationService } from './translation.service'; import type { InitOptions, Module, Modules, Namespace, Newable, NewableModule, ResourceStore, Services, TFunction, TFunctionReturn } from 'i18next'; const i18nextGlobal: i18n.i18n = i18n.default; @Injectable() export class I18NextService implements ITranslationService { private readonly i18next: i18n.i18n; events: ITranslationEvents = new I18NextEvents(); get language() { return this.i18next.language; } get languages() { return this.i18next.languages; } get options() { return this.i18next.options; } get modules(): Modules { return this.i18next.modules; } get services(): Services { return this.i18next.services; } get store(): ResourceStore { return this.i18next.store; } get resolvedLanguage() { return this.i18next.resolvedLanguage; } get isInitialized() { return this.i18next.isInitialized; } constructor( @Inject(I18NEXT_ERROR_HANDLING_STRATEGY) private errorHandlingStrategy: I18NextErrorHandlingStrategy, @Optional() @Inject(I18NEXT_INSTANCE) i18nextInstance?: i18n.i18n ) { this.i18next = i18nextInstance ?? i18nextGlobal; } t(key: string | string[], options?: ITranslationOptions | undefined): TFunctionReturn; t(key: string | string[] | (string | TemplateStringsArray)[], defaultValue: string, options?: ITranslationOptions | undefined): TFunctionReturn; t(key: unknown, defaultValueOrOptions?: unknown, options?: unknown): TFunctionReturn { const hasDefault = !!defaultValueOrOptions && typeof defaultValueOrOptions === 'string'; this.i18next.t.bind(this.i18next); if (hasDefault) { return this.i18next.t(key as (string | string[]), defaultValueOrOptions as string, options as ITranslationOptions); } else { return this.i18next.t(key as (string | string[]), defaultValueOrOptions as ITranslationOptions); } } public use( module: T | NewableModule | Newable ): ITranslationService { this.i18next.use.call(this.i18next, module); return this; } init(options: InitOptions): Promise { this.subscribeEvents(); return new Promise((resolve, reject) => { this.i18next.init.call( this.i18next, Object.assign({}, options ?? {}), this.errorHandlingStrategy.handle(resolve, reject) ); }); } public format(value: any, format?: string, lng?: string): string { return this.i18next.format.call(this.i18next, value, format, lng, {}); } public exists(key: string | string[], options: any) { return this.i18next.exists.call(this.i18next, key, options); } getFixedT(lng: string | readonly string[], ns?: string | readonly string[], keyPrefix?: string): TFunction; getFixedT(lng: null, ns: string | readonly string[] | null, keyPrefix?: string): TFunction; getFixedT(lng: any, ns?: any, keyPrefix?: any): TFunction { return this.i18next.getFixedT.call(this.i18next, lng, ns, keyPrefix); } public setDefaultNamespace(ns: string) { this.i18next.setDefaultNamespace.call(this.i18next, ns); } public dir(lng?: string) { return this.i18next.dir.call(this.i18next, lng); } public changeLanguage(lng: string): Promise { return new Promise( ( resolve: (thenableOrResult: TFunction | PromiseLike) => void, reject: (error: any) => void ) => { return this.i18next.changeLanguage.call( this.i18next, lng, this.errorHandlingStrategy.handle(resolve, reject) ); } ); } public loadNamespaces(namespaces: string | string[]): Promise { return new Promise( ( resolve: (thenableOrResult: I18NextLoadResult | PromiseLike) => void, reject: (error: any) => void ) => { this.i18next.loadNamespaces.call( this.i18next, namespaces, this.errorHandlingStrategy.handle(resolve, reject) ); } ); } public loadLanguages(lngs: string | string[]) { return new Promise( ( resolve: (thenableOrResult: void | PromiseLike) => void, reject: (error: any) => void ) => { this.i18next.loadLanguages.call( this.i18next, lngs, this.errorHandlingStrategy.handle(resolve, reject) ); } ); } //#region resource handling public loadResources(callback?: (err: any) => void): void { this.i18next.loadResources.call(this.i18next, callback); } public getDataByLanguage(lng: string) { return this.i18next.getDataByLanguage.call(this.i18next, lng); } public async reloadResources(...params: any) { await this.i18next.reloadResources.apply(this.i18next, params); } public getResource(lng: string, ns: string, key: string, options: any) { return this.i18next.getResource.call(this.i18next, lng, ns, key, options); } public addResource(lng: string, ns: string, key: string, value: any, options: any): i18n.i18n { return this.i18next.addResource.call(this.i18next, lng, ns, key, value, options); } public addResources(lng: string, ns: string, resources: any): i18n.i18n { return this.i18next.addResources.call(this.i18next, lng, ns, resources); } public addResourceBundle(lng: string, ns: string, resources: any, deep: any, overwrite: any): i18n.i18n { return this.i18next.addResourceBundle.call( this.i18next, lng, ns, resources, deep, overwrite ); } public hasResourceBundle(lng: string, ns: string) { return this.i18next.hasResourceBundle.call(this.i18next, lng, ns); } public getResourceBundle(lng: string, ns: string) { return this.i18next.getResourceBundle.call(this.i18next, lng, ns); } public removeResourceBundle(lng: string, ns: string): i18n.i18n { return this.i18next.removeResourceBundle.call(this.i18next, lng, ns); } //#endregion private subscribeEvents() { this.i18next.on.call(this.i18next, 'initialized', (options: InitOptions) => { this.events.initialized.next(options); }); this.i18next.on.call(this.i18next, 'loaded', (loaded: boolean) => this.events.loaded.next(loaded) ); this.i18next.on.call(this.i18next, 'failedLoading', (lng: string, ns: string, msg: string) => this.events.failedLoading.next({ lng, ns, msg }) ); this.i18next.on.call(this.i18next, 'languageChanged', (lng: string) => { this.events.languageChanged.next(lng); }); this.i18next.on.call(this.i18next, 'missingKey', (lngs: string, namespace: string, key: string, res: any) => this.events.missingKey.next({ lngs, namespace, key, res }) ); this.i18next.on.call(this.i18next, 'added', (lng: string, ns: string) => this.events.added.next({ lng, ns }) ); this.i18next.on.call(this.i18next, 'removed', (lng: string, ns: string) => this.events.removed.next({ lng, ns }) ); } } ================================================ FILE: libs/angular-i18next/src/lib/services/translation.events.ts ================================================ import { BehaviorSubject, Subject } from 'rxjs'; import * as i18n from 'i18next'; export type ResourceEvent = { lng: any; ns: any }; export type MissingKeyEvent = { lngs: any; namespace: any; key: any; res: any }; export interface ITranslationEvents { initialized: BehaviorSubject; loaded: BehaviorSubject; failedLoading: Subject; missingKey: Subject; added: Subject; removed: Subject; languageChanged: BehaviorSubject; } ================================================ FILE: libs/angular-i18next/src/lib/services/translation.service.ts ================================================ import * as i18n from 'i18next'; import { I18NextLoadResult } from '../I18NextLoadResult'; import { ITranslationEvents } from './translation.events'; import type { Callback, ExistsFunction, FormatFunction, InitOptions, Module, Modules, Namespace, Newable, NewableModule, ResourceStore, Services, TFunction, TOptions } from 'i18next'; type Modify = Omit & R; export type ITranslationOptions = TOptions; export type ITranslationService = Modify, { events: ITranslationEvents; language: string; languages: readonly string[]; options: InitOptions; modules: Modules; services: Services; store: ResourceStore; resolvedLanguage: string | undefined; use( module: T | NewableModule | Newable ): ITranslationService; init(options: InitOptions): Promise; t( key: string | string[], options?: Options, ): i18n.TFunctionReturn; t( key: string | string[], defaultValue: string, options?: Options ): i18n.TFunctionReturn; format: FormatFunction; exists: ExistsFunction; getFixedT( lng: string | readonly string[], ns?: string | readonly string[], keyPrefix?: string, ): TFunction; getFixedT(lng: null, ns: string | readonly string[] | null, keyPrefix?: string): TFunction; setDefaultNamespace(ns: string): void; dir(lng: string): string; changeLanguage(lng: string): Promise; loadNamespaces(namespaces: string[]): Promise; loadLanguages(lngs: string | readonly string[], callback?: Callback): Promise; loadResources(callback?: (err: any) => void): void; getDataByLanguage(lng: string): { [key: string]: { [key: string]: string; }; } | undefined; reloadResources( lngs?: string | readonly string[], ns?: string | readonly string[], callback?: () => void, ): Promise; reloadResources(lngs: null, ns: string | readonly string[], callback?: () => void): Promise; getResource( lng: string, ns: string, key: string, options?: Pick, ): any; addResource( lng: string, ns: string, key: string, value: string, options?: { keySeparator?: string; silent?: boolean }, ): i18n.i18n; addResources(lng: string, ns: string, resources: any): i18n.i18n; addResourceBundle( lng: string, ns: string, resources: any, deep?: boolean, overwrite?: boolean, ): i18n.i18n; hasResourceBundle(lng: string, ns: string): boolean; getResourceBundle(lng: string, ns: string): any; removeResourceBundle(lng: string, ns: string): i18n.i18n; }>; ================================================ FILE: libs/angular-i18next/src/lib/tokens.ts ================================================ import * as i18n from 'i18next' import { InjectionToken } from '@angular/core'; import { I18NextErrorHandlingStrategy } from './I18NextErrorHandlingStrategies'; import { ITranslationService } from './services/translation.service'; import { NamespaceResolver } from './models'; export const I18NEXT_SCOPE = new InjectionToken( 'I18NEXT_SCOPE' ); export const I18NEXT_NAMESPACE = new InjectionToken( 'I18NEXT_NAMESPACE' ); export const I18NEXT_SERVICE = new InjectionToken( 'I18NEXT_SERVICE' ); export const I18NEXT_NAMESPACE_RESOLVER = new InjectionToken( 'I18NEXT_NAMESPACE_RESOLVER' ); export const I18NEXT_ERROR_HANDLING_STRATEGY = new InjectionToken( 'I18NEXT_ERROR_HANDLING_STRATEGY' ); export const I18NEXT_INSTANCE = new InjectionToken('I18NEXT_INSTANCE'); ================================================ FILE: libs/angular-i18next/src/test-setup.ts ================================================ import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; import { TestBed } from "@angular/core/testing"; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; setupZonelessTestEnv(); TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); ================================================ FILE: libs/angular-i18next/src/tests/module/module.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { defaultInterpolationFormat, I18NEXT_NAMESPACE, I18NEXT_NAMESPACE_RESOLVER, I18NEXT_SCOPE, I18NEXT_SERVICE, I18NextCapPipe, I18NextFormatPipe, I18NextModule, I18NextPipe, I18NextService, I18NextTitle, } from '../../lib/index'; describe('I18NextModule', () => { const DEFAULT_NAMESPACE = ''; const DEFAULT_SCOPE = ''; beforeEach(() => { TestBed.configureTestingModule({ imports: [I18NextModule.forRoot()], }); }); it('should provide tokens with default values', () => { let tokenNs = TestBed.inject(I18NEXT_NAMESPACE); expect(tokenNs).toEqual(DEFAULT_NAMESPACE); let tokenScope = TestBed.inject(I18NEXT_SCOPE); expect(tokenScope).toEqual(DEFAULT_SCOPE); }); it('should provide I18NextService', () => { let i18nextService = TestBed.inject(I18NEXT_SERVICE); expect(i18nextService).not.toBeNull(); let i18nextService2 = TestBed.inject(I18NextService); expect(i18nextService2).not.toBeNull(); }); it('should provide pipes', () => { let i18nextPipe = TestBed.inject(I18NextPipe); expect(i18nextPipe).not.toBeNull(); let i18nextCapPipe = TestBed.inject(I18NextCapPipe); expect(i18nextCapPipe).not.toBeNull(); let i18nextFormatPipe = TestBed.inject(I18NextFormatPipe); expect(i18nextFormatPipe).not.toBeNull(); }); it('should provide title', () => { let title: I18NextTitle = TestBed.inject(I18NextTitle); expect(title).toBeTruthy(); expect(title instanceof I18NextTitle).toBeTruthy(); }); it('should have default formatters', () => { const capitalizedTest = defaultInterpolationFormat('test', 'cap'); const capitalized2Test = defaultInterpolationFormat('test', 'capitalize'); expect(capitalizedTest).toEqual('Test'); expect(capitalizedTest).toEqual(capitalized2Test); const uppercaseTest = defaultInterpolationFormat('test', 'upper'); const uppercase2Test = defaultInterpolationFormat('test', 'uppercase'); expect(uppercaseTest).toEqual('TEST'); expect(uppercaseTest).toEqual(uppercase2Test); let lowercaseTest = defaultInterpolationFormat('TEST', 'lower'); let lowercase2Test = defaultInterpolationFormat('TEST', 'lowercase'); expect(lowercaseTest).toEqual('test'); expect(lowercaseTest).toEqual(lowercase2Test); let noFormat = defaultInterpolationFormat('test', undefined); let noFormat2 = defaultInterpolationFormat('test', 'none'); expect(noFormat).toEqual('test'); expect(noFormat).toEqual(noFormat2); }); it('should support interpolation custom formatters', () => { const valueParam = 'test'; const formatParam = 'cap'; const lngParam = 'en'; let customFormat = function (value: any, format: any, lng: any) { expect(value).toEqual('Test'); expect(format).toEqual(formatParam); expect(lng).toEqual(lngParam); return `$${value}$`; }; let formatFunc = I18NextModule.interpolationFormat(customFormat); let result = formatFunc(valueParam, formatParam, lngParam); expect(result).toBe('$Test$'); }); it('should provide resolver', (done) => { let resolver = TestBed.inject(I18NEXT_NAMESPACE_RESOLVER); expect(resolver).toBeTruthy(); TestBed.runInInjectionContext(() => { resolver({ data: { i18nextNamespaces: [], }, }).then(() => { done(); }); }); }); }); ================================================ FILE: libs/angular-i18next/src/tests/pipes/I18NextEagerPipe.spec.ts ================================================ import { ApplicationInitStatus, ChangeDetectorRef } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { I18NextEagerPipe, I18NextModule, I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE, ITranslationService, PipeOptions } from '../../lib'; import { MOCK_I18N_PROVIDERS } from '../setup'; describe('I18NextEagerPipe', () => { let pipe: I18NextEagerPipe, service: ITranslationService, markForCheckSpy: jest.SpyInstance; beforeEach(async () => { TestBed.configureTestingModule({ providers: [...MOCK_I18N_PROVIDERS, { provide: ChangeDetectorRef, useValue: { detectChanges: () => { }, markForCheck: () => { } } }, { provide: I18NEXT_SCOPE, useValue: 'scope' }, { provide: I18NEXT_NAMESPACE, useValue: 'ns' }], }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; service = TestBed.inject(I18NEXT_SERVICE); pipe = TestBed.inject(I18NextEagerPipe); const changeDetector = TestBed.inject(ChangeDetectorRef); // So, I am spying directly on the prototype. markForCheckSpy = jest.spyOn(changeDetector, 'markForCheck'); }); it('should create the pipe', () => { expect(pipe).toBeTruthy(); }); describe('when called with key and options', () => { let result: string; let myOptions: PipeOptions; beforeEach(() => { myOptions = { defaultValue: 'value1' }; result = pipe.transform('myKey', myOptions); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey', 'scope.myKey', 'ns:myKey', 'myKey'], myOptions ); }); it('should return the correct result', () => { expect(result).toBe('ns:scope.myKey'); }); describe('when the language changes', () => { beforeEach(() => { service.events.languageChanged.next('es'); service.language = 'es'; }); it('should mark for check so it triggers the pipe transform', () => { expect(markForCheckSpy).toHaveBeenCalled(); }); describe('when the pipe gets triggered by change detection', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey', myOptions); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey', 'scope.myKey', 'ns:myKey', 'myKey'], { defaultValue: 'value1' } ); }); it('should return the new result', () => { expect(result).toBe('ns:scope.myKey'); }); }); }); describe('when called with same key and options', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey', myOptions); }); it('should not call the translate service', () => { expect(service.t).not.toHaveBeenCalled(); }); it('should return the previously cached result', () => { expect(result).toBe('ns:scope.myKey'); }); }); describe('when called with same key but different options', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey', { defaultValue: 'value2' }); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey', 'scope.myKey', 'ns:myKey', 'myKey'], { defaultValue: 'value2' } ); }); it('should return the new result', () => { expect(result).toBe('ns:scope.myKey'); }); }); describe('when called with different key but same options', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey2', myOptions); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey2', 'scope.myKey2', 'ns:myKey2', 'myKey2'], { defaultValue: 'value1' } ); }); it('should return the new result', () => { expect(result).toBe('ns:scope.myKey2'); }); }); }); describe('when called with only with key', () => { let result: string; beforeEach(() => { result = pipe.transform('myKey'); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey', 'scope.myKey', 'ns:myKey', 'myKey'], {} ); }); it('should return the correct result', () => { expect(result).toBe('ns:scope.myKey'); }); describe('when called with same key', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey'); }); it('should not call the translate service', () => { expect(service.t).not.toHaveBeenCalled(); }); it('should return the previously cached result', () => { expect(result).toBe('ns:scope.myKey'); }); }); describe('when called with different key', () => { beforeEach(() => { jest.clearAllMocks(); result = pipe.transform('myKey2'); }); it('should call the translate service', () => { expect(service.t).toHaveBeenCalledWith( ['ns:scope.myKey2', 'scope.myKey2', 'ns:myKey2', 'myKey2'], {} ); }); it('should return the new result', () => { expect(result).toBe('ns:scope.myKey2'); }); }); }); }); ================================================ FILE: libs/angular-i18next/src/tests/pipes/I18NextPipe.spec.ts ================================================ import { ApplicationInitStatus } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { I18NextCapPipe, I18NextModule, I18NextPipe, I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE, ITranslationService } from '../../lib'; import { MOCK_I18N_PROVIDERS } from '../setup'; describe('I18NextPipe tests', function () { beforeEach(() => { TestBed.configureTestingModule({ imports: [I18NextPipe], providers: [...MOCK_I18N_PROVIDERS], }); }); afterEach(() => { TestBed.resetTestingModule(); }); it('transform', async () => { // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const key = 'test'; const transResult = pipe.transform(key); expect(transResult).toEqual(key); }); it('format options', async () => { // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const capPipe = TestBed.inject(I18NextCapPipe); const key = 'test'; const pipeResult = pipe.transform(key, { format: 'cap' }); const capPipeResult = capPipe.transform(key); expect(pipeResult).toEqual(capPipeResult); const keyCap = key.charAt(0).toUpperCase() + key.slice(1); expect(pipeResult).toEqual(keyCap); }); it('namespace prefix', async () => { const namespace = 'error'; TestBed.overrideProvider(I18NEXT_NAMESPACE, { useValue: namespace }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; await TestBed.compileComponents(); const pipe = TestBed.inject(I18NextPipe); const service = TestBed.inject(I18NEXT_SERVICE) const key = 'test'; const transResult = pipe.transform(key); expect(transResult).toEqual(buildKeyWithNs(service, namespace, key)); // for array key const arrayKey = ['test_1', 'test_2']; const arrResult = pipe.transform(arrayKey); expect(arrResult).toEqual(buildKeyWithNs(service, namespace, arrayKey[0])); }); it('ignore namespace param if key already contains it', async () => { const namespace = 'error'; TestBed.overrideProvider(I18NEXT_NAMESPACE, { useValue: namespace }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const realns = 'realns'; const service = TestBed.inject(I18NEXT_SERVICE); const key = [realns, 'test'].join(service.options.keySeparator || '.'); const transResult = pipe.transform(key); expect(transResult).toEqual(buildKeyWithNs(service, namespace, key)); }); it('scope prefix', async () => { const scope = 'scope'; TestBed.overrideProvider(I18NEXT_SCOPE, { useValue: scope }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const service = TestBed.inject(I18NEXT_SERVICE); const key = 'test'; // for primitive key const transResult = pipe.transform(key); expect(transResult).toEqual([scope, key].join(service.options.keySeparator || '.')); // for array key const arrayKey = ['test_1', 'test_2']; const arrResult = pipe.transform(arrayKey); expect(arrResult).toEqual([scope, arrayKey[0]].join(service.options.keySeparator || '.')); }); it('ns and scope prefix', async () => { const scope = 'scope'; const ns = 'ns'; TestBed.overrideProvider(I18NEXT_SCOPE, { useValue: scope }); TestBed.overrideProvider(I18NEXT_NAMESPACE, { useValue: ns }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const service = TestBed.inject(I18NEXT_SERVICE); const key = 'test'; const transResult = pipe.transform(key); expect(transResult).toEqual(buildKeyWithNs(service, ns, [scope, key].join(service.options.keySeparator || '.'))); }); it('ns and scope prefix (arrays)', async () => { const scope = ['scope1', 'scope2']; const ns = ['ns1', 'ns2']; TestBed.overrideProvider(I18NEXT_SCOPE, { useValue: scope }); TestBed.overrideProvider(I18NEXT_NAMESPACE, { useValue: ns }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const service = TestBed.inject(I18NEXT_SERVICE); const key = 'test'; const transResult = pipe.transform(key); expect(transResult).toEqual(buildKeyWithNs(service, ns[0], [scope[0], key].join(service.options.keySeparator || '.'))); }); it('ns and scope no prefix (prependScope = false and prependNamespace = false)', async () => { const scope = 'scope'; const ns = 'ns'; TestBed.overrideProvider(I18NEXT_SCOPE, { useValue: scope }); TestBed.overrideProvider(I18NEXT_NAMESPACE, { useValue: ns }); // until https://github.com/angular/angular/issues/24218 is fixed await TestBed.inject(ApplicationInitStatus).donePromise; const pipe = TestBed.inject(I18NextPipe); const service = TestBed.inject(I18NEXT_SERVICE); const key = 'test'; const transResult = pipe.transform(key, { prependScope: false, prependNamespace: false, }); expect(transResult).toEqual(key); }); }); function buildKeyWithNs(service: ITranslationService, ns: string, key: string): string { return `${ns}${service.options.nsSeparator}${key}`; } ================================================ FILE: libs/angular-i18next/src/tests/projectTests/project.component.ts ================================================ import { Component, ViewEncapsulation } from '@angular/core'; import { I18NextPipe } from '../../lib'; @Component({ selector: 'project-component', encapsulation: ViewEncapsulation.None, template: '
{{ "privet" | i18next }}
', standalone: true, imports: [I18NextPipe] }) export class ProjectComponent {} ================================================ FILE: libs/angular-i18next/src/tests/projectTests/projectTests.spec.ts ================================================ import { TestBed } from "@angular/core/testing"; import { MOCK_I18N_PROVIDERS } from "../setup"; import { ProjectComponent } from './project.component'; describe('Project component tests', function () { beforeEach(() => { TestBed.configureTestingModule({ imports: [ProjectComponent], providers: [MOCK_I18N_PROVIDERS], }); }); it('should test project component', function () { let pc = TestBed.createComponent(ProjectComponent); expect(pc).toBeDefined(); pc.detectChanges(); }); }); ================================================ FILE: libs/angular-i18next/src/tests/provider/provider.spec.ts ================================================ import { ChangeDetectorRef, inject, provideAppInitializer } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Title } from '@angular/platform-browser'; import { MockI18NextService, withMock } from '../../../testing/src/public_api'; import { I18NEXT_ERROR_HANDLING_STRATEGY, I18NEXT_INSTANCE, I18NEXT_NAMESPACE, I18NEXT_SCOPE, I18NEXT_SERVICE, I18NextCapPipe, I18NextEagerPipe, I18NextFormatPipe, I18NextLoadResult, I18NextPipe, I18NextService, I18NextTitle, interpolationFormat, NativeErrorHandlingStrategy, provideI18Next, withCustomErrorHandlingStrategy, withTitle } from '../../lib/index'; describe('I18Next Provider', () => { describe('provideI18Next', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ ChangeDetectorRef, provideAppInitializer(() => { const i18next = inject(I18NEXT_SERVICE); let promise: Promise = i18next.init({ lng: 'en', interpolation: { format: interpolationFormat(), }, }); return promise; }), provideI18Next(), ], }); }); it('should provide default tokens', () => { const namespace = TestBed.inject(I18NEXT_NAMESPACE); const scope = TestBed.inject(I18NEXT_SCOPE); expect(namespace).toBe(''); expect(scope).toBe(''); }); it('should provide i18next instance', () => { const instance = TestBed.inject(I18NEXT_INSTANCE); expect(instance).toBeTruthy(); }); it('should provide I18NextService with NativeErrorHandlingStrategy', () => { const service = TestBed.inject(I18NEXT_SERVICE); const strategy = TestBed.inject(I18NEXT_ERROR_HANDLING_STRATEGY); expect(service).toBeInstanceOf(I18NextService); expect(strategy).toBeInstanceOf(NativeErrorHandlingStrategy); }); it('should provide all required pipes', () => { const i18nextPipe = TestBed.inject(I18NextPipe); const eagerPipe = TestBed.inject(I18NextEagerPipe); const capPipe = TestBed.inject(I18NextCapPipe); const formatPipe = TestBed.inject(I18NextFormatPipe); expect(i18nextPipe).toBeInstanceOf(I18NextPipe); expect(eagerPipe).toBeInstanceOf(I18NextEagerPipe); expect(capPipe).toBeInstanceOf(I18NextCapPipe); expect(formatPipe).toBeInstanceOf(I18NextFormatPipe); }); }); describe('withMock feature', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideI18Next(withMock())], }); }); it('should provide MockI18NextService', () => { const service = TestBed.inject(I18NEXT_SERVICE); expect(service).toBeInstanceOf(MockI18NextService); }); }); describe('withTitle feature', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideI18Next(withTitle())], }); }); it('should provide I18NextTitle service', () => { const title = TestBed.inject(Title); expect(title).toBeInstanceOf(I18NextTitle); }); }); describe('withCustomErrorHandlingStrategy feature', () => { class CustomErrorStrategy extends NativeErrorHandlingStrategy {} beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideI18Next(withCustomErrorHandlingStrategy(CustomErrorStrategy)), ], }); }); it('should use the custom error handling strategy', () => { const strategy = TestBed.inject(I18NEXT_ERROR_HANDLING_STRATEGY); expect(strategy).toBeInstanceOf(CustomErrorStrategy); }); }); }); ================================================ FILE: libs/angular-i18next/src/tests/service/I18NextService.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { I18NextModule, I18NEXT_SERVICE, ITranslationService } from '../../lib'; const i18nextOptions: i18n.InitOptions = { lng: 'cimode', supportedLngs: ['cimode', 'en', 'ru'], appendNamespaceToCIMode: true }; import * as i18n from 'i18next'; // Be descriptive with titles here. The describe and it titles combined read like a sentence. describe('I18nService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [I18NextModule.forRoot()] }); }); it('should trigger initialize event', (done) => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); service.events.initialized.subscribe((isInited) => { if (isInited) done(); }); service.init(i18nextOptions); }); it('should init', (done) => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); service.init(i18nextOptions).then(() => { expect(service.options).toBeTruthy(); done(); }); }); it('should load namespace', (done) => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); service.init(i18nextOptions).then(()=> { service.loadNamespaces(['somens']).then(() => { done(); }); }); }); it('should translate', (done) => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); const key = 'test'; service.init(i18nextOptions).then(()=> { const serviceResult = service.t(key); expect(serviceResult).toEqual(`${service.options.defaultNS}${service.options.nsSeparator}test`); const serviceResult2 = service.t([key, key + '2']); expect(serviceResult2).toEqual(`${service.options.defaultNS}${service.options.nsSeparator}test2`); done(); }) }); it('should dir', () => { const lng = 'ru'; const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); expect(service.dir(lng)).toEqual('ltr'); expect(service.dir(lng)).toEqual(service.dir(lng)); }); it('should able to pass custom params (no typechecking errors)', () => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); service.t('some.string', { hello: 'there', }); }); it('should be able to return different types while setting returnObjects: true (no typechecking errors)', () => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); service.t('some.string') as string; service.t('some.string', { returnObjects: true }) as string[]; }); /* // does not work because language=cimode ignores default value // setting language to anything other than 'cimode' breaks the rest of the tests xit('should translate with default value', (done) => { const service: ITranslationService = TestBed.inject(I18NEXT_SERVICE); let title: Title = TestBed.inject(Title); let i18nextPipe: I18NextPipe = TestBed.inject(I18NextPipe); const key = 'test2'; const defaultValue = 'test3'; service.events.initialized.subscribe((value) => { if (value) { // service const serviceResult = service.t(key, defaultValue); expect(serviceResult).toEqual(defaultValue); // pipes const pipeResult = i18nextPipe.transform(key, { defaultValue }); expect(pipeResult).toEqual(defaultValue); done(); } }); }); */ }); ================================================ FILE: libs/angular-i18next/src/tests/setup.ts ================================================ import { provideI18NextMockAppInitializer, withMock } from '../../testing/src/public_api'; import { provideI18Next, } from '../lib'; export const MOCK_I18N_PROVIDERS = [ provideI18NextMockAppInitializer(), provideI18Next(withMock()), ]; ================================================ FILE: libs/angular-i18next/ssr/ng-package.json ================================================ {} ================================================ FILE: libs/angular-i18next/ssr/src/provider.ssr.ts ================================================ import { REQUEST_CONTEXT } from '@angular/core'; import { I18NEXT_INSTANCE, I18NextFeature, I18NextFeatureKind, makeI18NextFeature } from 'angular-i18next'; /** * Feature for use when configuring `provideI18Next` to enable SSR. * * @description * This feature expects the Express request object to be injected as * `REQUEST` and will extract the `i18n` object from it. * * @publicApi */ export function withSSR(): I18NextFeature { return makeI18NextFeature(I18NextFeatureKind.SSR, [ { provide: I18NEXT_INSTANCE, useFactory: (reqCtx: any) => { return reqCtx?.i18n; }, deps: [REQUEST_CONTEXT], }, ]); } ================================================ FILE: libs/angular-i18next/ssr/src/public_api.ts ================================================ export * from './provider.ssr'; ================================================ FILE: libs/angular-i18next/testing/ng-package.json ================================================ {} ================================================ FILE: libs/angular-i18next/testing/src/mock.service.ts ================================================ import { Injectable } from '@angular/core'; import { jest } from '@jest/globals'; import { defaultInterpolationFormat, I18NextEvents, I18NextLoadResult, ITranslationEvents, ITranslationService } from 'angular-i18next'; import type { Callback, FormatFunction, i18n, InterpolationOptions, TFunction, Modules, Services, ResourceStore } from 'i18next'; import * as i18next from 'i18next'; @Injectable() export class MockI18NextService implements ITranslationService { private i18next: i18n; get isInitialized() { return this.i18next.isInitialized; } get modules(): Modules { return this.i18next.modules; } get services(): Services { return this.i18next.services; } get store(): ResourceStore { return this.i18next.store; } get resolvedLanguage() { return this.i18next.resolvedLanguage; } constructor( ) { this.i18next = i18next.default; } t = jest.fn((key: string | string[], optionsOrDefault?: string | i18next.TOptions, options?: i18next.TOptions): i18next.TFunctionReturn => { if (key instanceof Array) { return key.length > 0 ? key[0] : ''; } return key; }) format: FormatFunction = jest.fn(( value: any, format?: string, lng?: string, options?: InterpolationOptions & { [key: string]: any }, ) => defaultInterpolationFormat(value, format, lng)); getFixedT(lng: string | readonly string[], ns?: string | readonly string[], keyPrefix?: string): TFunction; getFixedT(lng: null, ns: string | readonly string[] | null, keyPrefix?: string): TFunction; getFixedT(lng: any, ns?: any, keyPrefix?: any): import("i18next").TFunction { throw new Error('Method not implemented.'); } loadLanguages(lngs: string | readonly string[], callback?: Callback): Promise { throw new Error('Method not implemented.'); } loadResources(callback?: (err: any) => void): void { throw new Error('Method not implemented.'); } getDataByLanguage(lng: string): { translation: { [key: string]: string; }; } | undefined { throw new Error('Method not implemented.'); } reloadResources(lngs?: string | readonly string[], ns?: string | readonly string[], callback?: () => void): Promise; reloadResources(lngs: null, ns: string | readonly string[], callback?: () => void): Promise; reloadResources(lngs?: any, ns?: any, callback?: any): Promise { throw new Error('Method not implemented.'); } addResource(lng: string, ns: string, key: string, value: string, options?: { keySeparator?: string | undefined; silent?: boolean | undefined; }): i18n { throw new Error('Method not implemented.'); } addResources(lng: string, ns: string, resources: any): i18n { throw new Error('Method not implemented.'); } addResourceBundle(lng: string, ns: string, resources: any, deep?: boolean, overwrite?: boolean): i18n { throw new Error('Method not implemented.'); } removeResourceBundle(lng: string, ns: string): i18n { throw new Error('Method not implemented.'); } events: ITranslationEvents = new I18NextEvents(); language: string = ''; languages: string[] = []; get options(): any { return { keySeparator: '.', nsSeparator: ':', }; } public use(plugin: any): ITranslationService { return this; } public init(options?: any): Promise { options = options || {}; return new Promise( ( resolve: (thenableOrResult?: any) => void, reject: (error: any) => void ) => { resolve(null); } ); } public changeLanguage(lng: string): Promise { return new Promise( ( resolve: (thenableOrResult?: any) => void, reject: (error: any) => void ) => { this.language = lng; resolve(this.language); } ); } public loadNamespaces(namespaces: string[]): Promise { return new Promise( ( resolve: (thenableOrResult?: any) => void, reject: (error: any) => void ) => { resolve(); } ); } exists(key: any, options: any) { return true; } setDefaultNamespace(ns: string) {} dir(lng: string) { return 'ltr'; } getResource(lng: any, ns: any, key: any, options: any) { return null; } hasResourceBundle(lng: any, ns: any) { return true; } getResourceBundle(lng: any, ns: any) { return null; } } ================================================ FILE: libs/angular-i18next/testing/src/provider.ts ================================================ import { inject, provideAppInitializer } from '@angular/core'; import { I18NEXT_SERVICE, I18NextFeature, I18NextFeatureKind, I18NextLoadResult, interpolationFormat, makeI18NextFeature } from 'angular-i18next'; import { MockI18NextService } from './mock.service'; /** * Initializes i18next with mock settings for testing */ export function mockAppInit() { const i18next = inject(I18NEXT_SERVICE); let promise: Promise = i18next.init({ lng: 'cimode', interpolation: { format: interpolationFormat(), }, }); return promise; } export const provideI18NextMockAppInitializer = () => provideAppInitializer(mockAppInit); /** * Provides a mock implementation of I18NEXT_SERVICE for testing purposes. * Also initializes i18next with mock settings. * * @returns An I18NextFeature that configures the service to use MockI18NextService * * Example: * ```typescript * providers: [ * provideI18Next(withMock()) * ] * ``` */ export function withMock(): I18NextFeature { return makeI18NextFeature(I18NextFeatureKind.Mock, [ { provide: I18NEXT_SERVICE, useClass: MockI18NextService, }, ]); } ================================================ FILE: libs/angular-i18next/testing/src/public_api.ts ================================================ export * from './mock.service'; export * from './provider'; ================================================ FILE: libs/angular-i18next/tsconfig.json ================================================ { "extends": "../../tsconfig.base.json", "compilerOptions": { "target": "es2022", "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" } ], "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true, "compilationMode": "partial" } } ================================================ FILE: libs/angular-i18next/tsconfig.lib.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../dist/out-tsc", "declaration": true, "declarationMap": false, "types": [] }, "exclude": [ "src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts" ], "include": ["src/**/*.ts"] } ================================================ FILE: libs/angular-i18next/tsconfig.spec.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../dist/out-tsc", "module": "preserve", "target": "es2016", "types": ["jest", "node"], "allowSyntheticDefaultImports": false, "esModuleInterop": true, "isolatedModules": true }, "files": ["src/test-setup.ts"], "include": [ "jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: migrations.json ================================================ { "migrations": [ { "version": "22.0.0-beta.1", "description": "Updates release version config based on the breaking changes in Nx v22", "implementation": "./src/migrations/update-22-0-0/release-version-config-changes", "package": "nx", "name": "22-0-0-release-version-config-changes" }, { "version": "22.0.0-beta.2", "description": "Consolidates releaseTag* options into nested releaseTag object structure", "implementation": "./src/migrations/update-22-0-0/consolidate-release-tag-config", "package": "nx", "name": "22-0-0-consolidate-release-tag-config" }, { "cli": "nx", "version": "22.1.0-beta.5", "description": "Updates the nx wrapper.", "implementation": "./src/migrations/update-22-1-0/update-nx-wrapper", "package": "nx", "name": "22-1-0-update-nx-wrapper" }, { "version": "21.5.0-beta.2", "description": "Migrate the legacy 'development' custom condition to a workspace-unique custom condition name.", "factory": "./src/migrations/update-21-5-0/migrate-development-custom-condition", "package": "@nx/js", "name": "migrate-development-custom-condition" }, { "version": "22.0.0-beta.0", "description": "Remove the deprecated `external` and `externalBuildTargets` options from the `@nx/js:swc` and `@nx/js:tsc` executors.", "factory": "./src/migrations/update-22-0-0/remove-external-options-from-js-executors", "package": "@nx/js", "name": "remove-external-options-from-js-executors" }, { "version": "22.1.0-rc.1", "description": "Removes redundant TypeScript project references from project's tsconfig.json files when runtime tsconfig files (e.g., tsconfig.lib.json, tsconfig.app.json) exist.", "factory": "./src/migrations/update-22-1-0/remove-redundant-ts-project-references", "package": "@nx/js", "name": "remove-redundant-ts-project-references" }, { "version": "22.2.0-beta.2", "description": "Convert jest.config.ts files from ESM to CJS syntax (export default -> module.exports, import -> require) for projects using CommonJS resolution to ensure correct loading under Node.js type-stripping.", "implementation": "./src/migrations/update-22-2-0/convert-jest-config-to-cjs", "package": "@nx/jest", "name": "convert-jest-config-to-cjs" }, { "version": "22.3.2-beta.0", "requires": { "jest": ">=30.0.0" }, "description": "Replace removed matcher aliases in Jest v30 with their corresponding matcher", "implementation": "./src/migrations/update-21-3-0/replace-removed-matcher-aliases", "package": "@nx/jest", "name": "replace-removed-matcher-aliases-v22-3" } ] } ================================================ FILE: nx.json ================================================ { "affected": { "defaultBase": "master" }, "targetDependencies": { "build": [ { "target": "build", "projects": "dependencies" } ] }, "cli": { "packageManager": "npm" }, "generators": { "@nx/angular:application": { "style": "css", "linter": "eslint", "unitTestRunner": "jest", "e2eTestRunner": "cypress" }, "@nx/angular:library": { "linter": "eslint", "unitTestRunner": "jest" }, "@nx/angular:component": { "style": "css" }, "@nx/angular": { "application": { "linter": "eslint" }, "library": { "linter": "eslint" }, "storybook-configuration": { "linter": "eslint" } } }, "defaultProject": "angular-i18next", "$schema": "./node_modules/nx/schemas/nx-schema.json", "targetDefaults": { "lint": { "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], "cache": true }, "build": { "cache": true, "inputs": ["production", "^production"] }, "e2e": { "cache": true }, "@nx/jest:jest": { "cache": true, "inputs": ["default", "^default", "{workspaceRoot}/jest.preset.js"], "options": { "passWithNoTests": true }, "configurations": { "ci": { "ci": true, "codeCoverage": true } } }, "test": { "cache": true } }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "sharedGlobals": [], "production": ["default"] }, "neverConnectToCloud": true } ================================================ FILE: package.json ================================================ { "name": "angular-i18next-rep", "version": "0.0.1", "author": { "name": "Sergey Romanchuk" }, "homepage": "https://github.com/Romanchuk/angular-i18next#readme", "repository": { "type": "git", "url": "git+https://github.com/Romanchuk/angular-i18next.git" }, "license": "MIT", "description": "i18next module for Angular", "keywords": [ "i18n", "i18next", "angular" ], "bugs": { "url": "https://github.com/Romanchuk/angular-i18next/issues" }, "engines": { "node": ">=22.13.0" }, "maintainers": [ { "email": "rezety@gmail.com", "name": "Sergey Romanchuk" } ], "scripts": { "start": "npx nx run angular-i18next-demo:serve:development", "test": "npx nx test angular-i18next", "lint": "npx nx workspace-lint && ng lint", "affected:apps": "npx nx affected:apps", "affected:libs": "npx nx affected:libs", "affected:build": "npx nx affected:build", "affected:e2e": "npx nx affected:e2e", "affected:test": "npx nx affected:test", "affected:lint": "npx nx affected:lint", "affected:dep-graph": "npx nx affected:dep-graph", "affected": "npx nx affected", "build": "npx nx run angular-i18next-demo:build", "prepare:demo": "npx cpy --cwd=dist/angular-i18next-demo/browser --rename=index.html ./index.csr.html ./", "format": "npx nx format:write", "format:write": "npx nx format:write", "format:check": "npx nx format:check", "update": "npx nx migrate latest", "release": "npx nx run angular-i18next:release", "pm2:prod": "npx pm2-runtime start ecosystem.config.js --env production" }, "devDependencies": { "@angular-devkit/build-angular": "21.1.1", "@angular-devkit/core": "21.1.1", "@angular-devkit/schematics": "21.1.1", "@angular-eslint/eslint-plugin": "21.1.0", "@angular-eslint/eslint-plugin-template": "21.1.0", "@angular-eslint/template-parser": "21.1.0", "@angular/cli": "21.1.1", "@angular/compiler-cli": "21.1.1", "@angular/language-service": "21.1.1", "@angular/platform-browser": "21.1.1", "@angular/platform-browser-dynamic": "21.1.1", "@angular/platform-server": "21.1.1", "@nx/angular": "22.4.0", "@nx/eslint": "22.4.0", "@nx/eslint-plugin": "22.4.0", "@nx/jest": "22.4.0", "@nx/js": "22.4.0", "@nx/workspace": "22.4.0", "@schematics/angular": "21.1.1", "@swc-node/register": "~1.11.1", "@swc/core": "~1.13.4", "@swc/helpers": "~0.5.11", "@types/express": "4.17.14", "@types/jest": "^29.5.14", "@types/node": "20.9.1", "@typescript-eslint/eslint-plugin": "7.11.0", "@typescript-eslint/parser": "7.11.0", "@typescript-eslint/utils": "^7.16.0", "browser-sync": "^3.0.3", "conventional-changelog-cli": "^2.1.1", "conventional-github-releaser": "^3.1.5", "coveralls": "^3.1.0", "cpy-cli": "^4.1.0", "cross-env": "~7.0.3", "cz-conventional-changelog": "^2.1.0", "es6-shim": "0.35.4", "eslint": "8.57.0", "eslint-config-prettier": "10.0.0", "i18next": "24.2.2", "jest": "~30.2.0", "jest-environment-jsdom": "~30.2.0", "jest-preset-angular": "16.0.0", "keyv": "^4.5.1", "ng-packagr": "21.1.0", "np": "11.0.1", "nx": "22.4.0", "pm2": "5.4.3", "prettier": "3.1.0", "replace": "^1.2.0", "require-dir": "^1.2.0", "rimraf": "3.0.0", "ts-jest": "29.4.1", "ts-node": "10.9.2", "typescript": "~5.9.3", "webpack": "~5.89.0", "jest-util": "30.0.5" }, "dependencies": { "@angular/common": "21.1.1", "@angular/compiler": "21.1.1", "@angular/core": "21.1.1", "@angular/forms": "21.1.1", "@angular/platform-browser": "21.1.1", "@angular/platform-browser-dynamic": "21.1.1", "@angular/platform-server": "21.1.1", "@angular/router": "21.1.1", "@angular/ssr": "21.1.1", "express": "~4.18.2", "i18next": "25.4.0", "i18next-browser-languagedetector": "~8.0.4", "i18next-chained-backend": "~4.6.2", "i18next-http-backend": "~2.4.1", "i18next-http-middleware": "~3.7.1", "i18next-resources-to-backend": "~1.2.0", "rxjs": "~7.8.2", "tslib": "~2.6.2" }, "optionalDependencies": { "@nx/nx-linux-x64-gnu": "20.3.1" } } ================================================ FILE: tools/tsconfig.tools.json ================================================ { "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "../dist/out-tsc/tools", "rootDir": ".", "module": "commonjs", "target": "es5", "types": ["node"], "importHelpers": false }, "include": ["**/*.ts"] } ================================================ FILE: tsconfig.base.json ================================================ { "compileOnSave": false, "compilerOptions": { "rootDir": ".", "sourceMap": true, "declaration": false, "moduleResolution": "bundler", "alwaysStrict": true, "forceConsistentCasingInFileNames": true, "downlevelIteration": true, "experimentalDecorators": true, "noImplicitOverride": true, "noImplicitReturns": true, "noImplicitAny": true, "noImplicitThis": true, "importHelpers": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictNullChecks": true, "target": "ES2022", "module": "ES2022", "lib": ["ES2022", "dom"], "strict": true, "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { "angular-i18next": ["libs/angular-i18next/src/index.ts"], "angular-i18next/forms": ["libs/angular-i18next/forms/src/public_api.ts"], "angular-i18next/ssr": ["libs/angular-i18next/ssr/src/public_api.ts"], "angular-i18next/testing": [ "libs/angular-i18next/testing/src/public_api.ts" ], } }, "exclude": ["node_modules", "tmp"] } ================================================ FILE: tsconfig.json ================================================ { "extends": "./tsconfig.base.json" }