├── docs ├── _config.yml ├── _data │ └── menu.yml ├── javascripts │ └── scale.fix.js ├── TranslateComponent.md ├── TranslateLogHandler.md ├── TranslatorContainer.md ├── TranslatePipe.md ├── images │ ├── modules.svg │ ├── classes.graphml │ └── classes-1.4.graphml ├── _layouts │ └── default.html ├── TranslationLoaderJson.md ├── TranslationLoader.md ├── TranslatorConfig.md ├── stylesheets │ ├── highlight.css │ └── styles.css ├── modules.md ├── dynamize.md ├── Translator.md └── index.md ├── tests ├── helper │ ├── JasmineHelper.ts │ ├── TranslatorMocks.ts │ └── promise-matcher.ts ├── TranslationLoader.spec.ts ├── TranslatorModule.spec.ts ├── TranslateLogHandler.spec.ts ├── TranslationLoader │ ├── Fake.spec.ts │ └── Json.spec.ts ├── TranslatorContainer.spec.ts ├── TranslateComponent.spec.ts ├── TranslatePipe.spec.ts └── TranslatorConfig.spec.ts ├── .npmignore ├── .travis.yml ├── .gitignore ├── src ├── TranslateLogHandler.ts ├── TranslationLoader │ ├── Fake.ts │ └── Json.ts ├── TranslationLoader.ts ├── TranslateComponent.ts ├── TranslatorModule.ts ├── TranslatePipe.ts ├── TranslatorContainer.ts └── TranslatorConfig.ts ├── index.ts ├── tslint.json ├── tsconfig.json ├── config ├── helpers.js ├── spec-bundle.js ├── karma.conf.js └── webpack.test.js ├── typings └── promise-matcher │ └── index.d.ts ├── LICENSE ├── webpack.config.js ├── package.json └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | baseurl: /angular-translator 2 | -------------------------------------------------------------------------------- /tests/helper/JasmineHelper.ts: -------------------------------------------------------------------------------- 1 | export class JasmineHelper { 2 | public static calls(spy: any): jasmine.Calls { 3 | return spy.calls; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /typings/ 3 | /tests/ 4 | /.idea/ 5 | 6 | /docs/* 7 | !/docs/*.md 8 | !/docs/images/ 9 | 10 | /src/**/*.ts 11 | !/src/**/*.d.ts 12 | 13 | .gitignore 14 | .editorconfig 15 | karma.conf.js 16 | node_modules 17 | npm-debug.log 18 | tsconfig.json 19 | tslint.json 20 | typings.json 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | chrome: stable 4 | cache: npm 5 | sudo: false 6 | node_js: 7 | - '10' 8 | 9 | matrix: 10 | fast_finish: true 11 | 12 | before_script: 13 | - npm ci 14 | - npm install coveralls 15 | 16 | script: 17 | - npm run tslint 18 | - npm test 19 | - npm run coveralls 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm - without package-lock 2 | /node_modules 3 | /npm-debug.log 4 | 5 | # generated files 6 | coverage/* 7 | docs/_site 8 | /bundles 9 | /src/**/*.js* 10 | /src/**/*.d.ts 11 | /index.* 12 | !/index.ts 13 | /tests/**/*.js* 14 | /tests/**/*.d.ts 15 | !/tests/helper/promise-matchers.d.ts 16 | 17 | # ide 18 | .editorconfig 19 | -------------------------------------------------------------------------------- /src/TranslateLogHandler.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | export class TranslateLogHandler { 3 | public error(message: string | Error): void { 4 | if (console && console.error) { 5 | console.error(message); 6 | } 7 | } 8 | 9 | public info(message: string): void { 10 | } 11 | 12 | public debug(message: string): void { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/TranslateComponent"; 2 | export * from "./src/TranslateLogHandler"; 3 | export * from "./src/TranslatePipe"; 4 | export * from "./src/TranslationLoader"; 5 | export * from "./src/TranslationLoader/Json"; 6 | export * from "./src/TranslationLoader/Fake"; 7 | export * from "./src/Translator"; 8 | export * from "./src/TranslatorConfig"; 9 | export * from "./src/TranslatorContainer"; 10 | export * from "./src/TranslatorModule"; 11 | -------------------------------------------------------------------------------- /docs/_data/menu.yml: -------------------------------------------------------------------------------- 1 | Introduction: / 2 | Dynamize: /dynamize.html 3 | Modules: /modules.html 4 | 5 | ' ': /placeholder 6 | 7 | TranslateComponent: /TranslateComponent.html 8 | TranslateLogHandler: /TranslateLogHandler.html 9 | TranslatePipe: /TranslatePipe.html 10 | TranslationLoader: /TranslationLoader.html 11 | TranslationLoaderJson: /TranslationLoaderJson.html 12 | Translator: /Translator.html 13 | TranslatorConfig: /TranslatorConfig.html 14 | TranslatorContainer: /TranslatorContainer.html 15 | -------------------------------------------------------------------------------- /tests/TranslationLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import {TranslationLoader} from "../src/TranslationLoader"; 2 | 3 | describe("TranslationLoader", () => { 4 | it("is defined", () => { 5 | expect(TranslationLoader).toBeDefined(); 6 | }); 7 | 8 | // because it is abstract we can only test if it is defined 9 | // we can not test: 10 | // - is abstract 11 | // - has abstract method load 12 | 13 | // the interface: ITranslateLoader { public load(lang: string): Promise } 14 | }); 15 | -------------------------------------------------------------------------------- /src/TranslationLoader/Fake.ts: -------------------------------------------------------------------------------- 1 | 2 | import { TranslationLoader } from "../TranslationLoader"; 3 | 4 | import { Injectable } from "@angular/core"; 5 | 6 | @Injectable() 7 | export class TranslationLoaderFake extends TranslationLoader { 8 | protected translations: any = {}; 9 | 10 | public addTranslations(translations: any = {}) { 11 | this.flattenTranslations(this.translations, translations); 12 | } 13 | 14 | public load(): Promise { 15 | return Promise.resolve(this.translations); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | "no-empty": false, 5 | "no-eval": false, 6 | "no-console": [true, "log", "info", "debug"], 7 | "switch-default": false, 8 | "whitespace": false, 9 | "no-submodule-imports": [true, "rxjs", "@angular/common", "@angular/platform-browser", "@angular/core/testing"], 10 | "variable-name": [true, "allow-leading-underscore"], 11 | "only-arrow-functions": false, 12 | "object-literal-sort-keys": false, 13 | "prefer-const": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "declaration": true, 11 | "noImplicitAny": false, 12 | "typeRoots": [ 13 | "node_modules/@types", 14 | "typings" 15 | ] 16 | }, 17 | "exclude": [ 18 | "node_modules" 19 | ], 20 | "angularCompilerOptions": { 21 | "disableTypeScriptVersionCheck": true, 22 | "skipTemplateCodegen": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/javascripts/scale.fix.js: -------------------------------------------------------------------------------- 1 | var metas = document.getElementsByTagName('meta'); 2 | var i; 3 | if (navigator.userAgent.match(/iPhone/i)) { 4 | for (i=0; i { 13 | return Promise.resolve(this._provided[lang] || {}); 14 | } 15 | } 16 | 17 | export class TranslateLogHandlerMock extends TranslateLogHandler { 18 | public error(message: string): void {} 19 | } 20 | -------------------------------------------------------------------------------- /src/TranslationLoader.ts: -------------------------------------------------------------------------------- 1 | export abstract class TranslationLoader { 2 | public abstract load(options: any): Promise; 3 | 4 | protected flattenTranslations(translations: any, data: any, prefix: string = "") { 5 | for (let key in data) { 6 | if (Array.isArray(data[key])) { 7 | translations[prefix + key] = data[key].filter((v: any) => typeof v === "string").join(""); 8 | } else if (typeof data[key] === "object") { 9 | this.flattenTranslations(translations, data[key], prefix + key + "."); 10 | } else if (typeof data[key] === "string") { 11 | translations[prefix + key] = data[key]; 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/TranslatorModule.spec.ts: -------------------------------------------------------------------------------- 1 | import {TranslateComponent, Translator, TranslatorModule} from "../index"; 2 | 3 | import {TestBed} from "@angular/core/testing"; 4 | 5 | describe("TranslatorModule", () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ TranslatorModule.forRoot() ], 9 | }); 10 | }); 11 | 12 | it("provides the default Translator", () => { 13 | let translator = TestBed.get(Translator); 14 | 15 | expect(translator.module).toBe("default"); 16 | }); 17 | 18 | it("defines the translate component", () => { 19 | let component = TestBed.createComponent(TranslateComponent); 20 | 21 | expect(component.componentInstance).toEqual(jasmine.any(TranslateComponent)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/TranslateLogHandler.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TranslateLogHandler, 3 | } from "../index"; 4 | 5 | describe("TranslateLogHandler", () => { 6 | it("writes errors to console.error", () => { 7 | spyOn(console, "error"); 8 | let logHandler: TranslateLogHandler = new TranslateLogHandler(); 9 | 10 | logHandler.error("This was bad"); 11 | 12 | expect(console.error).toHaveBeenCalledWith("This was bad"); 13 | }); 14 | 15 | it("does not throw when console.error is undefined", () => { 16 | let logHandler: TranslateLogHandler = new TranslateLogHandler(); 17 | let error = console.error; 18 | 19 | delete console.error; 20 | let action = () => { 21 | logHandler.error("This was bad"); 22 | }; 23 | 24 | expect(action).not.toThrow(); 25 | console.error = error; 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * taken from angular2-webpack-starter 3 | */ 4 | var path = require('path'); 5 | 6 | // Helper functions 7 | var ROOT = path.resolve(__dirname, '..'); 8 | 9 | function hasProcessFlag(flag) { 10 | return process.argv.join('').indexOf(flag) > -1; 11 | } 12 | 13 | function isWebpackDevServer() { 14 | return process.argv[1] && !! (/webpack-dev-server$/.exec(process.argv[1])); 15 | } 16 | 17 | function root(args) { 18 | args = Array.prototype.slice.call(arguments, 0); 19 | return path.join.apply(path, [ROOT].concat(args)); 20 | } 21 | 22 | function checkNodeImport(context, request, cb) { 23 | if (!path.isAbsolute(request) && request.charAt(0) !== '.') { 24 | cb(null, 'commonjs ' + request); return; 25 | } 26 | cb(); 27 | } 28 | 29 | exports.hasProcessFlag = hasProcessFlag; 30 | exports.isWebpackDevServer = isWebpackDevServer; 31 | exports.root = root; 32 | exports.checkNodeImport = checkNodeImport; -------------------------------------------------------------------------------- /typings/promise-matcher/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module JasminePromiseMatchers { 2 | export function install():void; 3 | export function uninstall():void; 4 | } 5 | 6 | declare module jasmine { 7 | 8 | interface Matchers { 9 | /** 10 | * Verifies that a Promise is (or has been) rejected. 11 | */ 12 | toBeRejected(done?: () => void): boolean; 13 | 14 | /** 15 | * Verifies that a Promise is (or has been) rejected with the specified parameter. 16 | */ 17 | toBeRejectedWith(value: any, done?: () => void): boolean; 18 | 19 | /** 20 | * Verifies that a Promise is (or has been) resolved. 21 | */ 22 | toBeResolved(done?: () => void): boolean; 23 | 24 | /** 25 | * Verifies that a Promise is (or has been) resolved with the specified parameter. 26 | */ 27 | toBeResolvedWith(value: any, done?: () => void): boolean; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Thomas Flori 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /tests/TranslationLoader/Fake.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TranslationLoaderFake, 3 | } from "../../index"; 4 | 5 | import {PromiseMatcher} from "../helper/promise-matcher"; 6 | 7 | describe("TranslationLoaderFake", () => { 8 | it("is defined", () => { 9 | expect(TranslationLoaderFake).toBeDefined(); 10 | }); 11 | 12 | describe("instant", () => { 13 | let loader: TranslationLoaderFake; 14 | 15 | beforeEach(() => { 16 | loader = new TranslationLoaderFake(); 17 | PromiseMatcher.install(); 18 | }); 19 | 20 | afterEach(PromiseMatcher.uninstall); 21 | 22 | it("returns an empty translation table", () => { 23 | let result = loader.load(); 24 | 25 | expect(result).toBeResolvedWith({}); 26 | }); 27 | 28 | it("returns the object from addTranslations", () => { 29 | loader.addTranslations({k1: "value1", k2: "value2"}); 30 | 31 | let result = loader.load(); 32 | 33 | expect(result).toBeResolvedWith({k1: "value1", k2: "value2"}); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /docs/TranslateComponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateComponent 4 | permalink: /TranslateComponent.html 5 | --- 6 | # TranslateComponent 7 | 8 | This is a angular2 component for the selector `[translate]`. It is a component and has a template that is not 9 | keeping your html code inside of the element -> **the element should be empty**. 10 | 11 | ## Example 12 | 13 | ```html 14 |

Loading... this text will be replaced by the translated value

15 | ``` 16 | 17 | ## Parameters 18 | 19 | To [dynamize](docs/dynamize.md) your translation you can add parameters in the attribute `translateParams`. Keep 20 | in minde that it will be a string if you don't add brackets around the parameter and translation only accepts 21 | objects. 22 | 23 | ```html 24 |

25 | ``` 26 | 27 | ## Variable Translation Key 28 | You can make the key variable in two different ways. 29 | 30 | By adding brackets: 31 | 32 | ```html 33 |

34 | ``` 35 | 36 | By using variable inside attribute: 37 | 38 | ```html 39 |

40 | ``` 41 | 42 | ## Module 43 | 44 | You can use a specific module by adding `translatorModule` attribute - you can make it dynamic the same way as 45 | parameters. If this is empty the provided `Translator` will be used. You can overwrite the provided translator in the 46 | component (described in section [Modules](modules.md)). 47 | 48 | ```html 49 |

50 | ``` 51 | -------------------------------------------------------------------------------- /docs/TranslateLogHandler.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateLogHandler 4 | permalink: /TranslateLogHandler.html 5 | --- 6 | # TranslateLogHandler 7 | 8 | This is a very simple class to log infos, errors and debug informations. The default provided class that is used by 9 | default just writes errors to `console.error`. The other two functions have no operations. If you want to send errors 10 | to a logger and or output info or debug messages you have to extend this class. 11 | 12 | Missing translations messages are send to `TranslateLogHandler.info`. 13 | 14 | To overwrite the TranslateLogHandler you can just write this in your app module: 15 | ```ts 16 | import { BrowserModule } from '@angular/platform-browser'; 17 | import { NgModule } from '@angular/core'; 18 | import { FormsModule } from '@angular/forms'; 19 | import { HttpModule } from '@angular/http'; 20 | import { TranslateLogHandler, TranslatorModule } from 'angular-translator'; 21 | 22 | import { AppComponent } from './app.component'; 23 | 24 | export class AppTranslateLogHandler extends TranslateLogHandler { 25 | public info(message: string) { 26 | if (console && console.log) { 27 | console.log(message); 28 | } 29 | } 30 | } 31 | 32 | @NgModule({ 33 | declarations: [ 34 | AppComponent 35 | ], 36 | imports: [ 37 | BrowserModule, 38 | FormsModule, 39 | HttpModule, 40 | TranslatorModule 41 | ], 42 | providers: [ 43 | { provide: TranslateLogHandler, useClass: AppTranslateLogHandler } 44 | ], 45 | bootstrap: [AppComponent] 46 | }) 47 | export class AppModule { } 48 | ``` 49 | -------------------------------------------------------------------------------- /src/TranslationLoader/Json.ts: -------------------------------------------------------------------------------- 1 | import { TranslationLoader } from "../TranslationLoader"; 2 | 3 | import { HttpClient, HttpErrorResponse } from "@angular/common/http"; 4 | import { Injectable } from "@angular/core"; 5 | 6 | @Injectable() 7 | export class TranslationLoaderJson extends TranslationLoader { 8 | constructor(private http: HttpClient) { 9 | super(); 10 | } 11 | 12 | public load({ 13 | language, 14 | module = "default", 15 | path = "assets/i18n/{{ module }}/{{ language }}.json", 16 | }: { 17 | language?: string, 18 | module?: string, 19 | path?: string, 20 | }): Promise { 21 | return new Promise((resolve, reject) => { 22 | let file = path.replace(/\{\{\s*([a-z]+)\s*\}\}/g, (substring: string, ...args: any[]) => { 23 | switch (args[0]) { 24 | case "language": 25 | return language; 26 | case "module": 27 | return module !== "default" ? module : "."; 28 | } 29 | }); 30 | this.http.get(file) 31 | .subscribe( 32 | (data) => { 33 | let translations = {}; 34 | this.flattenTranslations(translations, data); 35 | resolve(translations); 36 | }, 37 | (response: HttpErrorResponse) => { 38 | reject(response.statusText); 39 | }, 40 | ); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from angular2-webpack-starter 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const path = require('path'); 7 | 8 | /** 9 | * Webpack Plugins 10 | */ 11 | const ProvidePlugin = require('webpack/lib/ProvidePlugin'); 12 | const DefinePlugin = require('webpack/lib/DefinePlugin'); 13 | const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 14 | 15 | module.exports = { 16 | devtool: 'source-map', 17 | 18 | resolve: { 19 | extensions: ['.ts', '.js'] 20 | }, 21 | 22 | entry: path.resolve('./index'), 23 | 24 | output: { 25 | path: path.resolve('./bundles'), 26 | publicPath: 'angular-translator', 27 | filename: 'angular-translator.js', 28 | libraryTarget: 'umd', 29 | library: 'angular-translator' 30 | }, 31 | 32 | // require those dependencies but don't bundle them 33 | externals: [/^\@angular\//, /^rxjs\//], 34 | 35 | module: { 36 | rules: [ { 37 | test: /\.ts$/, 38 | loader: 'awesome-typescript-loader' 39 | }] 40 | }, 41 | 42 | plugins: [ 43 | // fix the warning in ./~/@angular/core/src/linker/system_js_ng_module_factory_loader.js 44 | new webpack.ContextReplacementPlugin( 45 | /angular(\\|\/)core(\\|\/)@angular/, 46 | path.resolve('./src') 47 | ), 48 | 49 | new webpack.LoaderOptionsPlugin({ 50 | options: { 51 | tslintLoader: { 52 | emitErrors: false, 53 | failOnHint: false 54 | } 55 | } 56 | }) 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /src/TranslateComponent.ts: -------------------------------------------------------------------------------- 1 | import { TranslateLogHandler } from "./TranslateLogHandler"; 2 | import { Translator } from "./Translator"; 3 | import { TranslatorContainer } from "./TranslatorContainer"; 4 | 5 | import { Component, Input } from "@angular/core"; 6 | import { Subscription } from "rxjs"; 7 | 8 | @Component({ 9 | selector: "[translate]", 10 | template: "{{translation}}", 11 | inputs: [ 12 | "params:translateParams", 13 | "module:translatorModule", 14 | ], 15 | }) 16 | export class TranslateComponent { 17 | public translation: string = ""; 18 | 19 | private _key: string; 20 | private _params: any = {}; 21 | private subscription: Subscription; 22 | 23 | constructor( 24 | private translator: Translator, 25 | private translatorContainer: TranslatorContainer, 26 | private logHandler: TranslateLogHandler, 27 | ) { 28 | this.subscription = this.translator.languageChanged.subscribe(() => { 29 | this.startTranslation(); 30 | }); 31 | } 32 | 33 | @Input("translate") set key(key: string) { 34 | this._key = key; 35 | this.startTranslation(); 36 | } 37 | 38 | set params(params: any) { 39 | if (typeof params !== "object") { 40 | this.logHandler.error("Params have to be an object"); 41 | return; 42 | } 43 | 44 | this._params = params; 45 | this.startTranslation(); 46 | } 47 | 48 | set module(module: string) { 49 | if (this.subscription) { 50 | this.subscription.unsubscribe(); 51 | } 52 | this.translator = this.translatorContainer.getTranslator(module); 53 | this.subscription = this.translator.languageChanged.subscribe(() => { 54 | this.startTranslation(); 55 | }); 56 | this.startTranslation(); 57 | } 58 | 59 | private startTranslation() { 60 | if (!this._key) { 61 | return; 62 | } 63 | this.translator.translate(this._key, this._params).then( 64 | (translation) => this.translation = String(translation), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/TranslatorContainer.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateContainer 4 | permalink: /TranslatorContainer.html 5 | --- 6 | # TranslatorContainer 7 | 8 | The `TranslationContainer` is a container that holds all instances of `Translator` and creates new instances when 9 | needed. 10 | 11 | It also has a property `language` where you can change the language for all contained `Translator` instances. To get 12 | this to work every module should have a subset of provided languages. 13 | 14 | ## The Public Properties 15 | 16 | | Name | Type | Access level | Description | 17 | |-----------------|---------------------|--------------|-------------| 18 | | language | string | read/write | The setter for this property is checking if the language is provided in the root configuration or not. If the language is not provided it does not change. | 19 | | languageChanged | Observable | read | The observer fires next when the language got changed. All `Translator` instances subscribe to this `Observable`. | 20 | 21 | ## The Public Methods 22 | 23 | ### getTranslator(module: string): Translator 24 | 25 | One reason to use this class is to get a `Translator` for a specific module when you don't want to overwrite the 26 | provider for `Translator`. 27 | 28 | ```ts 29 | import { Injectable } from '@angular/core'; 30 | 31 | import { Translator, TranslatorContainer } from 'angular-translator'; 32 | 33 | @Injectable() 34 | export class MyService { 35 | public translations: object = {}; 36 | 37 | constructor(private translator: Translator, private translatorContainer: TranslatorContainer) { 38 | // this comes from default module (assets/i18n/{{language}}.json) 39 | translator.translateArray(['STATUS_PENDING', 'STATUS_DONE']).then((translations) => { 40 | this.translations['STATUS_PENDING'] = translations[0]; 41 | this.translations['STATUS_DONE'] = translations[1]; 42 | }); 43 | 44 | // ['months.0', 'month.1', ...'month.11'] 45 | let months = [...Array(12).keys()].map(function(i) { return 'month.'+i; }); 46 | 47 | // this comes from 'calendar' module (assets/i18n/calendar/{{language}}.json) 48 | translatorContainer.getTranslator('calendar').translate(months).then((months) => { 49 | this.translations['months'] = months; 50 | }); 51 | } 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /config/spec-bundle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * When testing with webpack and ES6, we have to do some extra 3 | * things to get testing to work right. Because we are gonna write tests 4 | * in ES6 too, we have to compile those as well. That's handled in 5 | * karma.conf.js with the karma-webpack plugin. This is the entry 6 | * file for webpack test. Just like webpack will create a bundle.js 7 | * file for our client, when we run test, it will compile and bundle them 8 | * all here! Crazy huh. So we need to do some setup 9 | */ 10 | Error.stackTraceLimit = Infinity; 11 | 12 | require('core-js'); 13 | 14 | // Typescript emit helpers polyfill 15 | require('ts-helpers'); 16 | 17 | require('zone.js/dist/zone'); 18 | require('zone.js/dist/zone-testing'); 19 | // require('zone.js/dist/long-stack-trace-zone'); 20 | // require('zone.js/dist/async-test'); 21 | // require('zone.js/dist/fake-async-test'); 22 | // require('zone.js/dist/sync-test'); 23 | // require('zone.js/dist/proxy'); // since zone.js 0.6.15 24 | // require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14 25 | 26 | // RxJS 27 | require('rxjs/Rx'); 28 | 29 | var testing = require('@angular/core/testing'); 30 | var browser = require('@angular/platform-browser-dynamic/testing'); 31 | 32 | Zone[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true; 33 | 34 | testing.TestBed.initTestEnvironment( 35 | browser.BrowserDynamicTestingModule, 36 | browser.platformBrowserDynamicTesting() 37 | ); 38 | 39 | /* 40 | * Ok, this is kinda crazy. We can use the context method on 41 | * require that webpack created in order to tell webpack 42 | * what files we actually want to require or import. 43 | * Below, context will be a function/object with file names as keys. 44 | * Using that regex we are saying look in ../src then find 45 | * any file that ends with spec.ts and get its path. By passing in true 46 | * we say do this recursively 47 | */ 48 | var testContext = require.context('../tests', true, /\.spec\.ts/); 49 | 50 | /* 51 | * get all the files, for each file, call the context function 52 | * that will require the file and load it up here. Context will 53 | * loop and require those spec files here 54 | */ 55 | function requireAll(requireContext) { 56 | return requireContext.keys().map(requireContext); 57 | } 58 | 59 | // requires and returns all modules that match 60 | var modules = requireAll(testContext); 61 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function(config) { 4 | var testWebpackConfig = require('./webpack.test.js'); 5 | 6 | var configuration = { 7 | basePath: '../', 8 | 9 | frameworks: ['jasmine'], 10 | 11 | // list of files to exclude 12 | exclude: [], 13 | 14 | /* 15 | * list of files / patterns to load in the browser 16 | * 17 | * we are building the test environment in ./spec-bundle.js 18 | */ 19 | files: [ { pattern: './config/spec-bundle.js', watched: false } ], 20 | 21 | preprocessors: { './config/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] }, 22 | 23 | // Webpack Config at ./webpack.test.js 24 | webpack: testWebpackConfig, 25 | 26 | coverageReporter: { 27 | type: 'in-memory' 28 | }, 29 | 30 | coverageIstanbulReporter: { 31 | reports: ['html', 'lcovonly', 'text-summary'], 32 | dir: path.join(__dirname, '..', 'coverage'), 33 | 'report-config': { 34 | html: {subdir: 'html'} 35 | } 36 | }, 37 | 38 | // Webpack please don't spam the console when running in karma! 39 | webpackMiddleware: { stats: 'errors-only'}, 40 | 41 | reporters: [ 'spec', 'coverage-istanbul' ], 42 | 43 | specReporter: { 44 | showSpecTiming: true 45 | }, 46 | 47 | // web server port 48 | port: 9876, 49 | 50 | colors: true, 51 | 52 | /* 53 | * level of logging 54 | * possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 55 | */ 56 | logLevel: config.LOG_INFO, 57 | 58 | autoWatch: false, 59 | 60 | browsers: [ 61 | 'ChromeHeadless', 62 | 'FirefoxHeadless' 63 | ], 64 | 65 | customLaunchers: { 66 | ChromeTravisCi: { 67 | base: 'ChromeHeadless', 68 | flags: ['--no-sandbox'] 69 | }, 70 | }, 71 | 72 | singleRun: true 73 | }; 74 | 75 | if (process.env.TRAVIS){ 76 | configuration.browsers = [ 77 | 'ChromeTravisCi', 78 | 'FirefoxHeadless', 79 | ]; 80 | } 81 | 82 | config.set(configuration); 83 | }; 84 | -------------------------------------------------------------------------------- /docs/TranslatePipe.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslatePipe 4 | permalink: /TranslatePipe.html 5 | --- 6 | # TranslatePipe 7 | 8 | The TranslatePipe is the easiest way for translation. 9 | 10 | ## Usage 11 | 12 | Here are some examples how we can use it: 13 | 14 | ```html 15 |
Simple translation of words or phrases
16 |

{% raw %}{{ 'HELLO_WORLD' | translate }}{% endraw %}

17 | 18 |
Translate a variable
19 |

{% raw %}{{ status | translate }}{% endraw %}

20 | 21 |
Translate with variable
22 |

{% raw %}{{ 'LAST_LOGIN' |translate: logindate }}{% endraw %}

23 | 24 |
Translate with defined parameters
25 |

{% raw %}{{ 'GREET' | translate: { firstName: user.firstName, lastName: user.lastName } }}{% endraw %}

26 | ``` 27 | 28 | ## Module 29 | 30 | You can use a specific module by adding a second parameter. If this parameter is not given the provided `Translator` 31 | will be used. You can overwrite the provided translator in the component (described in section [Modules](modules.md)). 32 | 33 | ```html 34 |

{% raw %}{{ 'TITLE' | translate:{}:'admin' }}{% endraw %}

35 | ``` 36 | 37 | ## Better translate before 38 | 39 | For the most use cases we suggest you to translate before in the component. Here are the examples again: 40 | 41 | ```ts 42 | import { Component } from '@angular/core'; 43 | 44 | import { Translator } from 'angular-translator'; 45 | 46 | @Component({ 47 | selector: 'my-component', 48 | template: ' 49 |

{% raw %}{{ translatedStatus }}{% endraw %}

50 | 51 |

{% raw %}{{ lastLogin }}{% endraw %}

52 | 53 |

{% raw %}{{ greeting }}{% endraw %}

' 54 | }) 55 | export class MyComponent { 56 | constructor(private translator: Translator) { 57 | this.status = 'PENDING'; 58 | 59 | this.logindate = new Date('2016-02-11'); 60 | 61 | this.user = { 62 | firstName: 'John', 63 | lastName: 'Doe' 64 | } 65 | 66 | this.getTranslations(); 67 | 68 | translator.languageChanged.subscribe(() => this.getTranslations()); 69 | } 70 | 71 | private getTranslations() { 72 | TranslateService.waitForTranslation().then(() => { 73 | this.translatedStatus = TranslateService.instant(status); 74 | 75 | this.lastLogin = TranslateService.instant('LAST LOGIN', logindate); 76 | 77 | this.greeting = TranslateService.instant('GREET', this.user); 78 | }); 79 | } 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /config/webpack.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from angular2-webpack-starter 3 | */ 4 | 5 | const helpers = require('./helpers'), 6 | webpack = require('webpack'), 7 | LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); 8 | 9 | /** 10 | * Webpack Plugins 11 | */ 12 | 13 | module.exports = { 14 | 15 | /** 16 | * Source map for Karma from the help of karma-sourcemap-loader & karma-webpack 17 | * 18 | * Do not change, leave as is or it wont work. 19 | * See: https://github.com/webpack/karma-webpack#source-maps 20 | */ 21 | devtool: 'inline-source-map', 22 | 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | modules: [helpers.root('src'), 'node_modules'] 26 | }, 27 | 28 | module: { 29 | rules: [{ 30 | enforce: 'pre', 31 | test: /\.js$/, 32 | loader: 'source-map-loader', 33 | exclude: [ 34 | // these packages have problems with their sourcemaps 35 | helpers.root('node_modules/rxjs'), 36 | helpers.root('node_modules/@angular') 37 | ] 38 | }, { 39 | test: /\.ts$/, 40 | loader: 'awesome-typescript-loader', 41 | exclude: [/\.e2e\.ts$/] 42 | }, { 43 | enforce: 'post', 44 | test: /\.(js|ts)$/, 45 | loader: '@jsdevtools/coverage-istanbul-loader', 46 | include: helpers.root('src'), 47 | exclude: [/\.spec\.ts$/, /\.e2e\.ts$/, /node_modules/] 48 | }] 49 | }, 50 | 51 | plugins: [ 52 | // fix the warning in ./~/@angular/core/src/linker/system_js_ng_module_factory_loader.js 53 | new webpack.ContextReplacementPlugin( 54 | /(.+)?angular(\\|\/)core(.+)?/, 55 | helpers.root('./src') 56 | ), 57 | 58 | new LoaderOptionsPlugin({ 59 | debug: true, 60 | options: { 61 | 62 | /** 63 | * Static analysis linter for TypeScript advanced options configuration 64 | * Description: An extensible linter for the TypeScript language. 65 | * 66 | * See: https://github.com/wbuchwalter/tslint-loader 67 | */ 68 | tslint: { 69 | emitErrors: false, 70 | failOnHint: false, 71 | resourcePath: 'src' 72 | } 73 | } 74 | }) 75 | ], 76 | 77 | // disable warnings about bundle size for tests 78 | performance: {hints: false} 79 | }; 80 | -------------------------------------------------------------------------------- /docs/images/modules.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 11 | 13 | 18 | Config: 19 | providedLanguages: ['de', 'en', 'fr', 'ru', 'es', 'it'] 20 | loader: TranslationLoaderJson 21 | loaderOptions: 22 | path: assets/i18n/{{module}}/{{language}}.json 23 | 24 | 29 | Module: menu 30 | 31 | 36 | Module: admin 37 | ModuleConfig: 38 | providedLanguages: de, en 39 | 40 | 45 | Module: calendar 46 | ModuleConfig: 47 | loader: TranslationLoaderStatic 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/TranslatorModule.ts: -------------------------------------------------------------------------------- 1 | import { TranslateComponent } from "./TranslateComponent"; 2 | import { TranslateLogHandler } from "./TranslateLogHandler"; 3 | import { TranslatePipe } from "./TranslatePipe"; 4 | import { TranslationLoaderJson } from "./TranslationLoader/Json"; 5 | import { Translator } from "./Translator"; 6 | import { COMMON_PURE_PIPES, TranslatorConfig } from "./TranslatorConfig"; 7 | import { TranslatorContainer } from "./TranslatorContainer"; 8 | 9 | import { HttpClientModule } from "@angular/common/http"; 10 | import { InjectionToken, ModuleWithProviders, NgModule, PipeTransform, Provider, Type } from "@angular/core"; 11 | 12 | export const TRANSLATOR_OPTIONS: InjectionToken = new InjectionToken("TRANSLATOR_OPTIONS"); 13 | export const TRANSLATOR_MODULE: InjectionToken = new InjectionToken("TRANSLATOR_MODULE"); 14 | 15 | @NgModule({ 16 | declarations: [ 17 | TranslatePipe, 18 | TranslateComponent, 19 | ], 20 | exports: [ 21 | TranslatePipe, 22 | TranslateComponent, 23 | ], 24 | imports: [HttpClientModule], 25 | providers: [ 26 | TranslationLoaderJson, 27 | TranslateLogHandler, 28 | TranslatorContainer, 29 | COMMON_PURE_PIPES, 30 | ], 31 | }) 32 | export class TranslatorModule { 33 | public static forRoot(options: any = {}, module: string = "default"): ModuleWithProviders { 34 | return { 35 | ngModule: TranslatorModule, 36 | providers: [ 37 | { provide: TRANSLATOR_OPTIONS, useValue: options }, 38 | { 39 | provide: TranslatorConfig, useFactory: createTranslatorConfig, deps: [ 40 | TranslateLogHandler, 41 | TRANSLATOR_OPTIONS, 42 | ], 43 | }, 44 | provideTranslator(module), 45 | options.pipes || [], 46 | ], 47 | }; 48 | } 49 | } 50 | 51 | export function provideTranslator(module: string): Provider[] { 52 | return [ 53 | { provide: TRANSLATOR_MODULE, useValue: module }, 54 | { provide: Translator, useFactory: createTranslator, deps: [TranslatorContainer, TRANSLATOR_MODULE] }, 55 | ]; 56 | } 57 | 58 | export function createTranslatorConfig(logHandler: TranslateLogHandler, options: any = {}): TranslatorConfig { 59 | return new TranslatorConfig(logHandler, options); 60 | } 61 | 62 | export function createTranslator(translatorContainer: TranslatorContainer, module: string): Translator { 63 | return translatorContainer.getTranslator(module); 64 | } 65 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | angular-translator - {{ page.title }} 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 |
18 |

angular-translator
translation tables for angular2

19 | 20 | 21 |
    22 | {% for link in site.data.menu %} 23 |
  • 24 | {{ link[0] }} 25 |
  • 26 | {% endfor %} 27 |
28 |
29 | 30 |

View the Project on GitHub tflori/angular-translator

31 | 32 | Build Status 33 | Coverage Status 34 | npm version 35 | 36 | 41 |
42 |
43 | {{ content }} 44 |
45 |
46 |

This project is maintained by tflori

47 |

Hosted on GitHub Pages — Theme by orderedlist

48 |
49 |
50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-translator", 3 | "version": "3.1.2", 4 | "description": "A translate-service, translate-pipe and translate-component for angular2", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/tflori/angular-translator.git" 8 | }, 9 | "scripts": { 10 | "test": "karma start config/karma.conf.js", 11 | "tslint": "tslint -c tslint.json --exclude \"**/*.d.ts\" --exclude \"node_modules/**/*.ts\" \"**/*.ts\"", 12 | "prepublishOnly": "npm test && npm run build", 13 | "coveralls": "cat coverage/lcov.info | coveralls", 14 | "build": "tsc && ngc && webpack --mode production" 15 | }, 16 | "keywords": [ 17 | "angular2", 18 | "translate", 19 | "i18n" 20 | ], 21 | "author": { 22 | "name": "Thomas Flori", 23 | "email": "thflori@gmail.com", 24 | "url": "https://github.com/tflori" 25 | }, 26 | "contributors": [ 27 | { 28 | "name": "Marc Bornträger", 29 | "url": "https://github.com/BorntraegerMarc" 30 | } 31 | ], 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/tflori/angular-translator/issues" 35 | }, 36 | "main": "./bundles/angular-translator.js", 37 | "typings": "./index.d.ts", 38 | "homepage": "https://github.com/tflori/angular-translator", 39 | "peerDependencies": { 40 | "@angular/common": ">=7.0", 41 | "@angular/compiler": ">=7.0", 42 | "@angular/core": ">=7.0", 43 | "@angular/http": ">=7.0", 44 | "rxjs": "*" 45 | }, 46 | "devDependencies": { 47 | "@angular/common": "^7.2.16", 48 | "@angular/compiler": "^7.2.16", 49 | "@angular/compiler-cli": "^7.2.16", 50 | "@angular/core": "^7.2.16", 51 | "@angular/http": "^7.2.16", 52 | "@angular/platform-browser": "^7.2.16", 53 | "@angular/platform-browser-dynamic": "^7.2.16", 54 | "@types/jasmine": "^2.8.17", 55 | "@types/node": "~14.0.4", 56 | "awesome-typescript-loader": "^5.2.1", 57 | "core-js": "^3.12.1", 58 | "coverage-istanbul-loader": "^3.0.5", 59 | "jasmine-core": "2.5.*", 60 | "karma": "^6.3.2", 61 | "karma-chrome-launcher": "^2.2.0", 62 | "karma-coverage": "^2.0.3", 63 | "karma-coverage-istanbul-reporter": "^3.0.3", 64 | "karma-firefox-launcher": "^2.1.0", 65 | "karma-jasmine": "^1.1.2", 66 | "karma-sourcemap-loader": "^0.3.8", 67 | "karma-spec-reporter": "0.0.31", 68 | "karma-webpack": "^5.0.0", 69 | "reflect-metadata": "^0.1.13", 70 | "rxjs": "^6.6.7", 71 | "rxjs-compat": "^6.6.7", 72 | "source-map-loader": "^0.2.4", 73 | "ts-helpers": "*", 74 | "tslint": "^5.20.1", 75 | "typescript": "^3.2.4", 76 | "webpack": "^5.36.2", 77 | "webpack-cli": "^4.7.0", 78 | "zone.js": "^0.8.29" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/TranslationLoaderJson.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateLoaderJson 4 | permalink: /TranslationLoaderJson.html 5 | --- 6 | # TranslationLoaderJson 7 | 8 | The TranslateLoaderJson loads one json file for each language via angular2/http. 9 | 10 | ## Multiline translations 11 | 12 | To keep order in your translation file your can use arrays for translations. Example: 13 | 14 | ```json 15 | { 16 | "COOKIE_INFORMATION": [ 17 | "We are using cookies, to adjust our website to the needs of our customers. ", 18 | "By using our websites you agree to store cookies on your computer, tablet or smartphone." 19 | ] 20 | } 21 | ``` 22 | 23 | ## Nested translation tables 24 | 25 | For more structure in your translation file we allow objects. Please note that they are merged to one dimension. 26 | 27 | ```json 28 | { 29 | "app": { 30 | "loginText": "Please login before continuing!", 31 | "componentA": { 32 | "TEXT": "something else" 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | The translation table becomes: 39 | 40 | ```json 41 | { 42 | "app.loginText": "Please login before continuing!", 43 | "app.componentA.TEXT": "something else" 44 | } 45 | ``` 46 | 47 | So you can access them with `translate('app.loginText')`. You need to refer to translations with full key too: 48 | 49 | ```json 50 | { 51 | "app": { 52 | "A": "This gets \"something else\": [[ TEXT ]]", 53 | "B": "This gets \"something\" [[ app.TEXT ]]", 54 | "TEXT": "something" 55 | }, 56 | "TEXT": "something else" 57 | } 58 | ``` 59 | 60 | ## Configure 61 | 62 | There is only one option that can be changed: the path where to load json files. This path can contain two 63 | variables in mustache style. The default value is `assets/i18n/{{ module }}/{{ language }}.json`. To change it you 64 | pass this option to the configuration like here: 65 | 66 | > **CAUTION:** the default values changed from version 1.4. Before the default path was `i18n` - so you either change 67 | > this in your config or move the files. 68 | 69 | ### Example with customized path and extension: 70 | Directory structure: 71 | 72 | ``` 73 | + project 74 | + assets 75 | + localization 76 | - en-lang.json 77 | - de-lang.json 78 | - fr-lang.json 79 | + app 80 | - main.ts 81 | - myApp.ts 82 | ``` 83 | 84 | main.ts: 85 | 86 | {% raw %} 87 | ```ts 88 | import { BrowserModule } from '@angular/platform-browser'; 89 | import { NgModule } from '@angular/core'; 90 | 91 | import { TranslatorModule } from 'angular-translator'; 92 | 93 | import { AppComponent } from './app.component'; 94 | 95 | @NgModule({ 96 | declarations: [ 97 | AppComponent 98 | ], 99 | imports: [ 100 | BrowserModule, 101 | TranslatorModule.forRoot({ 102 | providedLanguages: ['en', 'de', 'fr'], 103 | loaderOptions: { 104 | path: 'assets/localization/{{language}}-lang.json 105 | } 106 | }), 107 | ], 108 | providers: [], 109 | bootstrap: [AppComponent] 110 | }) 111 | export class AppModule { } 112 | ``` 113 | {% endraw %} 114 | -------------------------------------------------------------------------------- /src/TranslatePipe.ts: -------------------------------------------------------------------------------- 1 | import { TranslateLogHandler } from "./TranslateLogHandler"; 2 | import { Translator } from "./Translator"; 3 | import { TranslatorContainer } from "./TranslatorContainer"; 4 | 5 | import { Inject, Pipe, PipeTransform } from "@angular/core"; 6 | import { Subscription } from "rxjs"; 7 | 8 | @Pipe({ 9 | name: "translate", 10 | pure: false, 11 | }) 12 | export class TranslatePipe implements PipeTransform { 13 | public static pipeName: string = "translate"; 14 | private static _parseParams(arg: string): object { 15 | try { 16 | let o = eval("(" + arg + ")"); 17 | if (typeof o === "object") { 18 | return o; 19 | } 20 | } catch (e) { 21 | } 22 | return {}; 23 | } 24 | 25 | private promise: Promise; 26 | private translation: string = ""; 27 | private translated: { key: string, params: any, module: string }; 28 | private subscription: Subscription; 29 | 30 | constructor( 31 | private translator: Translator, 32 | private translatorContainer: TranslatorContainer, 33 | private logHandler: TranslateLogHandler, 34 | ) { 35 | this.subscription = this.translator.languageChanged.subscribe(() => { 36 | this.startTranslation(); 37 | }); 38 | } 39 | 40 | /** 41 | * Translates key with given args. 42 | * 43 | * @see TranslateService.translator 44 | * @param {string} key 45 | * @param {object} params 46 | * @param {string?} module 47 | * @returns {string} 48 | */ 49 | public transform(key: string, params: object = {}, module?: string): string { 50 | if (module) { 51 | this.module = module; 52 | } 53 | 54 | // backward compatibility: highly deprecated 55 | if (params instanceof Array) { 56 | params = params[0]; 57 | if (typeof params === "string") { 58 | params = TranslatePipe._parseParams(params); 59 | } 60 | } 61 | 62 | if (this.translated && this.promise && 63 | ( 64 | this.translated.key !== key || 65 | JSON.stringify(this.translated.params) !== JSON.stringify(params) || 66 | this.translated.module !== module 67 | ) 68 | ) { 69 | this.promise = null; 70 | } 71 | 72 | if (!this.promise) { 73 | this.translated = { 74 | key, 75 | params, 76 | module, 77 | }; 78 | this.startTranslation(); 79 | } 80 | 81 | return this.translation; 82 | } 83 | 84 | set module(module: string) { 85 | if (this.subscription) { 86 | this.subscription.unsubscribe(); 87 | } 88 | this.translator = this.translatorContainer.getTranslator(module); 89 | this.subscription = this.translator.languageChanged.subscribe(() => { 90 | this.startTranslation(); 91 | }); 92 | } 93 | 94 | private startTranslation() { 95 | if (!this.translated || !this.translated.key) { 96 | return; 97 | } 98 | this.promise = this.translator.translate(this.translated.key, this.translated.params) as Promise; 99 | this.promise.then((translation) => this.translation = String(translation)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docs/TranslationLoader.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslationLoader 4 | permalink: /TranslationLoader.html 5 | --- 6 | # TranslationLoader 7 | 8 | This abstract class has to be used to define a new `TranslationLoader`. A `TranslationLoader` has to define a method 9 | `load` with the following footprint `load(options: any): Promise`. To inject the loader to 10 | [TranslateService](docs/TranslateService.md) it has to have the annotation `@Injectable()`. 11 | 12 | ## Load method 13 | 14 | This is the only method required in a `TranslationLoader`. The loader gets a string for the language that should be 15 | loaded (defined in `TranslateConfig` see [TranslateConfig](docs/TranslateConfig.md)). The loader should be able to 16 | load every language provided there. 17 | 18 | The loader has to return a `Promise`. This promise can also be rejected if something went wrong and 19 | the language could not be loaded (please provide a meaningful reason). The `Promise` then have to be fulfilled with an 20 | object that holds the translations. This could look like this JSON example: 21 | 22 | ```json 23 | { 24 | "HELLO WORLD": "Привет мир!" 25 | } 26 | ``` 27 | 28 | The load method gets an object with all options set in the `loaderOptions` configuration plus the key `language` and 29 | the key `module`. An example how to use this information can be found in the `TranslationLoaderJson` source. 30 | 31 | ## Static loader example 32 | 33 | Maybe you want to send only one javascript file for performance reasons and the translations should be included. Here 34 | is a complete example how this could look like: 35 | 36 | ```ts 37 | import {Injectable} from "@angular/core"; 38 | 39 | import {TranslationLoader} from "angular-translator"; 40 | 41 | @Injectable() 42 | export class TranslationLoaderStatic extends TranslationLoader { 43 | private translations:Object = { 44 | en: { 45 | "HELLO WORLD": "Hello World!" 46 | }, 47 | fr: { 48 | "HELLO WORLD": "Bonjour le monde!" 49 | }, 50 | de: { 51 | "HELLO WORLD": "Hallo Welt!" 52 | }, 53 | ru: { 54 | "HELLO WORLD": "Привет мир!" 55 | } 56 | }; 57 | 58 | public load({language}: any):Promise { 59 | if (!this.translations[language]) { 60 | Promise.reject("Language unknown"); 61 | } 62 | return Promise.resolve(this.translations[language]); 63 | } 64 | } 65 | ``` 66 | 67 | To use this loader in your application you have to provide it for your application. Here is an example how your 68 | bootstrap can look like: 69 | 70 | ```ts 71 | import { BrowserModule } from '@angular/platform-browser'; 72 | import { NgModule } from '@angular/core'; 73 | 74 | import { TranslatorModule } from "angular-translator"; 75 | 76 | import { AppComponent } from './app.component'; 77 | import { TranslationLoaderStatic } from "./TranslationLoaderStatic" 78 | 79 | 80 | @NgModule({ 81 | declarations: [ 82 | AppComponent 83 | ], 84 | imports: [ 85 | BrowserModule, 86 | TranslatorModule.forRoot({ 87 | defaultLanguage: "ru", 88 | providedLanguages: [ "de", "en", "fr", "ru" ], 89 | detectLanguage: false, 90 | loader: TranslationLoaderStatic 91 | }), 92 | ], 93 | providers: [ 94 | TranslationLoaderStatic, 95 | ], 96 | bootstrap: [AppComponent] 97 | }) 98 | export class AppModule { } 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/TranslatorConfig.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateConfig 4 | permalink: /TranslatorConfig.html 5 | --- 6 | # TranslatorConfig 7 | 8 | As the name reveals `TranslatorConfig` gives a configuration for this module. 9 | 10 | You can change the options by giving an object to the constructor: 11 | ```ts 12 | import { TranslatorConfig } from 'angular-translator'; 13 | 14 | new TranslatorConfig({ 15 | defaultLanguage: 'de' 16 | }); 17 | ``` 18 | 19 | This is done for you within `TranslatorModule.forRoot(config = {})`. You can also change the configuration afterwards 20 | with `TranslatorConfig.setOptions(options)` but we can't recommend as we never tested what happens. There is no reason 21 | to do so. 22 | 23 | ## The Options 24 | 25 | | Name | Type | Default | Description | 26 | |-----------------------|----------|----------|-------------| 27 | | defaultLanguage | string | `'en'` | Defines the default language to be used if no language got set and language detection is disabled or does not detect a language. | 28 | | providedLanguages | string[] | `['en']` | Defines a list of the languages that are supported from you. The provided languages has to match your file names. To make language detection work you should use the ISO format 639-1 (e.g. 'en') or the IETF language tag (e.g. 'de-AT'). You don't have to use "-" and don't have to care about case sensitive. A language 'en/us' will also match a browser language en-US and vise versa - but the file has to be *path**en/us**extension* then. | 29 | | detectLanguage | boolean | `true` | Defines whether the language should be detected by navigator.language(s) when TranslateService got initialized or not. | 30 | | loader | Type | `TranslationLoaderJson` | The loader that is used for loading translations. | 31 | | loaderOptions | any | `{}` | Options that are passed to the loader. | 32 | | modules | any | `{}` | The module configurations. The modules inherit the options from the root and overwrite with the options in this object. (see [Modules](modules.md) for more information) | 33 | 34 | ## The Methods 35 | 36 | ### providedLanguage(lang: string, strict: boolean): string 37 | 38 | Tries to find matching provided language and returns the provided language. The provided language and the language that 39 | is searched are getting normalized for matching. That means that `'EN/usa'` is getting `'en-US'`. 40 | 41 | Only valid language/region combinations are allowed for non-strict matching. This is necessary to exclude finding provided language Breton if the browser says `"british"`. Valid in this case means to use this format: 42 | `[[divider]]`. Or - to be more precise - this regular expression: 43 | `/^([A-Za-z]{2})(?:[.\-_\/]?([A-Za-z]{2}))?$/`. 44 | 45 | Example: 46 | 47 | ```ts 48 | import { TranslatorConfig } from 'angular-translator'; 49 | 50 | var translatorConfig = new TranslatorConfig({ 51 | providedLanguages: ['EN', 'EN/usa'] 52 | }); 53 | 54 | expect(translatorConfig.providedLanguage('en-US')).toBe('EN/usa'); 55 | ``` 56 | 57 | ## Example 58 | 59 | This example shows how you usually use the `TranslatorConfig` class: 60 | 61 | ```ts 62 | import { BrowserModule } from '@angular/platform-browser'; 63 | import { NgModule } from '@angular/core'; 64 | import { TranslatorModule } from "angular-translator"; 65 | 66 | import { AppComponent } from './app.component'; 67 | 68 | @NgModule({ 69 | declarations: [ 70 | AppComponent 71 | ], 72 | imports: [ 73 | BrowserModule, 74 | TranslatorModule.forRoot({ 75 | defaultLanguage: "de", 76 | providedLanguages: [ "de", "en" ], 77 | detectLanguage: false 78 | }), 79 | ], 80 | providers: [], 81 | bootstrap: [AppComponent] 82 | }) 83 | export class AppModule { } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/stylesheets/highlight.css: -------------------------------------------------------------------------------- 1 | pre.highlight, .highlight pre { background-color: #272822; } 2 | .highlight code { color: #999 } 3 | .highlight .hll { background-color: #272822; } 4 | .highlight .c { color: #75715e } /* Comment */ 5 | .highlight .err { color: #f92672 } /* Error */ 6 | .highlight .k { color: #66d9ef } /* Keyword */ 7 | .highlight .l { color: #ae81ff } /* Literal */ 8 | .highlight .n { color: #f8f8f2 } /* Name */ 9 | .highlight .o { color: #f92672 } /* Operator */ 10 | .highlight .p { color: #f8f8f2 } /* Punctuation */ 11 | .highlight .cm { color: #75715e } /* Comment.Multiline */ 12 | .highlight .cp { color: #75715e } /* Comment.Preproc */ 13 | .highlight .c1 { color: #75715e } /* Comment.Single */ 14 | .highlight .cs { color: #75715e } /* Comment.Special */ 15 | .highlight .ge { font-style: italic } /* Generic.Emph */ 16 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 17 | .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 18 | .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 19 | .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 20 | .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 21 | .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 22 | .highlight .kt { color: #66d9ef } /* Keyword.Type */ 23 | .highlight .ld { color: #e6db74 } /* Literal.Date */ 24 | .highlight .m { color: #ae81ff } /* Literal.Number */ 25 | .highlight .s { color: #e6db74 } /* Literal.String */ 26 | .highlight .na { color: #a6e22e } /* Name.Attribute */ 27 | .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 28 | .highlight .nc { color: #a6e22e } /* Name.Class */ 29 | .highlight .no { color: #66d9ef } /* Name.Constant */ 30 | .highlight .nd { color: #a6e22e } /* Name.Decorator */ 31 | .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 32 | .highlight .ne { color: #a6e22e } /* Name.Exception */ 33 | .highlight .nf { color: #a6e22e } /* Name.Function */ 34 | .highlight .nl { color: #f8f8f2 } /* Name.Label */ 35 | .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 36 | .highlight .nx { color: #a6e22e } /* Name.Other */ 37 | .highlight .py { color: #f8f8f2 } /* Name.Property */ 38 | .highlight .nt { color: #f92672 } /* Name.Tag */ 39 | .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 40 | .highlight .ow { color: #f92672 } /* Operator.Word */ 41 | .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 42 | .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 43 | .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 48 | .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 50 | .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 51 | .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 54 | .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 56 | .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 62 | 63 | .highlight .gh { } /* Generic Heading & Diff Header */ 64 | .highlight .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */ 65 | .highlight .gd { color: #f92672; } /* Generic.Deleted & Diff Deleted */ 66 | .highlight .gi { color: #a6e22e; } /* Generic.Inserted & Diff Inserted */ 67 | -------------------------------------------------------------------------------- /src/TranslatorContainer.ts: -------------------------------------------------------------------------------- 1 | import { TranslateLogHandler } from "./TranslateLogHandler"; 2 | import { Translator } from "./Translator"; 3 | import { TranslatorConfig } from "./TranslatorConfig"; 4 | 5 | import { Injectable, Injector } from "@angular/core"; 6 | import { Observable , Observer } from "rxjs"; 7 | import { share } from "rxjs/operators"; 8 | 9 | @Injectable() 10 | export class TranslatorContainer { 11 | private _language: string = "en"; 12 | private languageChangedObservable: Observable; 13 | private languageChangedObserver: Observer; 14 | private translators: any = {}; 15 | 16 | constructor( 17 | private config: TranslatorConfig, 18 | private logHandler: TranslateLogHandler, 19 | private injector: Injector, 20 | ) { 21 | this._language = config.defaultLanguage; 22 | if (config.detectLanguage) { 23 | this.detectLanguage(); 24 | } 25 | this.languageChangedObservable = new Observable((observer: Observer) => { 26 | this.languageChangedObserver = observer; 27 | }).pipe(share()); 28 | } 29 | 30 | get languageChanged(): Observable { 31 | return this.languageChangedObservable; 32 | } 33 | 34 | get language(): string { 35 | return this._language; 36 | } 37 | 38 | set language(language: string) { 39 | let providedLanguage = this.config.providedLanguage(language, true); 40 | 41 | if (typeof providedLanguage === "string") { 42 | this._language = providedLanguage; 43 | 44 | // only when someone subscribes the observer get created 45 | if (this.languageChangedObserver) { 46 | this.languageChangedObserver.next(providedLanguage); 47 | } 48 | } else { 49 | throw new Error("Language " + language + " not provided"); 50 | } 51 | } 52 | 53 | public getTranslator(module: string) { 54 | if (!this.translators[module]) { 55 | this.translators[module] = new Translator(module, this.injector); 56 | } 57 | return this.translators[module]; 58 | } 59 | 60 | /** 61 | * Detects the preferred language. 62 | * 63 | * Returns false if the user prefers a language that is not provided or 64 | * true. 65 | * 66 | * @returns {boolean} 67 | */ 68 | private detectLanguage(): boolean { 69 | let providedLanguage: string | boolean; 70 | let i: number; 71 | 72 | const detected = (language): boolean => { 73 | this._language = language; 74 | this.logHandler.info("Language " + language + " got detected"); 75 | return true; 76 | }; 77 | 78 | if (this.config.preferExactMatches) { 79 | this.logHandler.debug( 80 | "Detecting language from " + JSON.stringify(this.config.navigatorLanguages) + " in strict mode", 81 | ); 82 | for (i = 0; i < this.config.navigatorLanguages.length; i++) { 83 | providedLanguage = this.config.providedLanguage(this.config.navigatorLanguages[i], true); 84 | if (typeof providedLanguage === "string") { 85 | this.logHandler.debug("Detected " + providedLanguage + " by " + this.config.navigatorLanguages[i]); 86 | return detected(providedLanguage); 87 | } else { 88 | this.logHandler.debug("Language " + this.config.navigatorLanguages[i] + " is not provided"); 89 | } 90 | } 91 | } 92 | 93 | this.logHandler.debug( 94 | "Detecting language from " + JSON.stringify(this.config.navigatorLanguages) + " in non-strict mode", 95 | ); 96 | for (i = 0; i < this.config.navigatorLanguages.length; i++) { 97 | providedLanguage = this.config.providedLanguage(this.config.navigatorLanguages[i]); 98 | if (typeof providedLanguage === "string") { 99 | this.logHandler.debug("Detected " + providedLanguage + " by " + this.config.navigatorLanguages[i]); 100 | return detected(providedLanguage); 101 | } else { 102 | this.logHandler.debug("Language " + this.config.navigatorLanguages[i] + " is not provided"); 103 | } 104 | } 105 | 106 | this.logHandler.debug("No language got detected - using default language"); 107 | return false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | padding:50px; 4 | font: 13px/1.3 "Open Sans", "Helvetica Neue", Helvetica, Tahoma, Arial, sans-serif; 5 | color:#555; 6 | font-weight:400; 7 | } 8 | 9 | h1, h2, h3, h4, h5, h6 { 10 | color:#333; 11 | margin:0 0 20px; 12 | } 13 | 14 | p, ul, ol, table, pre, dl { 15 | margin:0 0 20px; 16 | } 17 | 18 | h1, h2, h3 { 19 | line-height:1.1; 20 | } 21 | 22 | h1 { 23 | font-size:28px; 24 | } 25 | 26 | h2 { 27 | font-size: 20px; 28 | } 29 | 30 | h3 { 31 | font-size: 18px; 32 | } 33 | 34 | h4 { 35 | font-size: 16px; 36 | } 37 | 38 | h5, h6 { 39 | font-size:14px; 40 | } 41 | 42 | h6 { 43 | font-weight: 600; 44 | } 45 | 46 | a { 47 | color:#39c; 48 | text-decoration:none; 49 | } 50 | 51 | a:hover { 52 | color:#069; 53 | } 54 | 55 | a small { 56 | font-size:11px; 57 | color:#777; 58 | margin-top:-0.3em; 59 | display:block; 60 | } 61 | 62 | a:hover small { 63 | color:#777; 64 | } 65 | 66 | .wrapper { 67 | max-width:1124px; 68 | margin:0 auto; 69 | } 70 | 71 | blockquote { 72 | border-left:1px solid #e5e5e5; 73 | margin:0; 74 | padding:0 0 0 20px; 75 | font-style:italic; 76 | } 77 | 78 | code, pre { 79 | font-family: "Source Code Pro", "Monaco", "DejaVu Sans Mono", "Lucida Console", "Consolas", monospace; 80 | color:#333; 81 | font-size:12px; 82 | } 83 | 84 | code.highlighter-rouge { 85 | background-color: #e9e9e9; 86 | font-size: 13px; 87 | display: inline-block; 88 | padding: 0 3px; 89 | } 90 | 91 | pre { 92 | padding:8px 15px; 93 | background: #222222; 94 | border-radius:3px; 95 | overflow-x: auto; 96 | } 97 | 98 | table { 99 | width:100%; 100 | border-collapse:collapse; 101 | } 102 | 103 | th, td { 104 | text-align:left; 105 | padding:5px 10px; 106 | border-bottom:1px solid #e5e5e5; 107 | vertical-align:top; 108 | font-size:11px; 109 | } 110 | 111 | th { 112 | font-size:13px; 113 | } 114 | 115 | dt { 116 | color:#444; 117 | font-weight:700; 118 | } 119 | 120 | th { 121 | color:#444; 122 | } 123 | 124 | img { 125 | max-width:100%; 126 | } 127 | 128 | header { 129 | width:270px; 130 | float:left; 131 | position:fixed; 132 | -webkit-font-smoothing:subpixel-antialiased; 133 | } 134 | 135 | ul.row { 136 | list-style:none; 137 | height:40px; 138 | padding:0; 139 | background: #f4f4f4; 140 | border-radius:5px; 141 | border:1px solid #e0e0e0; 142 | width:270px; 143 | } 144 | 145 | ul.row li { 146 | width:89px; 147 | float:left; 148 | border-right:1px solid #e0e0e0; 149 | height:40px; 150 | } 151 | 152 | ul.row li:first-child a { 153 | border-radius:5px 0 0 5px; 154 | } 155 | 156 | ul.row li:last-child a { 157 | border-radius:0 5px 5px 0; 158 | } 159 | 160 | ul.row a { 161 | line-height:1; 162 | font-size:11px; 163 | color:#999; 164 | display:block; 165 | text-align:center; 166 | padding-top:6px; 167 | height:34px; 168 | } 169 | 170 | ul.row a:hover { 171 | color:#999; 172 | } 173 | 174 | ul.row a:active { 175 | background-color:#f0f0f0; 176 | } 177 | 178 | strong { 179 | color:#222; 180 | font-weight:700; 181 | } 182 | 183 | ul.row li + li + li { 184 | border-right:none; 185 | width:89px; 186 | } 187 | 188 | ul.row a strong { 189 | font-size:14px; 190 | display:block; 191 | color:#222; 192 | } 193 | 194 | section { 195 | width: calc(100% - 300px); 196 | float:right; 197 | padding-bottom:50px; 198 | } 199 | 200 | small { 201 | font-size:11px; 202 | } 203 | 204 | hr { 205 | border:0; 206 | background:#e5e5e5; 207 | height:3px; 208 | margin:0 0 5em; 209 | } 210 | 211 | footer { 212 | width:270px; 213 | float:left; 214 | position:fixed; 215 | bottom:50px; 216 | -webkit-font-smoothing:subpixel-antialiased; 217 | } 218 | 219 | menu, menu ul, menu li { 220 | list-style: none; 221 | margin: 0; 222 | padding: 0; 223 | } 224 | 225 | menu { 226 | margin: 30px 0; 227 | position: relative; 228 | } 229 | 230 | menu li { 231 | font-size: 16px; 232 | font-weight: 700; 233 | margin-left: 20px; 234 | } 235 | 236 | menu li.selected:before { 237 | content: "\227b"; 238 | display: inline-block; 239 | width: 20px; 240 | position: absolute; 241 | left: 0; 242 | } 243 | 244 | @media print, screen and (max-width: 1124px) { 245 | 246 | div.wrapper { 247 | width:auto; 248 | margin:0; 249 | } 250 | 251 | header, section, footer { 252 | float:none; 253 | position:static; 254 | width:auto; 255 | } 256 | 257 | header { 258 | padding-right:320px; 259 | } 260 | 261 | section { 262 | border:1px solid #e5e5e5; 263 | border-width:1px 0; 264 | padding:20px 0; 265 | margin:0 0 20px; 266 | } 267 | 268 | header a small { 269 | display:inline; 270 | } 271 | 272 | ul.row { 273 | position:absolute; 274 | right:50px; 275 | top:52px; 276 | } 277 | } 278 | 279 | @media print, screen and (max-width: 720px) { 280 | body { 281 | word-wrap:break-word; 282 | } 283 | 284 | header { 285 | padding:0; 286 | } 287 | 288 | ul.row, header p.view { 289 | position:static; 290 | } 291 | 292 | pre, code { 293 | word-wrap:normal; 294 | } 295 | } 296 | 297 | @media print, screen and (max-width: 480px) { 298 | body { 299 | padding:15px; 300 | } 301 | 302 | ul.row { 303 | width:99%; 304 | } 305 | 306 | ul.row li, ul.row li + li + li { 307 | width:33%; 308 | } 309 | } 310 | 311 | @media print { 312 | body { 313 | padding:0.4in; 314 | font-size:12pt; 315 | color:#444; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /docs/modules.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Translator Modules 4 | permalink: /modules.html 5 | --- 6 | # Translator Modules 7 | 8 | With version 2 we support modules. This has some improvements for you: 9 | 10 | - You can split your translation files and load them only when needed 11 | - You can use different loader for another module - it could be faster or compiled to the code 12 | - You can provide different modules in different languages 13 | 14 | ## Example use case 15 | 16 | ![modules use case](images/modules.svg) 17 | 18 | In this use case we defined that we want to use the `TranslationLoaderJson` and provide the languages `de, en, fr, ru, 19 | es, it`. The `loaderConfig` is the default config - we could also omit it. 20 | 21 | The menu has own translation files that are provided under `assets/i18n/menu/{{language}}.json`. It also is provided 22 | in all the languages. On the right side is the admin component it uses the translations under 23 | `assets/i18n/admin/{{language}}.json`. This module is only available in `de` and `en`. Inside this admin component 24 | we use a date picker that uses the translation module `calendar`. Instead of the `TranslationLoaderJson` we use a 25 | custom loader (`TranslationLoaderStatic` here) with a custom configuration. This module is again provided in every language. 26 | 27 | This could be the `app.module.ts` for this use case: 28 | 29 | ```ts 30 | import { Injectable, NgModule } from '@angular/core'; 31 | 32 | import { TranslatorModule, TranslationLoader } from 'angular-translator'; 33 | 34 | @Injectable() 35 | export class TranslationLoaderStatic extends TranslationLoader { 36 | private static translations: any = { 37 | calendar: { 38 | de: { 39 | 'month.0': 'Januar', 40 | 'month.1': 'Februar', 41 | '...': 'und so weiter' 42 | }, 43 | en: { 44 | 'month.0': 'January', 45 | 'month.1': 'February', 46 | '...': 'and so on' 47 | } 48 | } 49 | }; 50 | 51 | public load({language, module}: any): Promise { 52 | const translations = TranslationLoaderStatic.translations; 53 | if (translations[module] && translations[module][language]) { 54 | return Promise.resolve(translations[module][language]); 55 | } else { 56 | return Promise.reject('This does not exist here... WTF?'); 57 | } 58 | } 59 | } 60 | 61 | @NgModule({ 62 | imports: [ 63 | TranslatorModule.forRoot({ 64 | defaultLanguage: 'de', 65 | providedLanguages: ['de', 'en', 'fr', 'ru', 'es', 'it'], 66 | modules: { 67 | admin: { 68 | providedLanguages: ['de', 'en'] 69 | }, 70 | calendar: { 71 | loader: TranslationLoaderStatic 72 | } 73 | } 74 | }) 75 | ], 76 | providers: [ TranslationLoaderStatic ] 77 | }) 78 | export class AppModule {} 79 | ``` 80 | 81 | ## The configuration 82 | 83 | As you can see in the example we don't have to define every module. All configurations are inherited from the root 84 | configuration. Because the language detection get executed from the `TranslatorContainer` it does not make sense to 85 | change `detectLanguage` inside the modules. 86 | 87 | For more information about configuration check the [TranslatorConfig](TranslatorConfig.md) section. 88 | 89 | ## Use modules 90 | 91 | You can use modules explicitly in [TranslateComponent](TranslateComponent.md), [TranslatePipe](TranslatePipe.md) and 92 | in your services or components with [TranslatorContainer](TranslatorContainer.md). 93 | 94 | In templates: 95 | 96 | ```html 97 |

{% raw %}{{ 'month.0' | translate:{}: 'calendar' }}{% endraw %}

98 |

99 | ``` 100 | 101 | In services and components: 102 | 103 | ```ts 104 | import { Injectable } from '@angular/core'; 105 | 106 | import { Translator, TranslatorContainer } from 'angular-translator'; 107 | 108 | @Injectable() 109 | export class MyService { 110 | private calendarTranslator: Translator; 111 | 112 | constructor(private translatorContainer: TranslatorContainer) { 113 | this.calendarTranslator = translatorContainer.getTranslator('calendar'); 114 | } 115 | } 116 | ``` 117 | 118 | For more details have a look in the appropriate sections of this documentation. 119 | 120 | ### Overwrite the provider 121 | 122 | Thanks to the hierarchical injection system from angular we can overwrite the provider for `Translator` and therefore 123 | use another module for all components under this component (except the components that define another module). 124 | 125 | For our example use case we could have this three components: 126 | 127 | ```ts 128 | import { Component } from '@angular/core'; 129 | 130 | import { provideTranslator } from 'angular-translator'; 131 | 132 | @Component({ 133 | selector: 'main-menu', 134 | providers: [ 135 | provideTranslator('menu') 136 | ] 137 | }) 138 | export class MainMenuComponent {} 139 | 140 | @Component({ 141 | selector: 'admin', 142 | providers: [ 143 | provideTranslator('admin') 144 | ] 145 | }) 146 | export class AdminComponent {} 147 | 148 | @Component({ 149 | selector: '[date-picker]', 150 | providers: [ 151 | provideTranslator('calendar') 152 | ] 153 | }) 154 | export class DatePickerComponent {} 155 | ``` 156 | 157 | Then we can use them in the templates (this is just an example and might not work): 158 | 159 | ```html 160 | 161 | 162 | 163 | 164 | 165 |

{{ 'BE_CAREFUL' | translate }}

166 | 167 | 172 |
173 | ``` 174 | 175 | The `item.title` will get translation from `assets/i18n/menu/{{language}}.json#item.title`, `BE_CAREFUL` comes from 176 | `assets/i18n/admin/{{language}}.json#BE_CAREFUL` and `month.0` / `month.1` from `TranslationLoaderStatic`. 177 | -------------------------------------------------------------------------------- /tests/helper/promise-matcher.ts: -------------------------------------------------------------------------------- 1 | import {flushMicrotasks} from "@angular/core/testing"; 2 | 3 | /* tslint:disable */ 4 | export class PromiseMatcher { 5 | public static getInstance() { 6 | if (!PromiseMatcher._instance) { 7 | PromiseMatcher._instance = new PromiseMatcher(); 8 | } 9 | return PromiseMatcher._instance; 10 | } 11 | 12 | public static install() { 13 | PromiseMatcher.getInstance()._install(); 14 | } 15 | 16 | public static uninstall() { 17 | PromiseMatcher.getInstance()._uninstall(); 18 | } 19 | 20 | private static _instance: PromiseMatcher; 21 | 22 | private _originalPromise: any; 23 | private _global: any; 24 | 25 | constructor() { 26 | this._global = window || global; 27 | this._originalPromise = this._global.Promise; 28 | } 29 | 30 | private _install() { 31 | if (this._global.Promise === JasminePromise) { 32 | return; 33 | } 34 | JasminePromise.NativePromise = this._originalPromise; 35 | JasminePromise.initialize(); 36 | Object.defineProperty(this._global, 'Promise', { 37 | enumerable: false, 38 | configurable: false, 39 | writable: true, 40 | value: JasminePromise, 41 | }); 42 | 43 | let createCompareFn = function(util, customs, state: string, withArgs: boolean = false) { 44 | return function(promise, ...args) { 45 | if (!(promise instanceof JasminePromise)) { 46 | throw new Error("Promise is not a JasminePromise - did you run PromiseMatcher.install()?"); 47 | } 48 | return promise.verify(util, customs, state, withArgs ? args : null); 49 | }; 50 | }; 51 | 52 | jasmine.addMatchers({ 53 | toBeRejected(util, customs) { 54 | return { compare: createCompareFn(util, customs, "rejected") }; 55 | }, 56 | toBeRejectedWith(util, customs) { 57 | return { compare: createCompareFn(util, customs, "rejected", true) }; 58 | }, 59 | toBeResolved(util, customs) { 60 | return { compare: createCompareFn(util, customs, "resolved") }; 61 | }, 62 | toBeResolvedWith(util, customs) { 63 | return { compare: createCompareFn(util, customs, "resolved", true) }; 64 | }, 65 | }); 66 | } 67 | 68 | private _uninstall() { 69 | this._global.Promise = this._originalPromise; 70 | } 71 | } 72 | 73 | let i = 0; 74 | export class JasminePromise { 75 | public static NativePromise: any = global.Promise; 76 | 77 | public static reject(reason) { 78 | return new JasminePromise((resolve, reject) => reject(reason)); 79 | } 80 | 81 | public static resolve(...args) { 82 | return new JasminePromise((resolve) => resolve.apply(null, args)); 83 | } 84 | 85 | public static initialize() { 86 | for (let k in this.NativePromise) { 87 | if (k.indexOf("__zone_symbol") > -1 && this.NativePromise.hasOwnProperty(k)) { 88 | this[k] = this.NativePromise[k]; 89 | } 90 | } 91 | } 92 | 93 | public static flush() { 94 | try { 95 | flushMicrotasks(); 96 | } catch (e) {} 97 | } 98 | 99 | public state: string = "pending"; 100 | public args: any[]; 101 | public id: number; 102 | 103 | private _nativePromise: Promise; 104 | 105 | constructor(resolver: any) { 106 | this.id = i++; 107 | let resolve; 108 | let reject; 109 | this._nativePromise = new JasminePromise.NativePromise((_resolve, _reject) => { 110 | resolve = _resolve; 111 | reject = _reject; 112 | }); 113 | this._nativePromise.catch(() => {}); 114 | 115 | resolver( 116 | (...args: any[]) => { 117 | if (this.state !== "pending") { return; } 118 | this.state = "resolved"; 119 | this.args = args; 120 | resolve.apply(null, args); 121 | }, 122 | (...args: any[]) => { 123 | if (this.state !== "pending") { return; } 124 | this.state = "rejected"; 125 | this.args = args; 126 | reject.apply(null, args); 127 | } 128 | ); 129 | } 130 | 131 | public then(...args: any[]) { 132 | this._nativePromise.then.apply(this._nativePromise, args); 133 | } 134 | 135 | public catch(...args: any[]) { 136 | this._nativePromise.catch.apply(this._nativePromise, args); 137 | } 138 | 139 | public verify(util: any, customEqualityTesters: any, state: string, args?: any[]) { 140 | JasminePromise.flush(); 141 | 142 | let result: { pass: boolean, message: string } = { 143 | message: "", 144 | pass: false, 145 | }; 146 | 147 | result.pass = this.state === state; 148 | 149 | if (result.pass) { 150 | if (args) { 151 | result.pass = util.equals(this.args, args, customEqualityTesters); 152 | if (result.pass) { 153 | result.message = "Expected promise not to be " + 154 | state + " with " + 155 | JSON.stringify(args) + " but it was"; 156 | } else { 157 | result.message = "Expected promise to be " + 158 | state + " with " + JSON.stringify(args) + 159 | " but it was " + state + " with " + JSON.stringify(this.args); 160 | } 161 | } else { 162 | result.message = "Expected promise not to be " + state + " but it was"; 163 | } 164 | } else { 165 | result.message = "Expected promise to be " + state + " but it is " + this.state; 166 | } 167 | 168 | return result; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /docs/dynamize.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Dynamic Translations 4 | permalink: /dynamize.html 5 | --- 6 | # Make your translations dynamic 7 | 8 | On every way (Component, Pipe and Service) you can give the translation params to make it dynamic. These params 9 | can then be used in your translations. 10 | 11 | ## Example 12 | 13 | This is a simple example with a count to show how it works. 14 | 15 | Your translation file: 16 | 17 | ```json 18 | { "NEW_MESSAGES": "You have {% raw %}{{ count }}{% endraw %} new message{% raw %}{{ count == 1 ? '' : 's' }}{% endraw %}" } 19 | ``` 20 | 21 | In your component you can use it like this: 22 | 23 | ```ts 24 | translator.translate('NEW_MESSAGES', {count: 42}).then((translation) => this.translation = translation); 25 | ``` 26 | 27 | In your template are two ways to use it: 28 | 29 | ```html 30 |
pipe example
31 |

{% raw %}{{ 'NEW_MESSAGES' | translate : { count: 42 } }}{% endraw %}

32 | 33 |
component example
34 |

35 | ``` 36 | 37 | ## Loading other translations 38 | 39 | You can load other translations in your translation with double brackets. This translations can not use the params that 40 | are passed to the translation. To use a parameter you have to define which parameters goes to the sub translation by 41 | writing them behind a colon and comma separated. 42 | 43 | Other translations are loaded before the content of double braces got parsed. 44 | 45 | ### Passing parameters 46 | 47 | Parameters can be passed directly under the same name, with a different name and partially. To pass a variable with a 48 | different name you define a getter. To pass a variable with the same name you can leave the getter empty. The getter 49 | can contain dots which means that you refer to the `object.key` and pass only key from this object. 50 | 51 | ### Limitations 52 | - the referred translation key can only contain `[A-Za-z0-9_.-]` 53 | - the submitted params can only contain `[A-Za-z0-9_]` 54 | 55 | ### Example 56 | 57 | ```json 58 | { 59 | "HELLO": "Hello", 60 | "GREET": "[[ HELLO ]] {% raw %}{{name}}{% endraw %}", 61 | "USER_LOGGED_IN": "[[GREET:name]], your last login was on {% raw %}{{lastLogin}}{% endraw %}", 62 | "SALUTATION": "{% raw %}{{title ? title : (gender == 'w' ? 'Mrs.' : 'Mr.')}} {{firstName}} {{lastName}}{% endraw %}", 63 | "WELCOME": [ 64 | "Welcome [[ SALUTATION : ", 65 | "title=user.title, gender=user.gender, firstName=user.firstName, lastName=user.lastName", 66 | "]]" 67 | ] 68 | } 69 | ``` 70 | 71 | > In this example we use `string[]` as translation. See [TranslationLoaderJson](TranslationLoaderJson.md) for more 72 | > details. 73 | 74 | ## Logic in translations 75 | 76 | In general we can not recommend to use logic inside translations. But we know that sometimes it is much easier and of 77 | course language related. 78 | 79 | > We suggest that you only use logic for language related stuff like pluralization like we did in the example. 80 | 81 | ## Provide parameters 82 | 83 | Parameters have to be stored in an object. That is easy when you using javascript: 84 | 85 | ```ts 86 | this.user = { name:'Thomas', lastLogin: moment('2016-03-06 22:13:31') }; 87 | translator.translate( 88 | 'USER_LOGGED_IN', 89 | { 90 | name: this.user.name, 91 | lastLogin: this.user.lastLogin.fromNow() 92 | } 93 | ); 94 | ``` 95 | 96 | For [TranslateComponent](TranslateComponent.md) there is a second attribute `translateParams`. To pass variables 97 | you need to write in brackets: 98 | 99 | ```html 100 |

102 | ``` 103 | 104 | For [TranslatePipe](TranslatePipe.md) you pass the params as first parameter: 105 | 106 | ```html 107 |

{% raw %}{{ 'USER_LOGGED_IN' | translate: { name: user.name, lastLogin: user.lastLogin.fromNow() } }}{% endraw %}

108 | ``` 109 | 110 | To generate objects inside the view looks some bit like logic in templates. A more reasonable way will be to create 111 | the object inside the component and pass it to pipe or params: 112 | 113 | ```html 114 |

115 |

{% raw %}{{ 'USER_LOGGED_IN' | translate: user }}{% endraw %}

116 | ``` 117 | 118 | This works only if you define lastLogin in user as string, or use a method of moment in your translation. 119 | 120 | ```ts 121 | this.user = { name: 'Thomas', lastLogin: moment('2016-03-06 22:13.31').format('LLL') } 122 | ``` 123 | 124 | ```json 125 | { 126 | "USER_LOGGED_IN": "[[ GREET : name ]], your last login was on {% raw %}{{ lastLogin.format('LLL') }}{% endraw %}" 127 | } 128 | ``` 129 | 130 | ## Performance 131 | 132 | To pass parameters to the pipe or the component have a slightly performance drawback because the objects needs to be 133 | checked every time if they changed. 134 | 135 | To have it under your control we suggest to use `Translator.translate()` or `Translator.instant()`. You can 136 | then subscribe to `Translator.languageChanged` to change your translation when the language got changed. Also you 137 | will know when your values have changed. 138 | 139 | ## Use pipes in translations 140 | 141 | By default you can use the pipes `CurrencyPipe`, `DatePipe`, `DecimalPipe`, `JsonPipe`, `LowerCasePipe`, `PercentPipe`, 142 | `SlicePipe`, `TitleCasePipe` and `UpperCasePipe`. 143 | 144 | Custom pipes can get tricky because we can't get the annotations. And therefore we don't know the name. There are two 145 | workarounds. First (recommended): you can add a `public static pipeName` property to your pipe. Second: you provide a 146 | pipe map to configuration. 147 | 148 | Anyway you need to pass them to the configuration. Here we use both methods to ge the custom pipe working: 149 | 150 | ```ts 151 | // the pipe 152 | @Pipe({ 153 | name: 'random', 154 | pure: true 155 | }) 156 | export class RandomPipe implements PipeTransform { 157 | public static pipeName = 'random'; 158 | 159 | transform(type: string, ...args: any[]): any { 160 | if (!args[0] || !args[0][value]) { 161 | return 'unknown'; 162 | } 163 | 164 | return args[0][type][Math.floor(Math.random() * args[0][value].length)]; 165 | } 166 | } 167 | 168 | @NgModule({ 169 | declarations: [ 170 | AppComponent, 171 | ], 172 | imports: [ 173 | BrowserModule, 174 | TranslatorModule.forRoot({ 175 | pipes: [ RandomPipe ], 176 | pipeMap: { random: RandomPipe } 177 | }) 178 | ], 179 | bootstrap: [AppComponent] 180 | }) 181 | export class AppModule {} 182 | ``` 183 | 184 | Then you can also use this pipe: 185 | 186 | {% raw %} 187 | ```json 188 | { 189 | "FUN": [ 190 | "{{ type | random: { joke: [", 191 | "'What\\'s the difference between snowmen and snowladies? Snowballs',", 192 | "'How do you make holy water? You boil the hell out of it.',", 193 | "'I say no to alcohol, it just doesn\\'t listen.',", 194 | "] } }}" 195 | ] 196 | } 197 | ``` 198 | {% endraw %} 199 | 200 | For the default pipes you can get more information in 201 | [the official API reference](https://angular.io/docs/ts/latest/api/#!?query=pipe) 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Translator 2 | 3 | [![Build Status](https://travis-ci.org/tflori/angular-translator.svg?branch=master)](https://travis-ci.org/tflori/angular-translator) 4 | [![Coverage Status](https://coveralls.io/repos/github/tflori/angular-translator/badge.svg?branch=master)](https://coveralls.io/github/tflori/angular-translator?branch=master) 5 | [![npm version](https://badge.fury.io/js/angular-translator.svg)](https://badge.fury.io/js/angular-translator) 6 | 7 | `angular-translator` is a simple translation service for angular applications. It should support all necessary 8 | features for translation. Like interpolation, references to other translations, modules and loaders. 9 | 10 | ## Features 11 | 12 | ### Interpolation 13 | 14 | It supports interpolation so you can: 15 | * output variables in your translations 16 | * calculate in your translations 17 | * pluralize in your translations 18 | * execute functions in your translations 19 | 20 | ```json 21 | { 22 | "HELLO": "Hello {{ name }}!", 23 | "ANSWER": "The answer is {{ 7 * 6 }}", 24 | "MESSAGES": "You have {{ count }} new message{{ count != 1 ? 's' : '' }}", 25 | "LAST_LOGIN": "Your last login was on {{ lastLogin.format('MM/DD/YYYY') }}" 26 | } 27 | ``` 28 | 29 | [* dynamic translations](https://tflori.github.io/angular-translator/dynamize.html) 30 | 31 | ### Refer to other translations 32 | 33 | By referring to other translations you can make it easy to have everywhere the same text without copy and paste. 34 | 35 | ```json 36 | { 37 | "GREETING": "Hello {{ name }}!", 38 | "REGISTERED": "[[ GREETING : name ]] Thanks for registering at this service.", 39 | "LOGIN_CONFIRM": "[[ GREETING : name ]] Your last login was on {{ lastLogin.format('L') }}." 40 | } 41 | ``` 42 | 43 | [* dynamic translations](https://tflori.github.io/angular-translator/dynamize.html) 44 | 45 | ### Use pipes in translations 46 | 47 | Pure pipes can be used inside translations. This makes formatting easier and localized. 48 | 49 | ```json 50 | { 51 | "DISCOUNT": "Save {{ original - price | currency:'USD':true }} when you order now!" 52 | } 53 | ``` 54 | 55 | [* dynamic translations](https://tflori.github.io/angular-translator/dynamize.html) 56 | 57 | ### Modules 58 | 59 | Your translations can be divided to multiple modules. Each module can have a different configuration. This way you have 60 | more control over the size of translation files and are able to provide some modules in more or less languages. 61 | 62 | [* Modules](https://tflori.github.io/angular-translator/modules.html) 63 | 64 | ### Different loaders 65 | 66 | This module supports different loaders. It is shipped with a basic JSON loader (next paragraph). You can create own 67 | and static loaders. It is also possible to use different loader strategies for each module. 68 | 69 | [* TranslationLoader](https://tflori.github.io/angular-translator/TranslationLoader.html) 70 | 71 | #### JSON loader 72 | 73 | It is a basic loader that loads the translation for a specific language and module from your JSON file. A translation 74 | can be an array to allow multi line translations (to make the files readable and better structured). 75 | 76 | [* TranslationLoaderJson](https://tflori.github.io/angular-translator/TranslationLoaderJson.html) 77 | 78 | ## How to use 79 | 80 | Simple basic usage: 81 | 82 | ```ts 83 | import { Component } from "angular2/core"; 84 | import { Translator } from "angular-translator"; 85 | 86 | @Component({ 87 | selector: "my-app", 88 | template: "{{ TEXT | translate }} is the same as " 89 | }) 90 | export class AppComponent { 91 | constructor(translator: Translator) { 92 | translator.translate("TEXT").then( 93 | (translation) => console.log(translation) 94 | ); 95 | } 96 | } 97 | ``` 98 | 99 | To learn more have a look at [the documentation](https://tflori.github.io/angular-translator/). 100 | 101 | ## How to upgrade from angular2-translator 102 | 103 | ### 1. Upgrade the package 104 | 105 | Remove angular2-translator and install angular-translator. 106 | 107 | ```bash 108 | npm remove angular2-translator --save 109 | npm install angular-translator --save 110 | ``` 111 | 112 | ### 2. Update your setup 113 | 114 | Angular translator now gives a simple-to-use static method for setup. This function also creates all required providers. 115 | The usage is as follows. 116 | 117 | ```ts 118 | import { BrowserModule } from '@angular/platform-browser'; 119 | import { NgModule } from '@angular/core'; 120 | import { FormsModule } from '@angular/forms'; 121 | import { HttpModule } from '@angular/http'; 122 | 123 | import { TranslatorModule } from 'angular-translator'; 124 | 125 | import { AppComponent } from './app.component'; 126 | 127 | @NgModule({ 128 | declarations: [ 129 | AppComponent 130 | ], 131 | imports: [ 132 | BrowserModule, 133 | FormsModule, 134 | HttpModule, 135 | TranslatorModule.forRoot({ 136 | providedLanguages: ['de', 'en', 'ru'], 137 | defaultLanguage: 'de' 138 | }) 139 | ], 140 | providers: [], 141 | bootstrap: [AppComponent] 142 | }) 143 | export class AppModule { } 144 | ``` 145 | 146 | ### 3. Change the implementation from TranslateService to Translator 147 | 148 | The `TranslateService` has been renamed to `Translator`. It has the same methods and can therefore be exchanged: 149 | 150 | ```ts 151 | import { Component } from '@angular/core'; 152 | 153 | import { TranslateService } from 'angular2-translator'; // before 154 | import { Translator } from 'angular-translator'; // now 155 | 156 | @Component() 157 | export class ComponentBefore { 158 | constructor(translateService: TranslateService) { 159 | translateService.translate('TEXT').then((translation) => this.text = translation); 160 | } 161 | } 162 | 163 | @Component() 164 | export class ComponentNow { 165 | constructor(translator: Translator) { 166 | translator.translate('TEXT').then((translation) => this.text = translation); 167 | } 168 | } 169 | ``` 170 | 171 | > You can do this by search and replace on your own risk. 172 | 173 | ### 4. Change the implementation for changing the language 174 | 175 | The `Translator` has a public property `language` and you can use it as before with `TranslateService`. There is a new 176 | service called `TranslatorContainer` that holds all `Translator`s for different modules. When you want to change the 177 | language for every module you may want to change `TranslatorContainer.language` instead. The change will be forwarded to 178 | every `Translator`. 179 | 180 | ### 5. Other questions 181 | 182 | > I used the `languageChanged` observable to update translations inside services and components. Do I need to change 183 | here something? 184 | 185 | No, the `Translator` has the same observable that should be used now. 186 | 187 | > My configuration seems to be ignored after upgrade. 188 | 189 | May be you copied your previous config. The parameters have changed: defaultLang - defaultLanguage, providedLangs - 190 | providedLanguages, detectLanguageOnStart - detectLanguage. 191 | 192 | ## How to install 193 | 194 | ### Get the package 195 | 196 | First you need to install the package. The easiest way is to install it via npm: 197 | 198 | ```bash 199 | npm install --save angular-translator 200 | ``` 201 | 202 | ## Setup angular module 203 | 204 | You have to set up each `NgModule` where you want to use the `TranslatorModule` and may be configure it: 205 | 206 | ```ts 207 | import { BrowserModule } from '@angular/platform-browser'; 208 | import { NgModule } from '@angular/core'; 209 | import { TranslatorModule } from "angular-translator"; 210 | 211 | import { AppComponent } from './app.component'; 212 | 213 | export function translateConfigFactory() { 214 | return new TranslateConfig(); 215 | } 216 | 217 | @NgModule({ 218 | declarations: [ 219 | AppComponent 220 | ], 221 | imports: [ 222 | BrowserModule, 223 | TranslatorModule.forRoot({ 224 | defaultLanguage: "de", 225 | providedLanguages: [ "de", "en" ], 226 | detectLanguage: false 227 | }), 228 | ], 229 | bootstrap: [AppComponent] 230 | }) 231 | export class AppModule { } 232 | ``` 233 | 234 | ### Using SystemJs 235 | 236 | When you are using SystemJs you need to configure where to load angular-translator: 237 | 238 | ```js 239 | System.config({ 240 | map: { 241 | 'angular-translator': 'npm:angular-translator/bundles/angular-translator.js' 242 | } 243 | }); 244 | ``` 245 | 246 | ### Manually 247 | 248 | You also can clone the repository and symlink the project folder or what ever: 249 | 250 | ```bash 251 | git clone https://github.com/tflori/angular-translator.git 252 | ln -s angular-translator MyApp/libs/angular-translator 253 | ``` 254 | 255 | > You should know what you do and don't follow this guide for installation. 256 | 257 | ## Demo 258 | 259 | [This project](https://github.com/tflori/angular-translator-demo) demonstrates how to use angular-translator. The 260 | production version is distributed [here](https://angular-translator-demo.my-first-domain.de/). 261 | -------------------------------------------------------------------------------- /src/TranslatorConfig.ts: -------------------------------------------------------------------------------- 1 | import { TranslateLogHandler } from "./TranslateLogHandler"; 2 | import { TranslationLoader } from "./TranslationLoader"; 3 | import { TranslationLoaderJson } from "./TranslationLoader/Json"; 4 | 5 | import { 6 | CurrencyPipe, 7 | DatePipe, 8 | DecimalPipe, 9 | JsonPipe, 10 | LowerCasePipe, 11 | PercentPipe, 12 | SlicePipe, 13 | TitleCasePipe, 14 | UpperCasePipe, 15 | } from "@angular/common"; 16 | import { PipeTransform, Type } from "@angular/core"; 17 | 18 | export const COMMON_PURE_PIPES: Array> = [ 19 | CurrencyPipe, 20 | DatePipe, 21 | DecimalPipe, 22 | JsonPipe, 23 | LowerCasePipe, 24 | PercentPipe, 25 | SlicePipe, 26 | TitleCasePipe, 27 | UpperCasePipe, 28 | ]; 29 | 30 | export const COMMON_PURE_PIPES_MAP: { [key: string]: Type } = { 31 | currency: CurrencyPipe, 32 | date: DatePipe, 33 | number: DecimalPipe, 34 | json: JsonPipe, 35 | lowercase: LowerCasePipe, 36 | percent: PercentPipe, 37 | slice: SlicePipe, 38 | titlecase: TitleCasePipe, 39 | uppercase: UpperCasePipe, 40 | }; 41 | 42 | export class TranslatorConfig { 43 | public static navigator: any = window && window.navigator ? window.navigator : {}; 44 | 45 | private static isoRegEx = /^([A-Za-z]{2})(?:[.\-_\/]?([A-Za-z]{2}))?$/; 46 | 47 | /** 48 | * Normalize a language 49 | * 50 | * @param {string} languageString 51 | * @returns {string} 52 | */ 53 | private static normalizeLanguage(languageString: string): string { 54 | if (!languageString.match(TranslatorConfig.isoRegEx)) { 55 | return languageString; 56 | } 57 | return languageString.replace( 58 | TranslatorConfig.isoRegEx, 59 | (substring: string, language: string, country: string = "") => { 60 | language = language.toLowerCase(); 61 | country = country.toUpperCase(); 62 | return country ? language + "-" + country : language; 63 | }, 64 | ); 65 | } 66 | 67 | private options: { [key: string]: any } = { 68 | defaultLanguage: "en", 69 | providedLanguages: ["en"], 70 | detectLanguage: true, 71 | preferExactMatches: false, 72 | navigatorLanguages: ["en"], 73 | loader: TranslationLoaderJson, 74 | pipes: Object.keys(COMMON_PURE_PIPES_MAP).map((key) => COMMON_PURE_PIPES_MAP[key]), 75 | pipeMap: (() => { 76 | let pipes = {}; 77 | for (let pipeName in COMMON_PURE_PIPES_MAP) { 78 | if (COMMON_PURE_PIPES_MAP.hasOwnProperty(pipeName)) { 79 | pipes[pipeName] = COMMON_PURE_PIPES_MAP[pipeName]; 80 | } 81 | } 82 | return pipes; 83 | })(), 84 | }; 85 | 86 | private moduleName: string; 87 | 88 | private pipeMap: { [key: string]: Type }; 89 | 90 | constructor( 91 | private logHandler: TranslateLogHandler, 92 | options?: any, 93 | module?: string, 94 | ) { 95 | this.options.navigatorLanguages = ((): string[] => { 96 | let navigator: any = TranslatorConfig.navigator; 97 | 98 | if (navigator.languages instanceof Array) { 99 | return Array.prototype.slice.call(navigator.languages); 100 | } else { 101 | return [ 102 | navigator.languages || 103 | navigator.language || 104 | navigator.browserLanguage || 105 | navigator.userLanguage, 106 | ].filter((v) => { 107 | return typeof v === "string"; 108 | }); 109 | } 110 | })(); 111 | 112 | this.setOptions(options); 113 | this.moduleName = module; 114 | } 115 | 116 | get defaultLanguage(): string { 117 | return this.options.defaultLanguage; 118 | } 119 | 120 | get providedLanguages(): string[] { 121 | return this.options.providedLanguages; 122 | } 123 | 124 | get loader(): Type { 125 | return this.options.loader; 126 | } 127 | 128 | get loaderOptions(): any { 129 | return this.options.loaderOptions || {}; 130 | } 131 | 132 | get detectLanguage(): boolean { 133 | return this.options.detectLanguage; 134 | } 135 | 136 | get preferExactMatches(): boolean { 137 | return this.options.preferExactMatches; 138 | } 139 | 140 | get navigatorLanguages(): string[] { 141 | return this.options.navigatorLanguages; 142 | } 143 | 144 | get pipes(): { [key: string]: Type } { 145 | if (!this.pipeMap) { 146 | this.pipeMap = this.options.pipeMap; 147 | const mappedPipes = Object.keys(this.pipeMap).map((key) => this.pipeMap[key]); 148 | const unmappedPipes = this.options.pipes.filter((pipe) => mappedPipes.indexOf(pipe) === -1); 149 | while (unmappedPipes.length) { 150 | let pipe = unmappedPipes.shift(); 151 | if (pipe.pipeName) { 152 | this.pipeMap[pipe.pipeName] = pipe; 153 | } else { 154 | this.logHandler.error("Pipe name for " + pipe.name + " can not be resolved"); 155 | } 156 | } 157 | } 158 | 159 | return this.pipeMap; 160 | } 161 | 162 | /** 163 | * Overwrite the options. 164 | * 165 | * @param {any} options 166 | */ 167 | public setOptions(options: { [key: string]: any }): void { 168 | for (let key in options) { 169 | if (!options.hasOwnProperty(key)) { 170 | continue; 171 | } 172 | 173 | if (key === "pipes") { 174 | this.options.pipes.push(...options.pipes.filter((pipe) => { 175 | return this.options.pipes.indexOf(pipe) === -1; 176 | })); 177 | } else if (key === "pipeMap") { 178 | for (let pipeName in options.pipeMap) { 179 | if (options.pipeMap.hasOwnProperty(pipeName)) { 180 | this.options.pipeMap[pipeName] = options.pipeMap[pipeName]; 181 | } 182 | } 183 | } else { 184 | this.options[key] = options[key]; 185 | } 186 | } 187 | 188 | if (this.options.providedLanguages.indexOf(this.options.defaultLanguage) === -1) { 189 | this.options.defaultLanguage = this.options.providedLanguages[0]; 190 | } 191 | } 192 | 193 | /** 194 | * Checks if given language "language" is provided and returns the internal name. 195 | * 196 | * The checks running on normalized strings matching this pattern: /[a-z]{2}(-[A-Z]{2})?/ 197 | * Transformation is done with this pattern: /^([A-Za-z]{2})([\.\-_\/]?([A-Za-z]{2}))?/ 198 | * 199 | * If strict is false it checks country independent. 200 | * 201 | * @param {string} language 202 | * @param {boolean?} strict 203 | * @returns {string|boolean} 204 | */ 205 | public providedLanguage(language: string, strict: boolean = false): string | boolean { 206 | let providedLanguagesNormalized = this.providedLanguages.map(TranslatorConfig.normalizeLanguage); 207 | language = TranslatorConfig.normalizeLanguage(language); 208 | 209 | let p: number = providedLanguagesNormalized.indexOf(language); 210 | if (p > -1) { 211 | return this.providedLanguages[p]; 212 | } else if (!strict && language.match(TranslatorConfig.isoRegEx)) { 213 | language = language.substr(0, 2); 214 | p = providedLanguagesNormalized.indexOf(language); 215 | if (p > -1) { 216 | return this.providedLanguages[p]; 217 | } else { 218 | p = providedLanguagesNormalized 219 | .map((l) => { 220 | return l.match(TranslatorConfig.isoRegEx) ? l.substr(0, 2) : l; 221 | }) 222 | .indexOf(language); 223 | if (p > -1) { 224 | return this.providedLanguages[p]; 225 | } 226 | } 227 | } 228 | 229 | return false; 230 | } 231 | 232 | /** 233 | * Get the configuration for module. 234 | * 235 | * @param {string} module 236 | * @returns {TranslatorConfig} 237 | */ 238 | public module(module: string) { 239 | if (this.moduleName) { 240 | throw new Error("Module configs can not be stacked"); 241 | } 242 | 243 | let moduleConfig = new TranslatorConfig(this.logHandler, this.options, module); 244 | 245 | if (this.options.modules && this.options.modules[module]) { 246 | moduleConfig.setOptions(this.options.modules[module]); 247 | } 248 | 249 | return moduleConfig; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /docs/Translator.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: TranslateService 4 | permalink: /Translator.html 5 | --- 6 | # Translator 7 | 8 | The `Translator` holds the core functionality for this module. It is not only for translation also it provides 9 | functions for control structures. For example: when you want to know if the language got loaded or changed you need 10 | this class. 11 | 12 | Translations can use params for dynamization (see [Dynamize translations with params](docs/dynamize.md) for more info). 13 | 14 | ## The Public Properties 15 | 16 | | Name | Type | Access level | Description | 17 | |-----------------|---------------------|--------------|-------------| 18 | | language | string | read/write | The setter for this property is checking if the language is provided or not. If the language is not provided it does not change. | 19 | | languageChanged | Observable | read | The observer fires next when the language got changed. If you use translator manually (not the component or the pipe) you may want to update your translations on this event. | 20 | | module | string | read | The name of the module for this translator. | 21 | 22 | ## The Public Methods 23 | 24 | ### Synchronous Methods 25 | 26 | These methods allow you to use the translations without passing a callback. The drawback is of course that they only 27 | work when the translations got loaded. Often you need to wait till the translation table for the language got loaded. 28 | 29 | #### instant 30 | 31 | ```ts 32 | public instant(key: string, params?: any, language?: string): string 33 | ``` 34 | 35 | Translates the `key` into `language` synchronously using `params` for interpolation. When the language is not loaded or 36 | the translation table does not have `key` it returns `key` - so make sure that the translation table is loaded 37 | before (e. g. by using [`waitForTranslation()`](#prerequisite-waitfortranslation)): 38 | 39 | ```ts 40 | expect(translator.instant('ISSUE_STATUS_IN_PROGRESS')).toEqual('in progress'); 41 | ``` 42 | 43 | This method is used from every other method that follows (`instantArray`, `search`, `translate`, `translateArray`, 44 | `translateSearch`, `observe`, `observeArray` and `observeSearch`) and holds the basic functionality for translations. 45 | 46 | > **Please note** that the signature changed in version 2.3 and the usage with array of keys is deprecated now. For 47 | > backward compatibility it is still supported but will be removed in version 3. Use `instantArray()` instead. 48 | 49 | #### instantArray 50 | 51 | ```ts 52 | public instantArray(keys: string[], params?: any, language?: string): string[] 53 | ``` 54 | 55 | Translates `keys` into `language` synchronously using `params` for interpolation. Internally it is using instant for 56 | each key - so it returns the keys when the language is not loaded. The translations are returned in strictly the same 57 | order as the parameter keys. 58 | 59 | ```ts 60 | expect(translator.instantArray(['KEY_1', 'KEY_2'])) 61 | .toEqual(['translation for KEY_1', 'translation for KEY_2']) 62 | ``` 63 | 64 | > This method got implemented to have a more strict interface - this method accepts only array of strings and returns 65 | > an array of strings. 66 | 67 | #### search 68 | 69 | ```ts 70 | public search(pattern: string, params?: any, language?: string): object 71 | ``` 72 | 73 | Searches synchronously for translations that matches `pattern` and returns an object with translations for each match 74 | in `language` using `params` for interpolation. It removes common text from pattern from keys in the object: 75 | 76 | ```ts 77 | expect(translator.search('MONTH_*')).toEqual({ 78 | JAN: "January", 79 | FEB: "Februrary", 80 | MAR: "March", 81 | APR: "April", 82 | MAY: "May", 83 | "...": "and so on", 84 | }); 85 | ``` 86 | 87 | #### Prerequisite waitForTranslation 88 | 89 | To make synchronous methods work you need to make sure the translation tables got loaded. For this propose we provide 90 | the following method: 91 | 92 | ```ts 93 | public waitForTranslation(language?: string): Promise 94 | ``` 95 | 96 | Waits for language to be loaded. If language is not given it loads the current language. Returns a promise that gets 97 | resolved once the language got loaded. 98 | 99 | This method is not synchronous and when you are otherwise sure that the translations are loaded you can skip these 100 | method. 101 | 102 | ```ts 103 | translator.waitForTranslation().then(() => { 104 | showButton(translator.instant('BUTTON_TEXT'), actionCallback); 105 | }); 106 | 107 | function actionCallback() { 108 | // we know the translations are loaded here 109 | showToast(translator.instant('SUCCESS_MESSAGE')); 110 | } 111 | ``` 112 | 113 | ### Thenable Methods 114 | 115 | These methods return a `Promise` that got resolved with the translation(s) once the translation table got loaded. 116 | Please note that the `Promise` gets resolved with the requested language (or current language at the time it was 117 | called) and can not be used again when the language changed. 118 | 119 | #### translate 120 | 121 | ```ts 122 | public translate(key: string, params?: any, language?: string): Promise 123 | ``` 124 | 125 | Translate `key` into `language` asynchronously using `params` for interpolation. If language is not given it uses the current language. 126 | 127 | The promise always gets resolved. Even if the loader rejects and especially when the translation does not exist. In this 128 | case the promise get resolved with `key`. 129 | 130 | ```ts 131 | translator.translate('STATUS_OPEN') .then((translation) => this.translations['open'] = translation); 132 | translator.translate('STATUS_CLOSED').then((translation) => this.translations['closed'] = translation); 133 | ``` 134 | 135 | > **Please note** that the signature changed in version 2.3 and the usage with array of keys is deprecated now. For 136 | > backward compatibility it is still supported but will be removed in version 3. Use `translateArray()` instead. 137 | 138 | #### translateArray 139 | 140 | ```ts 141 | public translateArray(keys: string[], params?: any, language?: string): Promise 142 | ``` 143 | 144 | Like `translate` but using `instantArray` for translating an array of `keys`. 145 | 146 | #### translateSearch 147 | 148 | ```ts 149 | public translateSearch(pattern: string, params?: any, language?: string): Promise 150 | ``` 151 | 152 | Like `translate` but using `search` to search for translations matching the given `pattern`. 153 | 154 | ### Observable Methods 155 | 156 | These methods return an `Observable` that provides the translation(s) in the the current selected language. When the 157 | language got changed they receive the new value after the translation table got loaded. 158 | 159 | #### observe 160 | 161 | ```ts 162 | public observe(key: string, params?: any): Observable 163 | ``` 164 | 165 | Translate `key` into the current language using `params` for interpolation. Once the language got changed and the 166 | translation table got loaded the observer receives the translation for the newly selected language. 167 | 168 | Like translate the observable always get's a new value - even if the key the loader rejects to load the language and 169 | especially when the `key` does not exist in translation table. 170 | 171 | ```ts 172 | translator.observe('STATUS_OPEN') .subscribe((translation) => this.translations['open'] = translation); 173 | translator.observe('STATUS_CLOSED').subscribe((translation) => this.translations['closed'] = translation); 174 | ``` 175 | 176 | > **Please note** that the signature changed in version 2.3 and the usage with array of keys is deprecated now. For 177 | > backward compatibility it is still supported but will be removed in version 3. Use `observeArray()` instead. 178 | 179 | #### observerArray 180 | 181 | ```ts 182 | public observeArray(keys: string[], params?: any): Observable 183 | ``` 184 | 185 | Like `observe` but using `instantArray` for translating an array of `keys`. 186 | 187 | #### observeSearch 188 | 189 | ```ts 190 | public observeSearch(pattern: string, params?: any): Observable 191 | ``` 192 | 193 | Like `observe` but using `search` to search for translations matching the given `pattern`. 194 | 195 | This has an interesting use case when you need multiple translations in a component. For example with statuses: 196 | 197 | ```ts 198 | // translation table: 199 | translations = { 200 | STATUS_OPEN: 'open', 201 | STATUS_TO_DO: 'to do', 202 | STATUS_IN_PROGRESS: 'in progress', 203 | STATUS_RESOLVED: 'resolved', 204 | STATUS_CLOSED: 'closed', 205 | }; 206 | 207 | class Issue { 208 | private statusTranslations: { [key: string]: string }; 209 | 210 | constructor(private translator: Translator) { 211 | translator.observeSearch('STATUS_*').subscribe((statusTranslations) => { 212 | this.statusTranslations = statusTranslations; 213 | }); 214 | } 215 | 216 | get status(): string { 217 | // assume statusKey is one of ['OPEN', 'TO_DO', 'IN_PROGRESS', 'RESOLVED', 'CLOSED'] 218 | return this.statusTranslations[this.statusKey]; 219 | } 220 | } 221 | 222 | ``` 223 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Introduction 4 | permalink: / 5 | --- 6 | # Angular Translator 7 | 8 | `angular-translator` is a simple translation service for angular applications. It should support all necessary 9 | features for translation. Like interpolation, references to other translations, modules and loaders. 10 | 11 | ## Demo 12 | 13 | [This project](https://github.com/tflori/angular-translator-demo) demonstrates how to use angular-translator. The 14 | production version is distributed [here](https://angular-translator-demo.my-first-domain.de/). 15 | 16 | ## Features 17 | 18 | ### Interpolation 19 | 20 | It supports interpolation so you can: 21 | * output variables in your translations 22 | * calculate in your translations 23 | * pluralize in your translations 24 | * execute functions in your translations 25 | 26 | {% raw %} 27 | ```json 28 | { 29 | "HELLO": "Hello {{ name }}!", 30 | "ANSWER": "The answer is {{ 7 * 6 }}", 31 | "MESSAGES": "You have {{ count }} new message{{ count != 1 ? 's' : '' }}", 32 | "LAST_LOGIN": "Your last login was on {{ lastLogin.format('MM/DD/YYYY') }}" 33 | } 34 | ``` 35 | {% endraw %} 36 | 37 | ### Refer to other translations 38 | 39 | By referring to other translations you can make it easy to have everywhere the same text without copy and paste. 40 | 41 | {% raw %} 42 | ```json 43 | { 44 | "GREETING": "Hello {{ name }}!", 45 | "REGISTERED": "[[ GREETING : name ]] Thanks for registering at this service.", 46 | "LOGIN_CONFIRM": "[[ GREETING : name ]] Your last login was on {{ lastLogin.format('L') }}." 47 | } 48 | ``` 49 | {% endraw %} 50 | 51 | ### Use pipes in translations 52 | 53 | Pure pipes can be used inside translations. This makes formatting easier and localized. 54 | 55 | {% raw %} 56 | ```json 57 | { 58 | "DISCOUNT": "Save {{ original - price | currency:'USD':true }} when you order now!" 59 | } 60 | ``` 61 | {% endraw %} 62 | 63 | ### Modules 64 | 65 | Your translations can be divided to multiple modules. Each module can have a different configuration. This way you have 66 | more control over the size of translation files and are able to provide some modules in more or less languages. 67 | 68 | ### Different loaders 69 | 70 | This module supports different loaders. It is shipped with a basic JSON loader (next paragraph). You can create own 71 | and static loaders. It is also possible to use different loader strategies for each module. 72 | 73 | #### JSON loader 74 | 75 | It is a basic loader that loads the translation for a specific language and module from your JSON file. A translation 76 | can be an array to allow multi line translations (to make the files readable and better structured). 77 | 78 | ## How to use 79 | 80 | Simple basic usage: 81 | 82 | ```ts 83 | import { Component } from "angular2/core"; 84 | import { Translator } from "angular-translator"; 85 | 86 | @Component({ 87 | selector: "my-app", 88 | template: "{TEXT|translate} is the same as " 89 | }) 90 | export class AppComponent { 91 | constructor(translator: Translator) { 92 | translator.translate("TEXT").then( 93 | (translation) => console.log(translation) 94 | ); 95 | } 96 | } 97 | ``` 98 | 99 | ### Get the package 100 | 101 | First you need to install the package. The easiest way is to install it via npm: 102 | 103 | ```bash 104 | npm install --save angular-translator 105 | ``` 106 | 107 | ## Setup angular module 108 | 109 | You have to set up each `NgModule` where you want to use the `TranslatorModule` and may be configure it: 110 | 111 | ```ts 112 | import { BrowserModule } from '@angular/platform-browser'; 113 | import { NgModule } from '@angular/core'; 114 | import { TranslatorModule } from "angular-translator"; 115 | 116 | import { AppComponent } from './app.component'; 117 | 118 | export function translateConfigFactory() { 119 | return new TranslateConfig(); 120 | } 121 | 122 | @NgModule({ 123 | declarations: [ 124 | AppComponent 125 | ], 126 | imports: [ 127 | BrowserModule, 128 | TranslatorModule.forRoot({ 129 | defaultLanguage: "de", 130 | providedLanguages: [ "de", "en" ], 131 | detectLanguage: false 132 | }), 133 | ], 134 | bootstrap: [AppComponent] 135 | }) 136 | export class AppModule { } 137 | ``` 138 | 139 | ### Using SystemJs 140 | 141 | When you are using SystemJs you need to configure where to load angular-translator: 142 | 143 | ```js 144 | System.config({ 145 | map: { 146 | 'angular-translator': 'npm:angular-translator/bundles/angular-translator.js' 147 | } 148 | }); 149 | ``` 150 | 151 | ### Manually 152 | 153 | You also can clone the repository and symlink the project folder or what ever: 154 | 155 | ```bash 156 | git clone https://github.com/tflori/angular-translator.git 157 | ln -s angular-translator MyApp/libs/angular-translator 158 | ``` 159 | 160 | > You should know what you do and don't follow this guide for installation. 161 | 162 | ## How to upgrade from angular2-translator 163 | 164 | ### 1. Upgrade the package 165 | 166 | Remove angular2-translator and install angular-translator. 167 | 168 | ```bash 169 | npm remove angular2-translator --save 170 | npm install angular-translator --save 171 | ``` 172 | 173 | ### 2. Update your setup 174 | 175 | Angular translator now gives a simple-to-use static method for setup. This function also creates all required providers. 176 | The usage is as follows. 177 | 178 | ```ts 179 | import { BrowserModule } from '@angular/platform-browser'; 180 | import { NgModule } from '@angular/core'; 181 | import { FormsModule } from '@angular/forms'; 182 | import { HttpModule } from '@angular/http'; 183 | 184 | import { TranslatorModule } from 'angular-translator'; 185 | 186 | import { AppComponent } from './app.component'; 187 | 188 | @NgModule({ 189 | declarations: [ 190 | AppComponent 191 | ], 192 | imports: [ 193 | BrowserModule, 194 | FormsModule, 195 | HttpModule, 196 | TranslatorModule.forRoot({ 197 | providedLanguages: ['de', 'en', 'ru'], 198 | defaultLanguage: 'de' 199 | }) 200 | ], 201 | providers: [], 202 | bootstrap: [AppComponent] 203 | }) 204 | export class AppModule { } 205 | ``` 206 | 207 | ### 3. Change the implementation from TranslateService to Translator 208 | 209 | The `TranslateService` has been renamed to `Translator`. It has the same methods and can therefore be exchanged: 210 | 211 | ```ts 212 | import { Component } from '@angular/core'; 213 | 214 | import { TranslateService } from 'angular2-translator'; // before 215 | import { Translator } from 'angular-translator'; // now 216 | 217 | @Component() 218 | export class ComponentBefore { 219 | constructor(translateService: TranslateService) { 220 | translateService.translate('TEXT').then((translation) => this.text = translation); 221 | } 222 | } 223 | 224 | @Component() 225 | export class ComponentNow { 226 | constructor(translator: Translator) { 227 | translator.translate('TEXT').then((translation) => this.text = translation); 228 | } 229 | } 230 | ``` 231 | 232 | > You can do this by search and replace on your own risk. 233 | 234 | ### 4. Change the implementation for changing the language 235 | 236 | The `Translator` has a public property `language` and you can use it as before with `TranslateService`. There is a new 237 | service called `TranslatorContainer` that holds all `Translator`s for different modules. When you want to change the 238 | language for every module you may want to change `TranslatorContainer.language` instead. The change will be forwarded to 239 | every `Translator`. 240 | 241 | ### 5. Other questions 242 | 243 | > I used the `languageChanged` observable to update translations inside services and components. Do I need to change 244 | here something? 245 | 246 | No, the `Translator` has the same observable that should be used now. 247 | 248 | > My configuration seems to be ignored after upgrade. 249 | 250 | Maybe you copied your previous config. The parameters have changed: defaultLang - defaultLanguage, providedLangs - 251 | providedLanguages, detectLanguageOnStart - detectLanguage. 252 | 253 | ## The Main Classes 254 | 255 | **[TranslatorConfig](TranslatorConfig.md)** - 256 | The `TranslatorConfig` holds the configuration for this module. 257 | 258 | **[Translator](Translator.md)** - 259 | The `Translator` provides the core functionality for this module. You can translate, check if translations are loaded 260 | and subscribe to language changes with this class. 261 | 262 | **[TranslatorContainer](TranslatorContainer.md)** - 263 | The `TranslatorContainer` holds the `Translator` instances - one for each module one. 264 | 265 | **[TranslateComponent](TranslateComponent.md)** - 266 | This is the component for the selector `[translate]` 267 | 268 | **[TranslatePipe](TranslatePipe.md)** - 269 | The `TranslatePipe` is the easiest way for translation in templates. 270 | 271 | **[TranslationLoaderJson](TranslationLoaderJson.md)** - 272 | For now this is the only existing TranslateLoader. 273 | 274 | ## Further Readings 275 | 276 | You can **[make translations dynamic](dynamize.md)** by giving parameters that can be used inside the translations. 277 | 278 | **[Modules](modules.md)** allow you to split translation files and provide subsets in different languages. 279 | 280 | Overwrite **[TranslateLogHandler](TranslateLogHandler.md)** to get information about missing translations and other 281 | problems in your translations. 282 | 283 | Create your own **[TranslationLoader](TranslationLoader.md)** that fits your needs. 284 | -------------------------------------------------------------------------------- /tests/TranslationLoader/Json.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TranslationLoaderJson, 3 | } from "../../index"; 4 | 5 | import {PromiseMatcher} from "../helper/promise-matcher"; 6 | 7 | import { HttpClient } from "@angular/common/http"; 8 | import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; 9 | import {TestBed} from "@angular/core/testing"; 10 | 11 | describe("TranslationLoaderJson", () => { 12 | it("is defined", () => { 13 | expect(TranslationLoaderJson).toBeDefined(); 14 | }); 15 | 16 | describe("load", () => { 17 | let loader: TranslationLoaderJson; 18 | let httpClient: HttpClient; 19 | let httpTestingController: HttpTestingController; 20 | 21 | beforeEach(() => { 22 | TestBed.configureTestingModule({ 23 | imports: [HttpClientTestingModule], 24 | providers: [ 25 | TranslationLoaderJson, 26 | ], 27 | }); 28 | 29 | httpClient = TestBed.get(HttpClient); 30 | loader = TestBed.get(TranslationLoaderJson); 31 | httpTestingController = TestBed.get(HttpTestingController); 32 | 33 | PromiseMatcher.install(); 34 | }); 35 | 36 | afterEach(() => { 37 | PromiseMatcher.uninstall(); 38 | httpTestingController.verify(); 39 | }); 40 | 41 | it("is defined", () => { 42 | expect(loader.load).toBeDefined(); 43 | expect(typeof loader.load).toBe("function"); 44 | }); 45 | 46 | it("returns a promise", () => { 47 | let promise = loader.load({ language: "en" }); 48 | 49 | httpTestingController.expectOne("assets/i18n/./en.json"); 50 | expect(promise instanceof Promise).toBeTruthy(); 51 | }); 52 | 53 | it("loads a language file", () => { 54 | loader.load({ language: "en" }); 55 | 56 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 57 | expect(request.request.method).toBe("GET"); 58 | }); 59 | 60 | it("can be configured", () => { 61 | loader.load({ 62 | language: "en", 63 | module: "test", 64 | path: "app/translations/{{module}}/{{language}}-lang.json", 65 | }); 66 | 67 | const request = httpTestingController.expectOne("app/translations/test/en-lang.json"); 68 | expect(request.request.method).toBe("GET"); 69 | }); 70 | 71 | it("resolves when connection responds", () => { 72 | let promise = loader.load({ language: "en" }); 73 | 74 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 75 | request.flush({TEXT: "This is a text"}); 76 | 77 | expect(promise).toBeResolved(); 78 | }); 79 | 80 | it("transforms result to object", () => { 81 | let promise = loader.load({ language: "en" }); 82 | 83 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 84 | request.flush({TEXT: "This is a text"}); 85 | 86 | expect(promise).toBeResolvedWith({TEXT: "This is a text"}); 87 | }); 88 | 89 | it("rejects when connection fails", () => { 90 | let promise = loader.load({ language: "en" }); 91 | 92 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 93 | request.flush("", { status: 500, statusText: "Internal Server Error" }); 94 | 95 | expect(promise).toBeRejectedWith("Internal Server Error"); 96 | }); 97 | 98 | it("combines arrays to a string", () => { 99 | let promise = loader.load({ language: "en" }); 100 | 101 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 102 | request.flush({ 103 | COOKIE_INFORMATION: [ 104 | "We are using cookies to adjust our website to the needs of our customers. ", 105 | "By using our websites you agree to store cookies on your computer, tablet or smartphone.", 106 | ], 107 | }); 108 | 109 | expect(promise).toBeResolvedWith({ 110 | COOKIE_INFORMATION: "We are using cookies to adjust our website to the needs of our customers. " + 111 | "By using our websites you agree to store cookies on your computer, tablet or smartphone.", 112 | }); 113 | }); 114 | 115 | it("allows nested objects", () => { 116 | let promise = loader.load({ language: "en" }); 117 | let nestedObj: any = { 118 | TEXT: { 119 | NESTED: "This is a text", 120 | }, 121 | }; 122 | 123 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 124 | request.flush(nestedObj); 125 | 126 | expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text"}); 127 | }); 128 | 129 | it("allows multiple nested objects", () => { 130 | let promise = loader.load({ language: "en" }); 131 | let nestedObj: any = { 132 | TEXT: { 133 | NESTED: "This is a text", 134 | SECONDNEST: { 135 | TEXT: "Second text", 136 | }, 137 | }, 138 | }; 139 | 140 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 141 | request.flush(nestedObj); 142 | 143 | expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text", "TEXT.SECONDNEST.TEXT": "Second text"}); 144 | }); 145 | 146 | it("combines arrays to a string while returning nested objects", () => { 147 | let promise = loader.load({ language: "en" }); 148 | let nestedObj: any = { 149 | COOKIE_INFORMATION: [ 150 | "We are using cookies to adjust our website to the needs of our customers. ", 151 | "By using our websites you agree to store cookies on your computer, tablet or smartphone.", 152 | ], 153 | TEXT: { 154 | NESTED: "This is a text", 155 | }, 156 | }; 157 | 158 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 159 | request.flush(nestedObj); 160 | 161 | expect(promise).toBeResolvedWith({ 162 | "COOKIE_INFORMATION": "We are using cookies to adjust our website to " + 163 | "the needs of our customers. By using our websites you agree to store cookies on your computer, " + 164 | "tablet or smartphone.", 165 | "TEXT.NESTED": "This is a text", 166 | }); 167 | }); 168 | 169 | it("allows nested objects with lower case keys and with camel case", () => { 170 | let promise = loader.load({ language: "en" }); 171 | let nestedObj: any = { 172 | text: { 173 | nestedText: "This is a text", 174 | }, 175 | }; 176 | 177 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 178 | request.flush(nestedObj); 179 | 180 | expect(promise).toBeResolvedWith({"text.nestedText": "This is a text"}); 181 | }); 182 | 183 | it("filters non string values within nested object", () => { 184 | let promise = loader.load({ language: "en" }); 185 | let nestedObj: any = { 186 | TEXT: { 187 | ANSWER: 42, 188 | NESTED: "This is a text", 189 | }, 190 | }; 191 | 192 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 193 | request.flush(nestedObj); 194 | 195 | expect(promise).toBeResolvedWith({"TEXT.NESTED": "This is a text"}); 196 | }); 197 | 198 | it("combines arrays to a string while beeing in nested objects", () => { 199 | let promise = loader.load({ language: "en" }); 200 | let nestedObj: any = { 201 | TEXT: { 202 | COOKIE_INFORMATION: [ 203 | "We are using cookies to adjust our website to the needs of our customers. ", 204 | "By using our websites you agree to store cookies on your computer, tablet or smartphone.", 205 | ], 206 | }, 207 | }; 208 | 209 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 210 | request.flush(nestedObj); 211 | 212 | expect(promise).toBeResolvedWith({ 213 | "TEXT.COOKIE_INFORMATION": "We are using cookies to adjust our website " + 214 | "to the needs of our customers. By using our websites you agree to store cookies on your " + 215 | "computer, tablet or smartphone.", 216 | }); 217 | }); 218 | 219 | it("merges translations to one dimension", () => { 220 | let promise = loader.load({ language: "en" }); 221 | 222 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 223 | request.flush({ 224 | app: { 225 | componentA: { 226 | TEXT: "something else", 227 | }, 228 | loginText: "Please login before continuing!", 229 | }, 230 | }); 231 | 232 | expect(promise).toBeResolvedWith({ 233 | "app.componentA.TEXT": "something else", 234 | "app.loginText": "Please login before continuing!", 235 | }); 236 | }); 237 | 238 | it("filters non string values", () => { 239 | let promise = loader.load({ language: "en" }); 240 | 241 | const request = httpTestingController.expectOne("assets/i18n/./en.json"); 242 | request.flush({ 243 | ANSWER: 42, 244 | COMBINED: [ 245 | "7 multiplied by 6 is ", 246 | 42, 247 | ], 248 | }); 249 | 250 | expect(promise).toBeResolvedWith({COMBINED: "7 multiplied by 6 is "}); 251 | }); 252 | }); 253 | }); 254 | -------------------------------------------------------------------------------- /tests/TranslatorContainer.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TranslateLogHandler, 3 | Translator, 4 | TranslatorConfig, 5 | TranslatorContainer, 6 | } from "../index"; 7 | 8 | import {ReflectiveInjector} from "@angular/core"; 9 | import {TestBed} from "@angular/core/testing"; 10 | import {Observable} from "rxjs"; 11 | import {TranslateLogHandlerMock, TranslationLoaderMock} from "./helper/TranslatorMocks"; 12 | 13 | describe("TranslatorContainer", () => { 14 | it("is defined", () => { 15 | expect(TranslatorContainer).toBeDefined(); 16 | }); 17 | 18 | it("requires a TranslatorConfig", () => { 19 | let injector = ReflectiveInjector.resolveAndCreate([ 20 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 21 | TranslatorContainer, 22 | ]); 23 | 24 | let action = () => { 25 | try { 26 | injector.get(TranslatorContainer); 27 | } catch (e) { 28 | expect(e.message).toBe( 29 | "No provider for TranslatorConfig! (TranslatorContainer -> TranslatorConfig)", 30 | ); 31 | throw e; 32 | } 33 | }; 34 | expect(action).toThrow(); 35 | }); 36 | 37 | it("requires a TranslateLogHandler", () => { 38 | let translatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 39 | loader: TranslationLoaderMock, 40 | }); 41 | let injector = ReflectiveInjector.resolveAndCreate([ 42 | { provide: TranslatorConfig, useValue: translatorConfig }, 43 | TranslatorContainer, 44 | ]); 45 | 46 | let action = () => { 47 | try { 48 | injector.get(TranslatorContainer); 49 | } catch (e) { 50 | expect(e.message).toBe( 51 | "No provider for TranslateLogHandler! (TranslatorContainer -> TranslateLogHandler)", 52 | ); 53 | throw e; 54 | } 55 | }; 56 | expect(action).toThrow(); 57 | }); 58 | 59 | describe("constructor", () => { 60 | let translatorConfig: TranslatorConfig; 61 | let translatorContainer: TranslatorContainer; 62 | 63 | beforeEach(() => { 64 | translatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 65 | loader: TranslationLoaderMock, 66 | }); 67 | TestBed.configureTestingModule({ 68 | providers: [ 69 | { provide: TranslatorConfig, useValue: translatorConfig}, 70 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 71 | TranslatorContainer, 72 | ], 73 | }); 74 | }); 75 | 76 | it("sets current lang to default lang", () => { 77 | translatorConfig.setOptions({ 78 | detectLanguage: false, 79 | defaultLanguage: "ru", 80 | providedLanguages: [ "ru", "en" ], 81 | }); 82 | 83 | translatorContainer = TestBed.get(TranslatorContainer); 84 | 85 | expect(translatorContainer.language).toBe("ru"); 86 | }); 87 | 88 | it("creates an observable for language changes", () => { 89 | translatorContainer = TestBed.get(TranslatorContainer); 90 | 91 | expect(translatorContainer.languageChanged).toEqual(jasmine.any(Observable)); 92 | }); 93 | 94 | describe("detectLanguage", () => { 95 | it("detects language automatically on start", () => { 96 | translatorConfig.setOptions({ 97 | providedLanguages: [ "en", "de" ], 98 | navigatorLanguages: [ "de-DE", "de", "en-US", "en" ], 99 | }); 100 | 101 | translatorContainer = TestBed.get(TranslatorContainer); 102 | 103 | expect(translatorContainer.language).toBe("de"); 104 | }); 105 | 106 | it("preferred language over exact matches", () => { 107 | translatorConfig.setOptions({ 108 | providedLanguages: ["de", "fr", "en-US", "en-GB"], 109 | navigatorLanguages: ["fr-ML", "en-US"], 110 | preferExactMatches: false, 111 | }); 112 | 113 | translatorContainer = TestBed.get(TranslatorContainer); 114 | 115 | expect(translatorContainer.language).toBe("fr"); 116 | }); 117 | 118 | it("prefers exact matches", () => { 119 | translatorConfig.setOptions({ 120 | providedLanguages: ["de", "fr", "en-US", "en-GB"], 121 | navigatorLanguages: ["fr-ML", "en-US"], 122 | preferExactMatches: true, 123 | }); 124 | 125 | translatorContainer = TestBed.get(TranslatorContainer); 126 | 127 | expect(translatorContainer.language).toBe("en-US"); 128 | }); 129 | 130 | it("informs about detected language", () => { 131 | translatorConfig.setOptions({ 132 | providedLanguages: [ "en", "de" ], 133 | navigatorLanguages: [ "de-DE", "de", "en-US", "en" ], 134 | }); 135 | let logHandler = TestBed.get(TranslateLogHandler); 136 | spyOn(logHandler, "info"); 137 | 138 | translatorContainer = TestBed.get(TranslatorContainer); 139 | 140 | expect(logHandler.info).toHaveBeenCalledWith("Language de got detected"); 141 | }); 142 | }); 143 | }); 144 | 145 | describe("instance", () => { 146 | let translatorContainer: TranslatorContainer; 147 | let translatorConfig: TranslatorConfig; 148 | 149 | beforeEach(() => { 150 | translatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 151 | loader: TranslationLoaderMock, 152 | providedLanguages: [ "en", "de" ], 153 | detectLanguage: false, 154 | }); 155 | 156 | TestBed.configureTestingModule({ 157 | providers: [ 158 | { provide: TranslatorConfig, useValue: translatorConfig}, 159 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 160 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 161 | TranslatorContainer, 162 | ], 163 | }); 164 | 165 | translatorContainer = TestBed.get(TranslatorContainer); 166 | }); 167 | 168 | describe("change language", () => { 169 | it("checks that language is provided using strict checking", () => { 170 | spyOn(translatorConfig, "providedLanguage").and.callThrough(); 171 | 172 | translatorContainer.language = "en" ; 173 | 174 | expect(translatorConfig.providedLanguage).toHaveBeenCalledWith("en", true); 175 | }); 176 | 177 | it("sets current language to the provided language", () => { 178 | translatorConfig.setOptions({ providedLanguages: [ "de/de" ]}); 179 | 180 | translatorContainer.language = "de-DE"; 181 | 182 | expect(translatorContainer.language).toBe("de/de"); 183 | }); 184 | 185 | it("throws error if language is not provided", () => { 186 | translatorConfig.setOptions({ providedLanguages: [ "de/de" ]}); 187 | 188 | let action = () => { 189 | translatorContainer.language = "de"; 190 | }; 191 | 192 | expect(action).toThrow(new Error("Language de not provided")); 193 | }); 194 | 195 | it("does not change when language not available", () => { 196 | try { 197 | translatorContainer.language = "ru"; 198 | } catch (e) {} 199 | 200 | expect(translatorContainer.language).toBe("en"); 201 | }); 202 | 203 | it("gives the next value to the observable", () => { 204 | translatorConfig.setOptions({ providedLanguages: ["en", "de"]}); 205 | let spy = jasmine.createSpy("languageChanged"); 206 | translatorContainer.languageChanged.subscribe(spy); 207 | 208 | translatorContainer.language = "de"; 209 | 210 | expect(spy).toHaveBeenCalledWith("de"); 211 | }); 212 | 213 | it("informs not about language change", () => { 214 | let translateLogHandler = TestBed.get(TranslateLogHandler); 215 | spyOn(translateLogHandler, "info"); 216 | translatorConfig.setOptions({ providedLanguages: [ "de/de" ]}); 217 | 218 | translatorContainer.language = "de-DE"; 219 | 220 | expect(translateLogHandler.info).not.toHaveBeenCalled(); 221 | }); 222 | 223 | it("hits all subscribers when language change", () => { 224 | translatorConfig.setOptions({ providedLanguages: ["en", "de"]}); 225 | let spy1 = jasmine.createSpy("languageChanged"); 226 | let spy2 = jasmine.createSpy("languageChanged"); 227 | translatorContainer.languageChanged.subscribe(spy1); 228 | translatorContainer.languageChanged.subscribe(spy2); 229 | 230 | translatorContainer.language = "de"; 231 | 232 | expect(spy1).toHaveBeenCalledWith("de"); 233 | expect(spy2).toHaveBeenCalledWith("de"); 234 | }); 235 | }); 236 | 237 | describe("get translator", () => { 238 | it("returns a translator", () => { 239 | let translator: Translator = translatorContainer.getTranslator("test"); 240 | 241 | expect(translator).toEqual(jasmine.any(Translator)); 242 | }); 243 | 244 | it("returns a translator for given module", () => { 245 | let translator: Translator = translatorContainer.getTranslator("test"); 246 | 247 | expect(translator.module).toBe("test"); 248 | }); 249 | 250 | it("returns previously created translators", () => { 251 | let translator: Translator = translatorContainer.getTranslator("test"); 252 | 253 | let result: Translator = translatorContainer.getTranslator("test"); 254 | 255 | expect(result).toBe(translator); 256 | }); 257 | }); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /tests/TranslateComponent.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | provideTranslator, 3 | TranslateComponent, 4 | TranslateLogHandler, 5 | Translator, 6 | TranslatorConfig, 7 | TranslatorContainer, 8 | TranslatorModule, 9 | } from "../index"; 10 | 11 | import {Component, ReflectiveInjector} from "@angular/core"; 12 | import {fakeAsync, flushMicrotasks, TestBed} from "@angular/core/testing"; 13 | import {JasmineHelper} from "./helper/JasmineHelper"; 14 | import {TranslateLogHandlerMock, TranslationLoaderMock} from "./helper/TranslatorMocks"; 15 | 16 | describe("TranslateComponent", () => { 17 | 18 | describe("constructor", () => { 19 | it("requires a Translator", () => { 20 | let injector = ReflectiveInjector.resolveAndCreate([ TranslateComponent ]); 21 | 22 | let action = () => { 23 | try { 24 | injector.get(TranslateComponent); 25 | } catch (e) { 26 | expect(e.message).toContain("No provider for Translator!"); 27 | throw e; 28 | } 29 | }; 30 | expect(action).toThrow(); 31 | }); 32 | 33 | it("requires a TranslateLogHandler", () => { 34 | let translatorConfig: TranslatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 35 | loader: TranslationLoaderMock, 36 | providedLanguages: [ "en", "de" ], 37 | }); 38 | let injector = ReflectiveInjector.resolveAndCreate([ 39 | TranslateComponent, 40 | TranslatorContainer, 41 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 42 | { provide: TranslatorConfig, useValue: translatorConfig }, 43 | provideTranslator("test"), 44 | ]); 45 | 46 | let action = () => { 47 | try { 48 | injector.get(TranslateComponent); 49 | } catch (e) { 50 | expect(e.message).toContain("No provider for TranslateLogHandler!"); 51 | throw e; 52 | } 53 | }; 54 | expect(action).toThrow(); 55 | }); 56 | 57 | it("subscribes on language changes", () => { 58 | let translatorConfig: TranslatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 59 | loader: TranslationLoaderMock, 60 | providedLanguages: [ "en", "de" ], 61 | }); 62 | let injector = ReflectiveInjector.resolveAndCreate([ 63 | TranslateComponent, 64 | TranslatorContainer, 65 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 66 | { provide: TranslatorConfig, useValue: translatorConfig }, 67 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 68 | provideTranslator("test"), 69 | ]); 70 | 71 | let translator: Translator = injector.get(Translator); 72 | spyOn(translator.languageChanged, "subscribe").and.callThrough(); 73 | 74 | injector.get(TranslateComponent); 75 | 76 | expect(translator.languageChanged.subscribe).toHaveBeenCalled(); 77 | }); 78 | }); 79 | 80 | describe("instance", () => { 81 | let translator: Translator; 82 | let translatorConfig: TranslatorConfig; 83 | let translateComponent: TranslateComponent; 84 | let logHandler: TranslateLogHandler; 85 | let translateContainer: TranslatorContainer; 86 | 87 | beforeEach(() => { 88 | translatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 89 | loader: TranslationLoaderMock, 90 | providedLanguages: [ "en", "de" ], 91 | }); 92 | 93 | TestBed.configureTestingModule({ 94 | providers: [ 95 | { provide: TranslatorConfig, useValue: translatorConfig}, 96 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 97 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 98 | provideTranslator("test"), 99 | TranslatorContainer, 100 | TranslateComponent, 101 | ], 102 | }); 103 | 104 | translator = TestBed.get(Translator); 105 | translateComponent = TestBed.get(TranslateComponent); 106 | logHandler = TestBed.get(TranslateLogHandler); 107 | translateContainer = TestBed.get(TranslatorContainer); 108 | 109 | spyOn(translator, "translate").and.returnValue(Promise.resolve("This is a text")); 110 | spyOn(logHandler, "error"); 111 | }); 112 | 113 | it("starts translation when key got set", () => { 114 | translateComponent.key = "TEXT"; 115 | 116 | expect(translator.translate).toHaveBeenCalledWith("TEXT", {}); 117 | }); 118 | 119 | it("starts translation when key is set and params got changed", () => { 120 | translateComponent.key = "TEXT"; 121 | JasmineHelper.calls(translator.translate).reset(); 122 | 123 | translateComponent.params = { some: "value" }; 124 | 125 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 126 | }); 127 | 128 | it("restarts translation when key got changed", () => { 129 | translateComponent.key = "ANYTHING"; 130 | translateComponent.params = { some: "value" }; 131 | JasmineHelper.calls(translator.translate).reset(); 132 | 133 | translateComponent.key = "TEXT"; 134 | 135 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 136 | }); 137 | 138 | it("does not translate when key got not set", () => { 139 | translateComponent.params = { some: "value" }; 140 | 141 | expect(translator.translate).not.toHaveBeenCalled(); 142 | }); 143 | 144 | it("does not accept non-object params", () => { 145 | translateComponent.key = "TEXT"; 146 | JasmineHelper.calls(translator.translate).reset(); 147 | 148 | translateComponent.params = "foo"; 149 | 150 | expect(translator.translate).not.toHaveBeenCalled(); 151 | }); 152 | 153 | it("stores translation when promise got resolved", fakeAsync(() => { 154 | translateComponent.key = "TEXT"; 155 | 156 | flushMicrotasks(); 157 | 158 | expect(translateComponent.translation).toBe("This is a text"); 159 | })); 160 | 161 | it("restarts translation when language got changed", () => { 162 | translateComponent.key = "TEXT"; 163 | JasmineHelper.calls(translator.translate).reset(); 164 | 165 | translator.language = "de"; 166 | 167 | expect(translator.translate).toHaveBeenCalledWith("TEXT", {}); 168 | }); 169 | 170 | it("shows error if params are not object", () => { 171 | translateComponent.params = "foo"; 172 | 173 | expect(logHandler.error).toHaveBeenCalledWith("Params have to be an object"); 174 | }); 175 | 176 | describe("translatorModule attribute", () => { 177 | let anotherTranslator: Translator; 178 | 179 | beforeEach(() => { 180 | anotherTranslator = translateContainer.getTranslator("another"); 181 | 182 | spyOn(anotherTranslator, "translate").and.returnValue(Promise.resolve("This is a text")); 183 | }); 184 | 185 | it("uses another module with translatorModule", () => { 186 | spyOn(translateContainer, "getTranslator").and.callThrough(); 187 | 188 | translateComponent.module = "another"; 189 | 190 | expect(translateContainer.getTranslator).toHaveBeenCalledWith("another"); 191 | }); 192 | 193 | it("subscribes to the other language changed", () => { 194 | spyOn(anotherTranslator.languageChanged, "subscribe").and.callThrough(); 195 | 196 | translateComponent.module = "another"; 197 | 198 | expect(anotherTranslator.languageChanged.subscribe).toHaveBeenCalled(); 199 | }); 200 | 201 | it("starts the translation after module is changed", () => { 202 | translateComponent.key = "TEXT"; 203 | 204 | translateComponent.module = "another"; 205 | 206 | expect(anotherTranslator.translate).toHaveBeenCalledWith("TEXT", {}); 207 | }); 208 | 209 | it("does not react on language changes of original translator", () => { 210 | translateComponent.key = "TEXT"; 211 | translateComponent.module = "another"; 212 | 213 | translator.language = "de"; 214 | 215 | expect(JasmineHelper.calls(anotherTranslator.translate).count()).toBe(1); 216 | }); 217 | 218 | it("restarts translation on language changes", () => { 219 | translateComponent.key = "TEXT"; 220 | translateComponent.module = "another"; 221 | JasmineHelper.calls(anotherTranslator.translate).reset(); 222 | 223 | anotherTranslator.language = "de"; 224 | 225 | expect(anotherTranslator.translate).toHaveBeenCalledWith("TEXT", {}); 226 | }); 227 | }); 228 | }); 229 | 230 | describe("within module", () => { 231 | let translator: Translator; 232 | 233 | @Component({ 234 | selector: "my-component", 235 | template: `

`, 236 | }) 237 | class MyComponent {} 238 | 239 | beforeEach(() => { 240 | TestBed.configureTestingModule({ 241 | imports: [ TranslatorModule.forRoot() ], 242 | declarations: [ MyComponent ], 243 | }); 244 | 245 | translator = TestBed.get(Translator); 246 | spyOn(translator, "translate").and.returnValue(Promise.resolve("some text")); 247 | }); 248 | 249 | it("first resolves the parameters", () => { 250 | let component = TestBed.createComponent(MyComponent); 251 | 252 | component.detectChanges(); 253 | 254 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 255 | expect(JasmineHelper.calls(translator.translate).count()).toBe(1); 256 | }); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /tests/TranslatePipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | provideTranslator, 3 | TranslateLogHandler, 4 | TranslatePipe, 5 | Translator, 6 | TranslatorConfig, 7 | TranslatorContainer, 8 | } from "../index"; 9 | 10 | import {ReflectiveInjector} from "@angular/core"; 11 | import {fakeAsync, flushMicrotasks, TestBed} from "@angular/core/testing"; 12 | import {JasmineHelper} from "./helper/JasmineHelper"; 13 | import {TranslateLogHandlerMock, TranslationLoaderMock} from "./helper/TranslatorMocks"; 14 | 15 | describe("TranslatePipe", () => { 16 | it("is defined", () => { 17 | expect(TranslatePipe).toBeDefined(); 18 | }); 19 | 20 | describe("constructor", () => { 21 | it("requires a Translator", () => { 22 | let injector = ReflectiveInjector.resolveAndCreate([ TranslatePipe ]); 23 | 24 | let action = () => { 25 | try { 26 | injector.get(TranslatePipe); 27 | } catch (e) { 28 | expect(e.message).toContain("No provider for Translator!"); 29 | throw e; 30 | } 31 | }; 32 | expect(action).toThrow(); 33 | }); 34 | 35 | it("requires a TranslateLogHandler", () => { 36 | let translatorConfig: TranslatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 37 | loader: TranslationLoaderMock, 38 | providedLanguages: [ "en", "de" ], 39 | }); 40 | let injector = ReflectiveInjector.resolveAndCreate([ 41 | TranslatePipe, 42 | TranslatorContainer, 43 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 44 | { provide: TranslatorConfig, useValue: translatorConfig }, 45 | provideTranslator("test"), 46 | ]); 47 | 48 | let action = () => { 49 | try { 50 | injector.get(TranslatePipe); 51 | } catch (e) { 52 | expect(e.message).toContain("No provider for TranslateLogHandler!"); 53 | throw e; 54 | } 55 | }; 56 | expect(action).toThrow(); 57 | }); 58 | 59 | it("subscribes on language changes", () => { 60 | let translatorConfig: TranslatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 61 | loader: TranslationLoaderMock, 62 | providedLanguages: [ "en", "de" ], 63 | }); 64 | let injector = ReflectiveInjector.resolveAndCreate([ 65 | TranslatePipe, 66 | TranslatorContainer, 67 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 68 | { provide: TranslatorConfig, useValue: translatorConfig }, 69 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 70 | provideTranslator("test"), 71 | ]); 72 | 73 | let translator: Translator = injector.get(Translator); 74 | spyOn(translator.languageChanged, "subscribe").and.callThrough(); 75 | 76 | injector.get(TranslatePipe); 77 | 78 | expect(translator.languageChanged.subscribe).toHaveBeenCalled(); 79 | }); 80 | }); 81 | 82 | describe("transform", () => { 83 | let translator: Translator; 84 | let translatorConfig: TranslatorConfig; 85 | let translatePipe: TranslatePipe; 86 | let logHandler: TranslateLogHandler; 87 | let translateContainer: TranslatorContainer; 88 | 89 | beforeEach(() => { 90 | translatorConfig = new TranslatorConfig(new TranslateLogHandlerMock(), { 91 | loader: TranslationLoaderMock, 92 | providedLanguages: [ "en", "de" ], 93 | }); 94 | 95 | TestBed.configureTestingModule({ 96 | providers: [ 97 | { provide: TranslatorConfig, useValue: translatorConfig}, 98 | { provide: TranslationLoaderMock, useValue: new TranslationLoaderMock() }, 99 | { provide: TranslateLogHandler, useClass: TranslateLogHandlerMock }, 100 | provideTranslator("test"), 101 | TranslatorContainer, 102 | TranslatePipe, 103 | ], 104 | }); 105 | 106 | translator = TestBed.get(Translator); 107 | translatePipe = TestBed.get(TranslatePipe); 108 | logHandler = TestBed.get(TranslateLogHandler); 109 | translateContainer = TestBed.get(TranslatorContainer); 110 | 111 | spyOn(translator, "translate").and.returnValue(Promise.resolve("This is a text")); 112 | spyOn(logHandler, "error"); 113 | }); 114 | 115 | it("returns an empty string", () => { 116 | let translation = translatePipe.transform("TEXT"); 117 | 118 | expect(translation).toBe(""); 119 | }); 120 | 121 | it("calls translate to get translation", () => { 122 | translatePipe.transform("TEXT"); 123 | 124 | expect(translator.translate).toHaveBeenCalledWith("TEXT", {}); 125 | }); 126 | 127 | it("calls translate only once", () => { 128 | translatePipe.transform("TEXT"); 129 | translatePipe.transform("TEXT"); 130 | 131 | expect(JasmineHelper.calls(translator.translate).count()).toBe(1); 132 | }); 133 | 134 | it("gets params from args[0]", () => { 135 | translatePipe.transform("TEXT", { some: "value" }); 136 | 137 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 138 | }); 139 | 140 | it("returns translation when promise got resolved", fakeAsync(() => { 141 | translatePipe.transform("TEXT"); 142 | 143 | flushMicrotasks(); 144 | let translation = translatePipe.transform("TEXT"); 145 | 146 | expect(translation).toBe("This is a text"); 147 | })); 148 | 149 | it("calls translate again when key changes", () => { 150 | translatePipe.transform("ANYTHING"); 151 | translatePipe.transform("TEXT"); 152 | 153 | expect(translator.translate).toHaveBeenCalledWith("ANYTHING", {}); 154 | expect(translator.translate).toHaveBeenCalledWith("TEXT", {}); 155 | expect(JasmineHelper.calls(translator.translate).count()).toBe(2); 156 | }); 157 | 158 | it("calls translate again when params changes", () => { 159 | translatePipe.transform("TEXT", { some: "value" }); 160 | translatePipe.transform("TEXT", { some: "otherValue" }); 161 | 162 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 163 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "otherValue" }); 164 | expect(JasmineHelper.calls(translator.translate).count()).toBe(2); 165 | }); 166 | 167 | it("calls translate again when language got changed", () => { 168 | translatePipe.transform("TEXT"); 169 | 170 | translator.language = "de"; 171 | 172 | expect(JasmineHelper.calls(translator.translate).count()).toBe(2); 173 | }); 174 | 175 | it("does not translate when no values given", () => { 176 | translator.language = "de"; 177 | 178 | expect(translator.translate).not.toHaveBeenCalled(); 179 | }); 180 | 181 | it("uses the first item of array if params is an array", () => { 182 | let params: any = { some: "value" }; 183 | translatePipe.transform("TEXT", [params]); 184 | 185 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 186 | }); 187 | 188 | it("parses string for backward compatibility", () => { 189 | let params: any = "{ some: 'value' }"; 190 | translatePipe.transform("TEXT", [params]); 191 | 192 | expect(translator.translate).toHaveBeenCalledWith("TEXT", { some: "value" }); 193 | }); 194 | 195 | it("ignores errors while parsing", () => { 196 | let params: any = "{something}"; 197 | translatePipe.transform("TEXT", [params]); 198 | 199 | expect(translator.translate).toHaveBeenCalledWith("TEXT", {}); 200 | }); 201 | 202 | describe("translatorModule attribute", () => { 203 | let anotherTranslator: Translator; 204 | 205 | beforeEach(() => { 206 | anotherTranslator = translateContainer.getTranslator("another"); 207 | 208 | spyOn(anotherTranslator, "translate").and.returnValue(Promise.resolve("This is a text")); 209 | }); 210 | 211 | it("uses another module with translatorModule", () => { 212 | spyOn(translateContainer, "getTranslator").and.callThrough(); 213 | 214 | translatePipe.transform("TEXT", {}, "another"); 215 | 216 | expect(translateContainer.getTranslator).toHaveBeenCalledWith("another"); 217 | }); 218 | 219 | it("subscribes to the other language changed", () => { 220 | spyOn(anotherTranslator.languageChanged, "subscribe").and.callThrough(); 221 | 222 | translatePipe.transform("TEXT", {}, "another"); 223 | 224 | expect(anotherTranslator.languageChanged.subscribe).toHaveBeenCalled(); 225 | }); 226 | 227 | it("starts the translation when module got changed", () => { 228 | translatePipe.transform("TEXT", {}); 229 | 230 | translatePipe.transform("TEXT", {}, "another"); 231 | 232 | expect(anotherTranslator.translate).toHaveBeenCalledWith("TEXT", {}); 233 | }); 234 | 235 | it("does not react on language changes of original translator", () => { 236 | translatePipe.transform("TEXT", {}); 237 | translatePipe.transform("TEXT", {}, "another"); 238 | 239 | translator.language = "de"; 240 | 241 | expect(JasmineHelper.calls(anotherTranslator.translate).count()).toBe(1); 242 | }); 243 | 244 | it("restarts translation on language changes", () => { 245 | translatePipe.transform("TEXT", {}, "another"); 246 | JasmineHelper.calls(anotherTranslator.translate).reset(); 247 | 248 | anotherTranslator.language = "de"; 249 | 250 | expect(anotherTranslator.translate).toHaveBeenCalledWith("TEXT", {}); 251 | }); 252 | }); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /tests/TranslatorConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import { COMMON_PURE_PIPES, TranslateLogHandler, TranslatePipe, TranslatorConfig } from "../index"; 2 | 3 | import { 4 | CurrencyPipe, 5 | DatePipe, 6 | DecimalPipe, 7 | JsonPipe, 8 | LowerCasePipe, 9 | PercentPipe, 10 | SlicePipe, 11 | TitleCasePipe, 12 | UpperCasePipe, 13 | } from "@angular/common"; 14 | import { PipeTransform } from "@angular/core"; 15 | import { TranslateLogHandlerMock } from "./helper/TranslatorMocks"; 16 | 17 | class AnotherPipe implements PipeTransform { 18 | public static pipeName = "another"; 19 | 20 | public transform(value: any, ...args: any[]) { 21 | throw new Error("Method not implemented."); 22 | } 23 | } 24 | 25 | describe("TranslatorConfig", () => { 26 | let logHandler: TranslateLogHandler; 27 | 28 | beforeEach(() => { 29 | logHandler = new TranslateLogHandlerMock(); 30 | }); 31 | 32 | it("is defined", () => { 33 | let translatorConfig = new TranslatorConfig(logHandler); 34 | 35 | expect(TranslatorConfig).toBeDefined(); 36 | expect(translatorConfig).toBeDefined(); 37 | expect(translatorConfig instanceof TranslatorConfig).toBeTruthy(); 38 | }); 39 | 40 | it("gets default language from parameter defaultLanguage", () => { 41 | let translatorConfig = new TranslatorConfig(logHandler, { 42 | defaultLanguage: "cn", 43 | providedLanguages: [ "en", "cn" ], 44 | }); 45 | 46 | expect(translatorConfig.defaultLanguage).toBe("cn"); 47 | }); 48 | 49 | it("ignores options from prototype", () => { 50 | // tslint:disable-next-line 51 | let MyObject = function MyOption(): void {}; 52 | MyObject.prototype.defaultLanguage = "de"; 53 | let config = new MyObject(); 54 | config.providedLanguages = ["en", "de"]; 55 | 56 | let translatorConfig = new TranslatorConfig(logHandler, config); 57 | 58 | expect(translatorConfig.defaultLanguage).toBe("en"); 59 | expect(translatorConfig.providedLanguages).toEqual(["en", "de"]); 60 | }); 61 | 62 | it("defines a list of provided languages", () => { 63 | let translatorConfig = new TranslatorConfig(logHandler); 64 | 65 | expect(translatorConfig.providedLanguages).toEqual(["en"]); 66 | }); 67 | 68 | it("gets provided languages from parameter providedLanguages", () => { 69 | let translatorConfig = new TranslatorConfig(logHandler, { providedLanguages: [ "cn" ] }); 70 | 71 | expect(translatorConfig.providedLanguages).toEqual([ "cn" ]); 72 | }); 73 | 74 | it("uses first provided language", () => { 75 | let translatorConfig = new TranslatorConfig(logHandler, { 76 | defaultLanguage: "en", // default - unnecessary 77 | providedLanguages: [ "cn" ], 78 | }); 79 | 80 | expect(translatorConfig.defaultLanguage).toBe("cn"); 81 | }); 82 | 83 | describe("languageProvided", () => { 84 | let translatorConfig: TranslatorConfig; 85 | 86 | it("returns the language if provided", () => { 87 | translatorConfig = new TranslatorConfig(logHandler, { 88 | providedLanguages: [ "bm", "en" ], 89 | }); 90 | 91 | let providedLanguage = translatorConfig.providedLanguage("bm"); 92 | 93 | expect(providedLanguage).toBe("bm"); 94 | }); 95 | 96 | it("returns false when it is not provided", () => { 97 | translatorConfig = new TranslatorConfig(logHandler, { 98 | providedLanguages: [ "en" ], 99 | }); 100 | 101 | let providedLanguage = translatorConfig.providedLanguage("bm"); 102 | 103 | expect(providedLanguage).toBeFalsy(); 104 | }); 105 | 106 | it("returns provided language when we search with country", () => { 107 | translatorConfig = new TranslatorConfig(logHandler, { 108 | providedLanguages: [ "en" ], 109 | }); 110 | 111 | let providedLanguage = translatorConfig.providedLanguage("en-US"); 112 | 113 | expect(providedLanguage).toBe("en"); 114 | }); 115 | 116 | it("returns the first provided country specific language", () => { 117 | translatorConfig = new TranslatorConfig(logHandler, { 118 | providedLanguages: [ "de-DE", "de-AT" ], 119 | }); 120 | 121 | let providedLanguage = translatorConfig.providedLanguage("de-CH"); 122 | 123 | expect(providedLanguage).toBe("de-DE"); 124 | }); 125 | 126 | it("normalizes provided languages for checks", () => { 127 | translatorConfig = new TranslatorConfig(logHandler, { 128 | providedLanguages: [ "DE", "DE_AT" ], 129 | }); 130 | 131 | let providedLanguage = translatorConfig.providedLanguage("de-AT"); 132 | 133 | expect(providedLanguage).toBe("DE_AT"); 134 | }); 135 | 136 | it("normalizes searched language", () => { 137 | translatorConfig = new TranslatorConfig(logHandler, { 138 | providedLanguages: [ "de-DE", "de-AT" ], 139 | }); 140 | 141 | let providedLanguage = translatorConfig.providedLanguage("DE/de"); 142 | 143 | expect(providedLanguage).toBe("de-DE"); 144 | }); 145 | 146 | it("only finds direct matches", () => { 147 | translatorConfig = new TranslatorConfig(logHandler, { 148 | providedLanguages: [ "de-DE" ], 149 | }); 150 | 151 | let providedLanguage = translatorConfig.providedLanguage("de", true); 152 | 153 | expect(providedLanguage).toBeFalsy(); 154 | }); 155 | 156 | it("only takes valid matches", () => { 157 | translatorConfig = new TranslatorConfig(logHandler, { 158 | providedLanguages: [ "br", "en" ], 159 | }); 160 | 161 | let providedLanguage = translatorConfig.providedLanguage("british"); 162 | 163 | expect(providedLanguage).toBeFalsy(); 164 | }); 165 | 166 | it("allows full language names", () => { 167 | translatorConfig = new TranslatorConfig(logHandler, { 168 | providedLanguages: ["klingon", "en"], 169 | }); 170 | 171 | let providedLanguage = translatorConfig.providedLanguage("klingon"); 172 | 173 | expect(providedLanguage).toBe("klingon"); 174 | }); 175 | 176 | it("full language names can not be found in non-strict mode", () => { 177 | translatorConfig = new TranslatorConfig(logHandler, { 178 | providedLanguages: ["klingon", "en"], 179 | }); 180 | 181 | let providedLanguage = translatorConfig.providedLanguage("kl"); 182 | 183 | expect(providedLanguage).toBeFalsy(); 184 | }); 185 | 186 | it("finds languages within provided languages in non-strict mode", () => { 187 | translatorConfig = new TranslatorConfig(logHandler, { 188 | providedLanguages: ["klingon", "en_US"], 189 | }); 190 | 191 | let providedLanguage = translatorConfig.providedLanguage("en"); 192 | 193 | expect(providedLanguage).toBe("en_US"); 194 | }); 195 | }); 196 | 197 | describe("navigatorLanguages", () => { 198 | it("is always an array", () => { 199 | let translateConfig = new TranslatorConfig(logHandler); 200 | 201 | expect(translateConfig.navigatorLanguages instanceof Array).toBe(true); 202 | }); 203 | 204 | it("uses navigator.languages when given", () => { 205 | TranslatorConfig.navigator = { languages: [ "bm", "de", "fr", "en" ] }; 206 | 207 | let translateConfig = new TranslatorConfig(logHandler); 208 | 209 | expect(translateConfig.navigatorLanguages).toEqual([ "bm", "de", "fr", "en" ]); 210 | }); 211 | 212 | it("transforms navigator.languages to Array if it is String", () => { 213 | TranslatorConfig.navigator = { languages: "bm" }; 214 | 215 | let translateConfig = new TranslatorConfig(logHandler); 216 | 217 | expect(translateConfig.navigatorLanguages).toEqual([ "bm" ]); 218 | }); 219 | 220 | it("falls back to navigator.language", () => { 221 | TranslatorConfig.navigator = {language: "fr"}; 222 | 223 | let translateConfig = new TranslatorConfig(logHandler); 224 | 225 | expect(translateConfig.navigatorLanguages).toEqual(["fr"]); 226 | }); 227 | 228 | it("can be overwritten by options", () => { 229 | TranslatorConfig.navigator = {language: "fr"}; 230 | 231 | let translateConfig = new TranslatorConfig(logHandler, { 232 | navigatorLanguages: ["de", "en"], 233 | }); 234 | 235 | expect(translateConfig.navigatorLanguages).toEqual(["de", "en"]); 236 | }); 237 | }); 238 | 239 | describe("module", () => { 240 | let main: TranslatorConfig = new TranslatorConfig(logHandler, { 241 | defaultLanguage: "fr", 242 | providedLanguages: [ "fr", "de", "en", "it" ], 243 | }); 244 | 245 | it("returns a TranslatorConfig", () => { 246 | let moduleConfig = main.module("menu"); 247 | 248 | expect(moduleConfig).toEqual(jasmine.any(TranslatorConfig)); 249 | }); 250 | 251 | it("can not be stacked", () => { 252 | let moduleConfig = main.module("menu"); 253 | 254 | let action = function() { 255 | moduleConfig.module("deeper"); 256 | }; 257 | 258 | expect(action).toThrow(new Error("Module configs can not be stacked")); 259 | }); 260 | 261 | it("has the same options", () => { 262 | let moduleConfig = main.module("menu"); 263 | 264 | expect(moduleConfig.providedLanguages).toEqual(main.providedLanguages); 265 | expect(moduleConfig.defaultLanguage).toEqual(main.defaultLanguage); 266 | }); 267 | 268 | it("overwrites with the module options", () => { 269 | main.setOptions({ 270 | modules: { 271 | menu: { providedLanguages: [ "en" ] }, 272 | }, 273 | }); 274 | 275 | let moduleConfig = main.module("menu"); 276 | 277 | expect(moduleConfig.providedLanguages).toEqual([ "en" ]); 278 | expect(moduleConfig.defaultLanguage).toEqual("en"); 279 | }); 280 | }); 281 | 282 | describe("pipes", () => { 283 | it("contains the pure pipes from common module by default", () => { 284 | let translatorConfig = new TranslatorConfig(logHandler); 285 | 286 | expect(translatorConfig.pipes).toEqual({ 287 | currency: CurrencyPipe, 288 | date: DatePipe, 289 | number: DecimalPipe, 290 | json: JsonPipe, 291 | lowercase: LowerCasePipe, 292 | percent: PercentPipe, 293 | slice: SlicePipe, 294 | titlecase: TitleCasePipe, 295 | uppercase: UpperCasePipe, 296 | }); 297 | }); 298 | 299 | it("appends pipes defined in options", () => { 300 | let translatorConfig = new TranslatorConfig(logHandler, { 301 | pipes: [ TranslatePipe ], 302 | }); 303 | 304 | expect(translatorConfig.pipes).toEqual({ 305 | currency: CurrencyPipe, 306 | date: DatePipe, 307 | number: DecimalPipe, 308 | json: JsonPipe, 309 | lowercase: LowerCasePipe, 310 | percent: PercentPipe, 311 | slice: SlicePipe, 312 | titlecase: TitleCasePipe, 313 | uppercase: UpperCasePipe, 314 | translate: TranslatePipe, 315 | }); 316 | }); 317 | 318 | it("uses pipeName for mapping if available", () => { 319 | let translatorConfig = new TranslatorConfig(logHandler, { 320 | pipes: [ AnotherPipe ], 321 | }); 322 | 323 | expect(translatorConfig.pipes).toEqual({ 324 | currency: CurrencyPipe, 325 | date: DatePipe, 326 | number: DecimalPipe, 327 | json: JsonPipe, 328 | lowercase: LowerCasePipe, 329 | percent: PercentPipe, 330 | slice: SlicePipe, 331 | titlecase: TitleCasePipe, 332 | uppercase: UpperCasePipe, 333 | another: AnotherPipe, 334 | }); 335 | }); 336 | 337 | it("does not modify the constant", () => { 338 | let translatorConfig = new TranslatorConfig(logHandler, { 339 | pipes: [ TranslatePipe ], 340 | }); 341 | 342 | expect(COMMON_PURE_PIPES).toEqual([ 343 | CurrencyPipe, 344 | DatePipe, 345 | DecimalPipe, 346 | JsonPipe, 347 | LowerCasePipe, 348 | PercentPipe, 349 | SlicePipe, 350 | TitleCasePipe, 351 | UpperCasePipe, 352 | ]); 353 | }); 354 | 355 | it("logs an error when pipeName can not be resolved", () => { 356 | AnotherPipe.pipeName = null; 357 | spyOn(logHandler, "error"); 358 | 359 | let translatorConfig = new TranslatorConfig(logHandler, { 360 | pipes: [ AnotherPipe ], 361 | }); 362 | 363 | expect(translatorConfig.pipes).toEqual({ 364 | currency: CurrencyPipe, 365 | date: DatePipe, 366 | number: DecimalPipe, 367 | json: JsonPipe, 368 | lowercase: LowerCasePipe, 369 | percent: PercentPipe, 370 | slice: SlicePipe, 371 | titlecase: TitleCasePipe, 372 | uppercase: UpperCasePipe, 373 | }); 374 | expect(logHandler.error).toHaveBeenCalledWith("Pipe name for AnotherPipe can not be resolved"); 375 | }); 376 | }); 377 | }); 378 | -------------------------------------------------------------------------------- /docs/images/classes.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | TranslatorContainer 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | + languageChanged: Observable 33 | + lang: string 34 | + getTranslator(module: string): Translator 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Translator 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | + languageChanged: Observable 55 | + lang: string 56 | ± factory(module: string): function 57 | + translate(keys: string[], params?: any, lang?: string) 58 | + instant(keys: string[], params?: any, lang?: string) 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | TranslatorConfig 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | TranslationLoader 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | TranslationLoaderJson 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | TranslateComponent 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | TranslatePipe 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | TranslateLogHandler 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /docs/images/classes-1.4.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | TranslateService 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | + languageChanged: Observable 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | TranslateModule 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | TranslateConfig 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | + getLoaderConfig(module: string) 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | TranslateLoader 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | TranslateLoaderJson 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | Translator 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | TranslateComponent 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | TranslatePipe 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | TranslateLogHandler 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | --------------------------------------------------------------------------------