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: multi-http-loader #3

Open
wants to merge 1 commit 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
44 changes: 44 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,50 @@
}
}
},
"multi-http-loader": {
"projectType": "library",
"root": "projects/multi-http-loader",
"sourceRoot": "projects/multi-http-loader/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/multi-http-loader/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/multi-http-loader/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/multi-http-loader/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "./tsconfig.spec.json",
"karmaConfig": "./karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"projects/multi-http-loader/**/*.ts",
"projects/multi-http-loader/**/*.html"
],
"eslintConfig": "projects/multi-http-loader/eslint.config.js"
}
}
}
},
"test-app": {
"projectType": "application",
"schematics": {
Expand Down
8 changes: 8 additions & 0 deletions projects/multi-http-loader/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Copyright (c) 2018 Olivier Combe
Copyright (c) 2024 Andreas Löw / CodeAndWeb GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
116 changes: 116 additions & 0 deletions projects/multi-http-loader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# @codeandweb/multi-http-loader

A loader for [ngx-translate](https://github.com/ngx-translate/core) that loads translations using http.

[![npm version](https://img.shields.io/npm/v/ngx-translate-multi-http-loader.svg)](https://www.npmjs.com/package/ngx-translate-multi-http-loader) ![NPM](https://img.shields.io/npm/l/ngx-translate-multi-http-loader) ![npm bundle size](https://img.shields.io/bundlephobia/min/ngx-translate-multi-http-loader)
![npm](https://img.shields.io/npm/dm/ngx-translate-multi-http-loader)

Angular 14 example: https://stackblitz.com/edit/ngx-translate-multi-http-loader-sample-2clau3?file=src/app/app.module.ts

Angular 6 example: https://stackblitz.com/edit/ngx-translate-multi-http-loader-sample

Get the complete changelog here: https://github.com/rbalet/ngx-translate-multi-http-loader/releases

* [Installation](#installation)
* [Usage](#usage)
* [Error & BugFix](#possible-error--bugfix)

## breaking change: v9.0.0
* This library is now using `httpBackend` instead of the `httpClient`, to avoid being delayed by interceptor, which was creating errors while loading.
* From the v9, the library will only be using a list of `string[]` so `prefix` & `suffix` aren't needed anymore and `.json` gonna be the default suffix.

## Installation

We assume that you already installed [ngx-translate](https://github.com/ngx-translate/core).

Now you need to install the npm module for `MultiTranslateHttpLoader`:

```sh
npm install ngx-translate-multi-http-loader
```

Choose the version corresponding to your Angular version:

| Angular | @ngx-translate/core | ngx-translate-multi-http-loader |
| ------- | ------------------- | ------------------------------- |
| >= 16 | 15.x+ | >= 15.x+ |
| 15 | 14.x+ | 9.x+ |
| 14 | 14.x+ | 8.x+ |
| 13 | 14.x+ | 7.x+ |
| 6 | 10.x+ | 1.x+ |

## Usage
_The `MultiTranslateHttpLoader` uses HttpBackend to load translations, therefore :_
1. Create and export a new `HttpLoaderFactory` function
2. Import the `HttpClientModule` from `@angular/common/http`
3. Setup the `TranslateModule` to use the `MultiTranslateHttpLoader`

```typescript
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HttpClientModule, HttpBackend} from '@angular/common/http';
import {TranslateModule, TranslateLoader} from '@ngx-translate/core';
import {MultiTranslateHttpLoader} from 'ngx-translate-multi-http-loader';
import {AppComponent} from './app';

// AoT requires an exported function for factories
export function HttpLoaderFactory(_httpBackend: HttpBackend) {
return new MultiTranslateHttpLoader(_httpBackend, ['/assets/i18n/core/', '/assets/i18n/vendors/']); // /i18n/core/ on angular >= v18 with the new public logic
}

@NgModule({
imports: [
BrowserModule,
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpBackend]
}
})
],
bootstrap: [AppComponent]
})
export class AppModule { }
```

The `MultiTranslateHttpLoader` takes a list of `string[]` or `TranslationResource[]`.

### String[]
For example `['/assets/i18n/core/', '/assets/i18n/vendors/']`,
will load your translations files for the lang "en" from : `/assets/i18n/core/en.json` and `/assets/i18n/vendors/en.json`

### Custom suffix
**For now this loader only support the `json` format.**

Instead of an array of `string[]`,
you may pass a list of parameters:
- `prefix: string = '/assets/i18n/'`
- `suffix: string = '.json'`
- `optional: boolean = true`

```typescript
export function HttpLoaderFactory(_httpBackend: HttpBackend) {
return new MultiTranslateHttpLoader(_httpBackend, [
{prefix: './assets/i18n/core/', suffix: '.json'},
{prefix: './assets/i18n/vendors/'}, // , "suffix: '.json'" being the default value
{prefix: './assets/i18n/non-existent/', optional: true}, // Wont create any log
]);
}
```

The loader will merge all translation files from the server


## Possible error & Bugfix
### values.at is not a function
1. Install `core-js`
2. In `polyfills.ts`, add `import 'core-js/modules/es.array.at'`


## Authors and acknowledgment
* maintainer [Raphaël Balet](https://github.com/rbalet)
* Former maintainer [Dennis Keil](https://github.com/denniske)
*
[![BuyMeACoffee](https://www.buymeacoffee.com/assets/img/custom_images/purple_img.png)](https://www.buymeacoffee.com/widness)
32 changes: 32 additions & 0 deletions projects/multi-http-loader/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @ts-check
const tseslint = require("typescript-eslint");
const rootConfig = require("../../eslint.config.js");

module.exports = tseslint.config(
...rootConfig,
{
files: ["**/*.ts"],
rules: {
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "lib",
style: "camelCase",
},
],
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "lib",
style: "kebab-case",
},
],
},
},
{
files: ["**/*.html"],
rules: {},
}
);
7 changes: 7 additions & 0 deletions projects/multi-http-loader/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/multi-http-loader",
"lib": {
"entryFile": "src/public-api.ts"
}
}
29 changes: 29 additions & 0 deletions projects/multi-http-loader/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@codeandweb/multi-http-loader",
"version": "16.0.0",
"description": "http loader for dynamically loading translation files for @codeandweb/ngx-translate",
"keywords": [
"@ngx-translate",
"ngx-translate",
"@ngx-translate/http-loader",
"multi-http-loader",
"@codeandweb/ngx-translate"
],
"maintainers": [
"[email protected]"
],
"author": {
"name": "Raphaël Balet",
"email": "[email protected]"
},
"license": "MIT",
"dependencies": {
"tslib": "^2.6.3"
},
"peerDependencies": {
"@angular/common": ">=16",
"@angular/core": ">=16",
"@codeandweb/ngx-translate": ">=16"
},
"sideEffects": false
}
115 changes: 115 additions & 0 deletions projects/multi-http-loader/src/lib/http-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { HttpClient, provideHttpClient } from "@angular/common/http";
import {
HttpTestingController,
provideHttpClientTesting,
} from "@angular/common/http/testing";
import { TestBed } from "@angular/core/testing";
import {
provideTranslateService,
TranslateLoader,
TranslateService,
Translation,
} from "@codeandweb/ngx-translate";
import { MultiTranslateHttpLoader } from "../public-api";

describe("TranslateLoader", () => {
let translate: TranslateService;
let http: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
TranslateService,
provideHttpClient(),
provideHttpClientTesting(),
provideTranslateService({
loader: {
provide: TranslateLoader,
useFactory: (httpClient: HttpClient) =>
new MultiTranslateHttpLoader(httpClient),
deps: [HttpClient],
},
}),
],
});
translate = TestBed.inject(TranslateService);
http = TestBed.inject(HttpTestingController);
});

it("should be able to provide MultiTranslateHttpLoader", () => {
expect(MultiTranslateHttpLoader).toBeDefined();
expect(translate.currentLoader).toBeDefined();
expect(
translate.currentLoader instanceof MultiTranslateHttpLoader
).toBeTruthy();
});

it("should be able to get translations", () => {
translate.use("en");

// this will request the translation from the backend because we use a static files loader for TranslateService
translate.get("TEST").subscribe((res: Translation) => {
expect(res).toEqual("This is a test");
});

// mock response after the xhr request, otherwise it will be undefined
http.expectOne("/assets/i18n/en.json").flush({
TEST: "This is a test",
TEST2: "This is another test",
});

// this will request the translation from downloaded translations without making a request to the backend
translate.get("TEST2").subscribe((res: Translation) => {
expect(res).toEqual("This is another test");
});
});

it("should be able to reload a lang", () => {
translate.use("en");

// this will request the translation from the backend because we use a static files loader for TranslateService
translate.get("TEST").subscribe((res: Translation) => {
expect(res).toEqual("This is a test");

// reset the lang as if it was never initiated
translate.reloadLang("en").subscribe(() => {
expect(translate.instant("TEST")).toEqual("This is a test 2");
});

http
.expectOne("/assets/i18n/en.json")
.flush({ TEST: "This is a test 2" });
});

// mock response after the xhr request, otherwise it will be undefined
http.expectOne("/assets/i18n/en.json").flush({ TEST: "This is a test" });
});

it("should be able to reset a lang", (done: DoneFn) => {
translate.use("en");
spyOn(http, "expectOne").and.callThrough();

// this will request the translation from the backend because we use a static files loader for TranslateService
translate.get("TEST").subscribe((res: Translation) => {
expect(res).toEqual("This is a test");
expect(http.expectOne).toHaveBeenCalledTimes(1);

// reset the lang as if it was never initiated
translate.resetLang("en");

expect(translate.instant("TEST")).toEqual("TEST");

// use set timeout because no request is really made and we need to trigger zone to resolve the observable
setTimeout(() => {
translate.get("TEST").subscribe((res2: Translation) => {
expect(res2).toEqual("TEST"); // because the loader is "pristine" as if it was never called
expect(http.expectOne).toHaveBeenCalledTimes(1);
done();
});
}, 10);
});

// mock response after the xhr request, otherwise it will be undefined
http.expectOne("/assets/i18n/en.json").flush({ TEST: "This is a test" });
});
});
Loading