Skip to content
Merged
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
104 changes: 42 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
![npm](https://img.shields.io/npm/dw/@larscom/ngx-translate-module-loader)
[![license](https://img.shields.io/npm/l/@larscom/ngx-translate-module-loader.svg)](https://github.com/larscom/ngx-translate-module-loader/blob/main/LICENSE)


> Highly configurable and flexible translations loader for [@ngx-translate/core](https://github.com/ngx-translate/core). Fetch multiple translations (http only) and configure them to your needs. Each translation file has it's own **namespace** out of the box so the key/value pairs do not conflict with each other.

### ✨ [View on StackBlitz](https://stackblitz.com/edit/ngx-translate-module-loader)
Expand All @@ -21,52 +20,36 @@ npm i @larscom/ngx-translate-module-loader

## Usage

Create an exported `moduleHttpLoaderFactory` function
Import the `provideModuleTranslateLoader` function and provide the options.

```ts
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { HttpClientModule, HttpClient } from '@angular/common/http'
import { TranslateModule, TranslateLoader } from '@ngx-translate/core'
import { ModuleTranslateLoader, IModuleTranslationOptions } from '@larscom/ngx-translate-module-loader'
import { AppComponent } from './app'

export function moduleHttpLoaderFactory(http: HttpClient) {
const baseTranslateUrl = './assets/i18n'

const options: IModuleTranslationOptions = {
modules: [
// final url: ./assets/i18n/en.json
{ baseTranslateUrl },
// final url: ./assets/i18n/feature1/en.json
{ baseTranslateUrl, moduleName: 'feature1' },
// final url: ./assets/i18n/feature2/en.json
{ baseTranslateUrl, moduleName: 'feature2' }
]
}

return new ModuleTranslateLoader(http, options)
}

@NgModule({
imports: [
BrowserModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: moduleHttpLoaderFactory,
deps: [HttpClient]
}
})
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(readonly translate: TranslateService) {
translate.setDefaultLang('en')
}
}
import { provideHttpClient } from '@angular/common/http'
import { bootstrapApplication } from '@angular/platform-browser'
import { provideModuleTranslateLoader } from '@larscom/ngx-translate-module-loader'
import { provideTranslateService } from '@ngx-translate/core'
import { AppComponent } from './app/app.component'

const baseTranslateUrl = './assets/i18n'

bootstrapApplication(AppComponent, {
providers: [
provideTranslateService({
// use loader and provide options
loader: provideModuleTranslateLoader({
modules: [
// final url: ./assets/i18n/en.json
{ baseTranslateUrl },
// final url: ./assets/i18n/feature1/en.json
{ moduleName: 'feature1', baseTranslateUrl },
// final url: ./assets/i18n/feature2/en.json
{ moduleName: 'feature2', baseTranslateUrl }
]
})
}),
// http client is mandatory
provideHttpClient()
]
}).catch((err) => console.error(err))
```

## Namespacing
Expand All @@ -76,20 +59,17 @@ By default, each translation file gets it's own namespace based on the `moduleNa
For example with these options:

```ts
export function moduleHttpLoaderFactory(http: HttpClient) {
const baseTranslateUrl = './assets/i18n'
const baseTranslateUrl = './assets/i18n'

const options: IModuleTranslationOptions = {
modules: [
// no moduleName/namespace
{ baseTranslateUrl },
// namespace: FEATURE1
{ baseTranslateUrl, moduleName: 'feature1' },
// namespace: FEATURE2
{ baseTranslateUrl, moduleName: 'feature2' }
]
}
return new ModuleTranslateLoader(http, options)
const options: IModuleTranslationOptions = {
modules: [
// no moduleName/namespace
{ baseTranslateUrl },
// namespace: FEATURE1
{ baseTranslateUrl, moduleName: 'feature1' },
// namespace: FEATURE2
{ baseTranslateUrl, moduleName: 'feature2' }
]
}
```

Expand Down Expand Up @@ -122,7 +102,7 @@ Even though all JSON files from those modules are the same, they don't conflict
The configuration is very flexible, you can even define custom templates for fetching translations.

```ts
interface IModuleTranslationOptions {
export interface IModuleTranslationOptions {
/**
* The translation module configurations
*/
Expand Down Expand Up @@ -155,7 +135,7 @@ interface IModuleTranslationOptions {
* Custom translate merge function after retrieving all translation files
* @param translations the resolved translation files
*/
translateMerger?: (translations: Translation[]) => Translation
translateMerger?: (translations: TranslationObject[]) => TranslationObject
/**
* Provide custom headers at 'root' level, which means this headers gets added to every request
* unless you specify headers at 'module' level.
Expand All @@ -166,7 +146,7 @@ interface IModuleTranslationOptions {
```

```ts
interface IModuleTranslation {
export interface IModuleTranslation {
/**
* The module name
*
Expand Down Expand Up @@ -194,7 +174,7 @@ interface IModuleTranslation {
* Custom translation map function after retrieving a translation file
* @param translation the resolved translation file
*/
translateMap?: (translation: Translation) => Translation
translateMap?: (translation: TranslationObject) => TranslationObject
/**
* Custom path template for fetching translations
* @example
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@angular/platform-browser": "20.1.3",
"@angular/platform-browser-dynamic": "20.1.3",
"@angular/router": "20.1.3",
"@ngx-translate/core": "16.0.4",
"@ngx-translate/core": "17.0.0",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"zone.js": "0.15.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@an
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { TestBed } from '@angular/core/testing'
import { BrowserTestingModule } from '@angular/platform-browser/testing'
import { Translation } from 'projects/ngx-translate-module-loader/src/public-api'
import { TranslationObject } from '@ngx-translate/core'
import { ModuleTranslateLoader } from './module-translate-loader'
import { IModuleTranslationOptions } from './module-translation-options'
import { TranslationKey } from './translation'

const translation: Translation = {
const translation: TranslationObject = {
key: 'value',
key1: 'value1',
parent: {
Expand All @@ -18,7 +17,7 @@ const translation: Translation = {
}
}

const completeTranslation: Translation = {
const completeTranslation: TranslationObject = {
feature1: {
key1: 'feature1_value1',
key2: 'feature1_value2',
Expand Down Expand Up @@ -175,7 +174,7 @@ describe('ModuleTranslateLoader', () => {
it('should load the english translation from different modules with a custom translateMerger', (done) => {
const options: IModuleTranslationOptions = {
...defaultOptions,
translateMerger: (translations: Translation[]) => {
translateMerger: (translations: TranslationObject[]) => {
return translations.reduce((acc, curr) => ({ ...acc, ...curr }), Object())
}
}
Expand Down Expand Up @@ -220,7 +219,7 @@ describe('ModuleTranslateLoader', () => {
const mock = createTestRequest(getTranslatePath(baseTranslateUrl, moduleName!, language))
expect(mock.request.method).toEqual('GET')
const response = moduleName ? completeTranslation[moduleName as keyof Object] : translation
mock.flush(response)
mock.flush(Object(response))
})
})
it('should load the english translation from different modules with a custom translateMap', (done) => {
Expand All @@ -230,9 +229,8 @@ describe('ModuleTranslateLoader', () => {
{
moduleName: undefined,
baseTranslateUrl: './assets/i18n',
translateMap: (translation: Translation) => {
translateMap: (translation: TranslationObject) => {
return Object.keys(translation)
.map((key) => key as TranslationKey)
.reduce((acc, curr) => {
return {
...acc,
Expand All @@ -244,9 +242,8 @@ describe('ModuleTranslateLoader', () => {
{
moduleName: 'feature1',
baseTranslateUrl: './assets/i18n',
translateMap: (translation: Translation) => {
translateMap: (translation: TranslationObject) => {
return Object.keys(translation)
.map((key) => key as TranslationKey)
.reduce((acc, curr) => {
return {
...acc,
Expand Down Expand Up @@ -284,7 +281,7 @@ describe('ModuleTranslateLoader', () => {
const mock = createTestRequest(getTranslatePath(baseTranslateUrl, moduleName!, language))
expect(mock.request.method).toEqual('GET')
const response = moduleName ? completeTranslation[moduleName as keyof Object] : translation
mock.flush(response)
mock.flush(Object(response))
})
})

Expand Down Expand Up @@ -323,7 +320,7 @@ describe('ModuleTranslateLoader', () => {
const mock = createTestRequest(getTranslatePath(baseTranslateUrl, moduleName!, language))
expect(mock.request.method).toEqual('GET')
const response = moduleName ? completeTranslation[moduleName as keyof Object] : translation
mock.flush(response)
mock.flush(Object(response))
})
})

Expand Down Expand Up @@ -362,7 +359,7 @@ describe('ModuleTranslateLoader', () => {
const mock = createTestRequest(getTranslatePath(baseTranslateUrl, moduleName!, language))
expect(mock.request.method).toEqual('GET')
const response = moduleName ? completeTranslation[moduleName as keyof Object] : translation
mock.flush(response)
mock.flush(Object(response))
})
})

Expand Down Expand Up @@ -406,7 +403,7 @@ describe('ModuleTranslateLoader', () => {

const response = moduleName ? completeTranslation[moduleName as keyof Object] : translation

mock.flush(response, {
mock.flush(Object(response), {
status: moduleName == null ? 404 : 200,
statusText: moduleName == null ? 'not found' : 'ok'
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { HttpClient } from '@angular/common/http'
import { TranslateLoader, mergeDeep } from '@ngx-translate/core'
import { TranslateLoader, TranslationObject, mergeDeep } from '@ngx-translate/core'
import { forkJoin as ForkJoin, MonoTypeOperatorFunction, Observable, of } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { IModuleTranslation } from './module-translation'
import { IModuleTranslationOptions } from './module-translation-options'
import { Translation } from './translation'

const concatJson = (path: string) => path.concat('.json')

Expand Down Expand Up @@ -32,15 +31,15 @@ export class ModuleTranslateLoader implements TranslateLoader {
*/
constructor(private readonly http: HttpClient, private readonly options: IModuleTranslationOptions) {}

public getTranslation(language: string): Observable<Translation> {
public getTranslation(language: string): Observable<TranslationObject> {
const { defaultOptions: options } = this
return this.mergeTranslations(this.getModuleTranslations(language, options), options)
}

private mergeTranslations(
moduleTranslations: Observable<Translation>[],
moduleTranslations: Observable<TranslationObject>[],
{ deepMerge, translateMerger }: IModuleTranslationOptions
): Observable<Translation> {
): Observable<TranslationObject> {
return ForkJoin(moduleTranslations).pipe(
map((translations) => {
return translateMerger
Expand All @@ -50,7 +49,7 @@ export class ModuleTranslateLoader implements TranslateLoader {
)
}

private getModuleTranslations(language: string, options: IModuleTranslationOptions): Observable<Translation>[] {
private getModuleTranslations(language: string, options: IModuleTranslationOptions): Observable<TranslationObject>[] {
const { modules } = options

return modules.map((module) => {
Expand All @@ -65,7 +64,7 @@ export class ModuleTranslateLoader implements TranslateLoader {
language: string,
{ translateError, version, headers }: IModuleTranslationOptions,
{ pathTemplate, baseTranslateUrl, translateMap }: IModuleTranslation
): Observable<Translation> {
): Observable<TranslationObject> {
const pathOptions = Object({ baseTranslateUrl, language })
const template = pathTemplate || DEFAULT_PATH_TEMPLATE

Expand All @@ -75,7 +74,7 @@ export class ModuleTranslateLoader implements TranslateLoader {

const path = version ? `${cleanedPath}?v=${version}` : cleanedPath

return this.http.get<Translation>(path, { headers }).pipe(
return this.http.get<TranslationObject>(path, { headers }).pipe(
map((translation) => (translateMap ? translateMap(translation) : translation)),
this.catchError(cleanedPath, translateError)
)
Expand All @@ -85,7 +84,7 @@ export class ModuleTranslateLoader implements TranslateLoader {
language: string,
{ disableNamespace, lowercaseNamespace, translateError, version, headers }: IModuleTranslationOptions,
{ pathTemplate, baseTranslateUrl, moduleName, namespace, translateMap, headers: moduleHeaders }: IModuleTranslation
): Observable<Translation> {
): Observable<TranslationObject> {
const pathOptions = Object({ baseTranslateUrl, moduleName, language })
const template = pathTemplate || DEFAULT_PATH_TEMPLATE

Expand All @@ -101,7 +100,7 @@ export class ModuleTranslateLoader implements TranslateLoader {

const path = version ? `${cleanedPath}?v=${version}` : cleanedPath

return this.http.get<Translation>(path, { headers: moduleHeaders || headers }).pipe(
return this.http.get<TranslationObject>(path, { headers: moduleHeaders || headers }).pipe(
map((translation) => {
return translateMap
? translateMap(translation)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HttpClient } from '@angular/common/http'
import { InjectionToken, Provider } from '@angular/core'
import { TranslateLoader } from '@ngx-translate/core'
import { ModuleTranslateLoader } from './module-translate-loader'
import { IModuleTranslationOptions } from './module-translation-options'

export const MODULE_TRANSLATE_LOADER_CONFIG = new InjectionToken<IModuleTranslationOptions>(
'MODULE_TRANSLATE_LOADER_CONFIG'
)

export function provideModuleTranslateLoader(options: IModuleTranslationOptions): Provider[] {
return [
{
provide: MODULE_TRANSLATE_LOADER_CONFIG,
useValue: options
},
{
provide: TranslateLoader,
useClass: ModuleTranslateLoader,
deps: [HttpClient, MODULE_TRANSLATE_LOADER_CONFIG]
}
]
}
Loading