Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat): Namespace support #1284

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 119 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,38 @@ Simple example using ngx-translate: https://stackblitz.com/github/ngx-translate/
Get the complete changelog here: https://github.com/ngx-translate/core/releases

## Table of Contents
* [Installation](#installation)
* [Usage](#usage)
* [Import the TranslateModule](#1-import-the-translatemodule)
* [SharedModule](#sharedmodule)
* [Lazy loaded modules](#lazy-loaded-modules)
* [Configuration](#configuration)
* [AoT](#aot)
* [Define the default language for the application](#2-define-the-default-language-for-the-application)
* [Init the TranslateService for your application](#3-init-the-translateservice-for-your-application)
* [Define the translations](#4-define-the-translations)
* [Use the service, the pipe or the directive](#5-use-the-service-the-pipe-or-the-directive)
* [Use HTML tags](#6-use-html-tags)
* [API](#api)
* [TranslateService](#translateservice)
* [Properties](#properties)
* [Methods](#methods)
* [Write & use your own loader](#write--use-your-own-loader)
* [Example](#example)
* [How to use a compiler to preprocess translation values](#how-to-use-a-compiler-to-preprocess-translation-values)
* [How to handle missing translations](#how-to-handle-missing-translations)
* [Example](#example-1)
* [Parser](#parser)
* [Methods](#methods)
* [FAQ](#faq)
* [I'm getting an error `npm ERR! peerinvalid Peer [...]`](#im-getting-an-error-npm-err-peerinvalid-peer-)
* [Plugins](#plugins)
* [Editors](#editors)
* [Additional Framework Support](#additional-framework-support)
- [Installation](#installation)
- [Usage](#usage)
- [1. Import the `TranslateModule`:](#1-import-the-translatemodule)
- [SharedModule](#sharedmodule)
- [Lazy loaded modules](#lazy-loaded-modules)
- [Configuration](#configuration)
- [AoT](#aot)
- [2. Define the `default language` for the application](#2-define-the-default-language-for-the-application)
- [3. Init the `TranslateService` for your application:](#3-init-the-translateservice-for-your-application)
- [4. Define the translations:](#4-define-the-translations)
- [5. Use the service, the pipe or the directive:](#5-use-the-service-the-pipe-or-the-directive)
- [6. Use HTML tags:](#6-use-html-tags)
- [7. Use the namespace-translate service, pipe or directive:](#7-use-the-namespace-translate-service-pipe-or-directive)
- [API](#api)
- [TranslateService](#translateservice)
- [Properties:](#properties)
- [Methods:](#methods)
- [Write & use your own loader](#write--use-your-own-loader)
- [Example](#example)
- [How to use a compiler to preprocess translation values](#how-to-use-a-compiler-to-preprocess-translation-values)
- [How to handle missing translations](#how-to-handle-missing-translations)
- [Example:](#example-1)
- [Parser](#parser)
- [Methods:](#methods-1)
- [FAQ](#faq)
- [I'm getting an error `npm ERR! peerinvalid Peer [...]`](#im-getting-an-error-npm-err-peerinvalid-peer-)
- [I want to hot reload the translations in my application but `reloadLang` does not work](#i-want-to-hot-reload-the-translations-in-my-application-but-reloadlang-does-not-work)
- [Plugins](#plugins)
- [Editors](#editors)
- [Extensions](#extensions)
- [VScode](#vscode)
- [Additional Framework Support](#additional-framework-support)


## Installation
Expand All @@ -47,16 +51,16 @@ npm install @ngx-translate/core --save

Choose the version corresponding to your Angular version:

Angular | @ngx-translate/core | @ngx-translate/http-loader
----------- | ------------------- | --------------------------
10 | 13.x+ | 6.x+
9 | 12.x+ | 5.x+
8 | 12.x+ | 4.x+
7 | 11.x+ | 4.x+
6 | 10.x | 3.x
5 | 8.x to 9.x | 1.x to 2.x
4.3 | 7.x or less | 1.x to 2.x
2 to 4.2.x | 7.x or less | 0.x
| Angular | @ngx-translate/core | @ngx-translate/http-loader |
| ---------- | ------------------- | -------------------------- |
| 10 | 13.x+ | 6.x+ |
| 9 | 12.x+ | 5.x+ |
| 8 | 12.x+ | 4.x+ |
| 7 | 11.x+ | 4.x+ |
| 6 | 10.x | 3.x |
| 5 | 8.x to 9.x | 1.x to 2.x |
| 4.3 | 7.x or less | 1.x to 2.x |
| 2 to 4.2.x | 7.x or less | 0.x |

---

Expand Down Expand Up @@ -363,6 +367,70 @@ To render them, simply use the `innerHTML` attribute with the pipe on any elemen
<div [innerHTML]="'HELLO' | translate"></div>
```

#### 7. Use the namespace-translate service, pipe or directive:

If you have a big complex application it can be tedious to provide the sometimes pretty long path to the translations for the specific component.
For this reason we provide a `NamespaceTranslateService`, `NamespaceTranslatePipe` and `NamespaceTranslateDirective`.

> The NamespaceTranslateService only provides functions to get translations for a specific key(s). It does not provide functions to set translations, language or do anything else. For all other things than getting the translations continue using the TranslateService!

The NamespaceTranslateService is not provided via the TranslateModule!

To use the NamespaceTranslateService, Pipe or Module you have to provide a `TRANSLATION_NAMESPACE` AND the `NamespaceTranslateService` in your component or directive.

<b>Read this if you don't know the specifics how Angular DI works:</b>

If you provide the `TRANSLATION_NAMESPACE` and the `NamespaceTranslateService` in a module, every component and directive declared by the module will share the namespace, unless you provide the booth the `TRANSLATION_NAMESPACE` AND `NamespaceTranslateService` in the `providers` section.

> Also be aware of the fact, that all the child components/directives of a component/directive have access to things provided in the `providers` section of a parent component/directive!

So if you don't provide booth the `TRANSLATION_NAMESPACE` and the `NamespaceTranslateService` in the providers section you will share either the instance of the `NamespaceTranslateService` or the `TRANSLATION_NAMESPACE` with the parent component/directive.

<b> Example usage:

```ts
import {Component} from '@angular/core';
import {NamespaceTranslateServiceNamespaceTranslateProvider} from '@ngx-translate/core';

@Component({
selector: 'my-deep-nested-component',
template: `
<div>{{ 'HELLO' | namespaceTranslate:param }}</div>
<div namespace-translate [translateParams]="{value: 'world'}" >Hello</div>
`,
providers: [{ provide: TRANSLATION_NAMESPACE, useValue: "PATH.TO.MY.DEEP.NESTED.COMPONENT"}, NamespaceTranslateService]
})
export class MyDeepNestedComponent {
param = {value: 'world'};

constructor(namespaceTranslate: NamespaceTranslateService) {
const instant = namespaceTranslate.instant("HELLO", param);

const observable = namespaceTranslate.get("HELLO", param);
}
}
```

The translation object for the example above would look like the following:

```json
{
"PATH":{
"TO":{
"MY":{
"DEEP":{
"NESTED":{
"COMPONENT":{
"HELLO": "hello {{value}}"
}
}
}
}
}
}
}
```

## API

### TranslateService
Expand Down Expand Up @@ -494,6 +562,19 @@ Setup the Missing Translation Handler in your module import by adding it to the
export class AppModule { }
```

### NamespaceTranslateService

#### Properties:
- `private readonly namespace: string`: The namespace with which all keys given to one of the function of this instance should be prefixed.
- `private readonly translate: TranslateService`: The global instance of the [`TranslateService`](###TranslateService)

#### Methods:

- `get(key: string|Array<string>, interpolateParams?: Object): Observable<string|Object>`: Gets the translated value of a key (or an array of keys) or the key if the value was not found
- `getStreamOnTranslationChange(key: string|Array<string>, interpolateParams?: Object): Observable<string|Object>`: Returns a stream of translated values of a key (or an array of keys) or the key if the value was not found. Without any `onTranslationChange` events this returns the same value as `get` but it will also emit new values whenever the translation changes.
- `stream(key: string|Array<string>, interpolateParams?: Object): Observable<string|Object>`: Returns a stream of translated values of a key (or an array of keys) or the key if the value was not found. Without any `onLangChange` events this returns the same value as `get` but it will also emit new values whenever the used language changes.
- `instant(key: string|Array<string>, interpolateParams?: Object): string|Object`: Gets the instant translated value of a key (or an array of keys). /!\ This method is **synchronous** and the default file loader is asynchronous. You are responsible for knowing when your translations have been loaded and it is safe to use this method. If you are not sure then you should use the `get` method instead.

### Parser

If you need it for some reason, you can use the `TranslateParser` service.
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
"@angular/cli": "10.0.0",
"@angular/compiler-cli": "10.0.0",
"@angular/language-service": "10.0.0",
"@types/flat": "^5.0.1",
"@types/jasmine": "^3.3.13",
"@types/jasminewd2": "^2.0.6",
"@types/node": "^12.0.10",
"codelyzer": "^5.2.2",
"commitizen": "^3.1.1",
"cz-conventional-changelog": "^2.1.0",
"flat": "^5.0.2",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "^4.2.1",
"karma": "^4.1.0",
Expand Down
165 changes: 165 additions & 0 deletions projects/ngx-translate/core/src/lib/base-translate.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { AfterViewChecked, ChangeDetectorRef, ElementRef, Input, OnDestroy } from '@angular/core';
import { Subscription, isObservable, Observable } from 'rxjs';
import { DefaultLangChangeEvent, LangChangeEvent, TranslateService, TranslationChangeEvent } from './translate.service';
import { equals, isDefined } from './util';

export abstract class BaseTranslateDirective implements AfterViewChecked, OnDestroy {

abstract getParsedResult(translations: any, key: string | Array<string>, interpolateParams?: Object): any;
abstract get(key: string | Array<string>, interpolateParams?: Object): Observable<any>;

key: string;
lastParams: any;
currentParams: any;
onLangChangeSub: Subscription;
onDefaultLangChangeSub: Subscription;
onTranslationChangeSub: Subscription;

constructor(protected translateService: TranslateService, private element: ElementRef, private _ref: ChangeDetectorRef) {
// subscribe to onTranslationChange event, in case the translations of the current lang change
if (!this.onTranslationChangeSub) {
this.onTranslationChangeSub = this.translateService.onTranslationChange.subscribe((event: TranslationChangeEvent) => {
if (event.lang === this.translateService.currentLang) {
this.checkNodes(true, event.translations);
}
});
}

// subscribe to onLangChange event, in case the language changes
if (!this.onLangChangeSub) {
this.onLangChangeSub = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.checkNodes(true, event.translations);
});
}

// subscribe to onDefaultLangChange event, in case the default language changes
if (!this.onDefaultLangChangeSub) {
this.onDefaultLangChangeSub = this.translateService.onDefaultLangChange.subscribe((event: DefaultLangChangeEvent) => {
this.checkNodes(true);
});
}
}

setTranslate(key: string) {
if (key) {
this.key = key;
this.checkNodes();
}
}

setTranslateParams(params: any) {
if (!equals(this.currentParams, params)) {
this.currentParams = params;
this.checkNodes(true);
}
}

ngAfterViewChecked() {
this.checkNodes();
}

checkNodes(forceUpdate = false, translations?: any) {
let nodes: NodeList = this.element.nativeElement.childNodes;
// if the element is empty
if (!nodes.length) {
// we add the key as content
this.setContent(this.element.nativeElement, this.key);
nodes = this.element.nativeElement.childNodes;
}
for (let i = 0; i < nodes.length; ++i) {
let node: any = nodes[i];
if (node.nodeType === 3) { // node type 3 is a text node
let key: string;
if (forceUpdate) {
node.lastKey = null;
}
if (isDefined(node.lookupKey)) {
key = node.lookupKey;
} else if (this.key) {
key = this.key;
} else {
let content = this.getContent(node);
let trimmedContent = content.trim();
if (trimmedContent.length) {
node.lookupKey = trimmedContent;
// we want to use the content as a key, not the translation value
if (content !== node.currentValue) {
key = trimmedContent;
// the content was changed from the user, we'll use it as a reference if needed
node.originalContent = content || node.originalContent;
} else if (node.originalContent) { // the content seems ok, but the lang has changed
// the current content is the translation, not the key, use the last real content as key
key = node.originalContent.trim();
} else if (content !== node.currentValue) {
// we want to use the content as a key, not the translation value
key = trimmedContent;
// the content was changed from the user, we'll use it as a reference if needed
node.originalContent = content || node.originalContent;
}
}
}
this.updateValue(key, node, translations);
}
}
}

updateValue(key: string, node: any, translations: any) {
if (key) {
if (node.lastKey === key && this.lastParams === this.currentParams) {
return;
}

this.lastParams = this.currentParams;

let onTranslation = (res: string) => {
if (res !== key) {
node.lastKey = key;
}
if (!node.originalContent) {
node.originalContent = this.getContent(node);
}
node.currentValue = isDefined(res) ? res : (node.originalContent || key);
// we replace in the original content to preserve spaces that we might have trimmed
this.setContent(node, this.key ? node.currentValue : node.originalContent.replace(key, node.currentValue));
this._ref.markForCheck();
};

if (isDefined(translations)) {
let res = this.getParsedResult(translations, key, this.currentParams);
if (isObservable(res)) {
res.subscribe(onTranslation);
} else {
onTranslation(res);
}
} else {
this.get(key, this.currentParams).subscribe(onTranslation);
}
}
}

getContent(node: any): string {
return isDefined(node.textContent) ? node.textContent : node.data;
}

setContent(node: any, content: string): void {
if (isDefined(node.textContent)) {
node.textContent = content;
} else {
node.data = content;
}
}

ngOnDestroy() {
if (this.onLangChangeSub) {
this.onLangChangeSub.unsubscribe();
}

if (this.onDefaultLangChangeSub) {
this.onDefaultLangChangeSub.unsubscribe();
}

if (this.onTranslationChangeSub) {
this.onTranslationChangeSub.unsubscribe();
}
}
}
Loading