diff --git a/package.json b/package.json index 6445287de..1508dbd06 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@po-ui/style": "2.3.0", "core-js": "3.6.4", "custom-idle-queue": "2.1.2", + "deepmerge": "^4.2.2", "http-status-codes": "^1.4.0", "localforage": "1.4.0", "lokijs": "1.5.8", diff --git a/projects/ui/ng-package.json b/projects/ui/ng-package.json index 7b99a596e..74df86e2c 100644 --- a/projects/ui/ng-package.json +++ b/projects/ui/ng-package.json @@ -2,7 +2,11 @@ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../dist/ng-components", "lib": { - "entryFile": "./src/public-api.ts" + "entryFile": "./src/public-api.ts", + "umdModuleIds": { + "deepmerge": "deepmerge", + "uuid": "uuid" + } }, - "whitelistedNonPeerDependencies": ["@po-ui/style", "@po-ui/ng-schematics"] + "whitelistedNonPeerDependencies": ["@po-ui/style", "@po-ui/ng-schematics", "deepmerge", "uuid"] } diff --git a/projects/ui/src/lib/services/po-i18n/index.ts b/projects/ui/src/lib/services/po-i18n/index.ts index 90c9aee36..657185864 100644 --- a/projects/ui/src/lib/services/po-i18n/index.ts +++ b/projects/ui/src/lib/services/po-i18n/index.ts @@ -1,4 +1,5 @@ export * from './interfaces/po-i18n-config.interface'; +export * from './interfaces/po-i18n-config-context.interface'; export * from './interfaces/po-i18n-config-default.interface'; export * from './interfaces/po-i18n-literals.interface'; export * from './po-i18n.pipe'; diff --git a/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config-context.interface.ts b/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config-context.interface.ts new file mode 100644 index 000000000..e8f5184c3 --- /dev/null +++ b/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config-context.interface.ts @@ -0,0 +1,12 @@ +/** + * @description + * + * + * + * Interface para a configuração dos contextos do módulo `PoI18nModule`. + * + * @usedBy PoI18nModule + */ +export interface PoI18nConfigContext { + [name: string]: { [language: string]: { [literal: string]: string } } | { url: string }; +} diff --git a/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config.interface.ts b/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config.interface.ts index bda96e402..6b13490bc 100644 --- a/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config.interface.ts +++ b/projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config.interface.ts @@ -1,3 +1,4 @@ +import { PoI18nConfigContext } from './po-i18n-config-context.interface'; import { PoI18nConfigDefault } from './po-i18n-config-default.interface'; /** @@ -10,7 +11,9 @@ import { PoI18nConfigDefault } from './po-i18n-config-default.interface'; * @usedBy PoI18nModule */ export interface PoI18nConfig { - /** Configurações padrões. */ + /** + * Configurações padrões. + */ default?: PoI18nConfigDefault; /** @@ -20,30 +23,34 @@ export interface PoI18nConfig { * * Portanto podemos utilizar constantes, onde devemos informar o nome do contexto recebendo um objeto com os * idiomas suportados e o arquivo de literais, por exemplo: - * ``` - * import { generalEn } from './i18n/general-en'; - * import { generalPt } from './i18n/general-pt'; + * + * ```typescript + * import { generalEn } from './i18n/general-en'; + * import { generalPt } from './i18n/general-pt'; + * * ... - * general: { - * pt: generalPt, - * en: generalEn - * } + * general: { + * pt: generalPt, + * en: generalEn + * } * ... * ``` * * E como informado, podemos utilizar a propriedade `url` que deve receber a URL do serviço que * retorne as literais traduzidas, por exemplo: - * ``` - * hcm: { - * url: 'http://localhost:3000/api/translations/hcm/' - * } + * + * ```typescript + * hcm: { + * url: 'http://localhost:3000/api/translations/hcm/' + * } * ``` * * Ao optar por utilizar um serviço, deverá ser definida a URL específica do contexto, * como nos exemplos abaixo: + * * ``` - * http://server:port/api/translations/crm - * http://server:port/api/translations/general + * http://server:port/api/translations/crm + * http://server:port/api/translations/general * ``` * * Os idiomas e literais serão automaticamente buscados com parâmetros na própria URL: @@ -53,9 +60,10 @@ export interface PoI18nConfig { * serviço deve retornar todas as literais do idioma. * * Exemplos de requisição: + * * ``` - * http://server:port/api/translations/crm?language=pt-br - * http://server:port/api/translations/crm?language=pt-br&literals=add,remove,text + * http://server:port/api/translations/crm?language=pt-br + * http://server:port/api/translations/crm?language=pt-br&literals=add,remove,text * ``` * * > Sempre que o idioma solicitado não for encontrado, será buscado por `pt-br`. @@ -63,18 +71,19 @@ export interface PoI18nConfig { * Existe também a possibilidade de utilizar ambos, onde será feito a busca das literais nas constantes e depois efetua * a busca no serviço, com isso as constantes podem servir como *backup* caso o serviço esteja indisponível, por exemplo: * - * ``` - * import { generalEn } from './i18n/general-en'; - * import { generalPt } from './i18n/general-pt'; + * ```typescript + * import { generalEn } from './i18n/general-en'; + * import { generalPt } from './i18n/general-pt'; + * * ... - * general: { - * pt: generalPt, - * en: generalEn, - * url: 'http://localhost:3000/api/translations/hcm/' - * } + * general: { + * pt: generalPt, + * en: generalEn, + * url: 'http://localhost:3000/api/translations/hcm/' + * } * ... * ``` * > Caso a constante contenha alguma literal que o serviço não possua será utilizado a literal da constante. */ - contexts: object; + contexts: PoI18nConfigContext; } diff --git a/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts b/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts index 2db7ca14d..1c9e9d4e1 100644 --- a/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts +++ b/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts @@ -1,13 +1,33 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HttpRequest } from '@angular/common/http'; - +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { NgModule } from '@angular/core'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { of } from 'rxjs'; import * as utils from '../../utils/util'; - import { PoI18nModule, PoI18nService } from '../po-i18n'; import { PoLanguageModule } from '../po-language'; +import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; + +const lazyConfig: PoI18nConfig = { + contexts: { + general: { + 'pt-br': { + insert: 'insert' + } + }, + special: { + 'pt-br': { + delete: 'delete' + } + } + } +}; + +@NgModule({ + imports: [PoI18nModule.forChild(lazyConfig)] +}) +class LazyModule {} describe('PoI18nService:', () => { describe('without Service:', () => { @@ -43,7 +63,7 @@ describe('PoI18nService:', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, PoLanguageModule, PoI18nModule.config(config)] + imports: [HttpClientTestingModule, LazyModule, PoLanguageModule, PoI18nModule.forRoot(config)] }); service = TestBed.inject(PoI18nService); @@ -117,6 +137,22 @@ describe('PoI18nService:', () => { }); }); + it('should return literal merged from context added in a "lazy module"', done => { + service.getLiterals({ context: 'general', language: 'pt-br' }).subscribe(literals => { + expect(literals['insert']).toBeTruthy(); + expect(literals['insert']).toBe(lazyConfig.contexts['general']['pt-br']['insert']); + done(); + }); + }); + + it('should return literal from context added in a "lazy module"', done => { + service.getLiterals({ context: 'special', language: 'pt-br' }).subscribe(literals => { + expect(literals['delete']).toBeTruthy(); + expect(literals['delete']).toBe(lazyConfig.contexts['special']['pt-br']['delete']); + done(); + }); + }); + describe('Methods:', () => { it('getLanguage: should call `languageService.getLanguage`.', () => { const languageServiceSpy = spyOn(service['languageService'], 'getLanguage'); diff --git a/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts b/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts index 1d36088b4..ded2d0aad 100644 --- a/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts +++ b/projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts @@ -1,14 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Inject } from '@angular/core'; - import { Observable } from 'rxjs'; import { isLanguage, reloadCurrentPage } from '../../utils/util'; import { PoLanguageService } from '../po-language/po-language.service'; - -import { I18N_CONFIG } from './po-i18n-config-injection-token'; import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; import { PoI18nLiterals } from './interfaces/po-i18n-literals.interface'; +import { I18N_CONFIG } from './po-i18n-config-injection-token'; /** * @description @@ -256,7 +254,7 @@ export class PoI18nBaseService { const context = options['context'] ? options['context'] : this.contextDefault; const literals: Array = options['literals'] ? options['literals'] : []; - return new Observable(observer => { + return new Observable(observer => { if (this.servicesContext[context]) { // Faz o processo de busca de um contexto que contém serviço this.getLiteralsFromContextService(language, context, literals, observer); diff --git a/projects/ui/src/lib/services/po-i18n/po-i18n-config-injection-token.ts b/projects/ui/src/lib/services/po-i18n/po-i18n-config-injection-token.ts index f0e5e57a9..8a64af3fa 100644 --- a/projects/ui/src/lib/services/po-i18n/po-i18n-config-injection-token.ts +++ b/projects/ui/src/lib/services/po-i18n/po-i18n-config-injection-token.ts @@ -2,4 +2,4 @@ import { InjectionToken } from '@angular/core'; import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; -export const I18N_CONFIG = new InjectionToken('I18N_CONFIG'); +export const I18N_CONFIG = new InjectionToken>('I18N_CONFIG'); diff --git a/projects/ui/src/lib/services/po-i18n/po-i18n.module.ts b/projects/ui/src/lib/services/po-i18n/po-i18n.module.ts index f8e74a02e..fe59e05cc 100644 --- a/projects/ui/src/lib/services/po-i18n/po-i18n.module.ts +++ b/projects/ui/src/lib/services/po-i18n/po-i18n.module.ts @@ -1,12 +1,11 @@ import { HttpClient, HttpClientModule } from '@angular/common/http'; -import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; +import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core'; +import { PoLanguageModule } from '../po-language/po-language.module'; import { PoLanguageService } from './../po-language/po-language.service'; - -import { I18N_CONFIG } from './po-i18n-config-injection-token'; -import { returnPoI18nService, PoI18nService } from './po-i18n.service'; import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; -import { PoLanguageModule } from '../po-language/po-language.module'; +import { I18N_CONFIG } from './po-i18n-config-injection-token'; +import { PoI18nService, returnPoI18nService } from './po-i18n.service'; /** * @description @@ -14,12 +13,15 @@ import { PoLanguageModule } from '../po-language/po-language.module'; * Módulo do serviço `PoI18nService` para controle de idiomas com PO. * * Para utilização do serviço de idiomas `PoI18nService`, deve-se importar este módulo mesmo já havendo importado - * o módulo `PoModule`. Na importação deve ser invocado o método `config`, informando um objeto que deve implementar + * o módulo `PoModule`. + * + * Na importação deve ser invocado o método `config`, informando um objeto que deve implementar * a interface [`PoI18nConfig`](documentation/po-i18n#poI18nConfig) para configuração. * * * **Exemplo de configuração do módulo do i18n:** - * ``` + * + * ```typescript * import { PoI18nConfig } from '@po-ui/ng-components'; * * import { generalEn } from './i18n/general-en'; @@ -56,27 +58,30 @@ import { PoLanguageModule } from '../po-language/po-language.module'; * de um objeto. Exemplo: * * Arquivo general-pt.ts - * ``` + * + * ```typescript * export const generalPt = { - * add: 'Adicionar', - * greeting: 'Prazer, {0} {1}', - * people: '{0} Pessoas, - * remove: 'Remover' + * add: 'Adicionar', + * greeting: 'Prazer, {0} {1}', + * people: '{0} Pessoas, + * remove: 'Remover' * } * ``` * * Arquivo general-en.ts - * ``` + * + * ```typescript * export const generalEn = { - * add: 'Add', - * greeting: 'Nice to meet you, {0} {1}', - * people: '{0} People, - * remove: 'Remove' + * add: 'Add', + * greeting: 'Nice to meet you, {0} {1}', + * people: '{0} People, + * remove: 'Remove' * } * ``` * * **Exemplo de configuração de contextos usando constantes externas:** - * ``` + * + * ```typescript * import { PoI18nConfig } from '@po-ui/ng-components'; * * import { generalEn } from './i18n/general-en'; @@ -121,16 +126,20 @@ import { PoLanguageModule } from '../po-language/po-language.module'; * módulo utilizando a interface [`PoI18nConfig`](documentation/po-i18n#poI18nConfig): * * **Exemplo de padrões definidos:** - * ``` + * + * ```typescript * const i18nConfig: PoI18nConfig = { - * contexts: { - * general: { } - * }, * default: { * language: 'pt-BR', * context: 'general', * cache: true - * } + * }, + * contexts: { + * general: { + * 'en-US': generalEs, + * 'pt-BR': generalPt + * } + * }, * } * ``` * @@ -141,8 +150,32 @@ import { PoLanguageModule } from '../po-language/po-language.module'; * * **i18n com *Lazy loading*** * - * Para aplicações que utilizem a abordagem de módulos com carregamento *lazy loading*, caso seja - * definida outra configuração do `PoI18nModule`, deve-se atentar os seguintes detalhes: + * Para aplicações que utilizem a abordagem de módulos com carregamento *lazy loading* + * ou para módulos de bibliotecas que utilizam o PO, caso seja definida outra configuração + * do `PoI18nModule`, deve-se atentar os seguintes detalhes: + * + * - Utilize o método **`forChild`** ao invés do método `config` para definir os + * contextos e suas respectivas literais: + * + * ```typescript + * const i18nConfig: PoI18nConfig = { + * contexts: { + * myLib: { + * 'en-US': myLibEn, + * 'pt-BR': myLibPt + * } + * } + * }; + * + * @NgModule({ + * declarations: [], + * imports: [ + * CommonModule, + * PoModule, + * PoI18nModule.forChild(i18nConfig) + * ] + * }) + * ``` * * - Caso existam literais comuns na aplicação, estas devem ser reimportadas; * - Não defina outra *default language* para este módulo. Caso for definida, será sobreposta para @@ -151,7 +184,6 @@ import { PoLanguageModule } from '../po-language/po-language.module'; * método [`setLanguage()`](documentation/po-i18n#setLanguage) disponibilizado pelo `PoI18nService` * para definir a linguagem da aplicação e dos módulos com as linguagens diferentes. */ - @NgModule({ imports: [HttpClientModule, PoLanguageModule] }) @@ -160,27 +192,48 @@ export class PoI18nModule { return { ngModule: PoI18nModule, providers: [ - { - provide: I18N_CONFIG, - useValue: config - }, + provideI18nConfig(config), { provide: APP_INITIALIZER, useFactory: initializeLanguageDefault, multi: true, deps: [I18N_CONFIG, PoLanguageService] - }, - { - provide: PoI18nService, - useFactory: returnPoI18nService, - deps: [I18N_CONFIG, HttpClient, PoLanguageService] } ] }; } + + static forRoot(config: PoI18nConfig): ModuleWithProviders { + return PoI18nModule.config(config); + } + + static forChild(config: PoI18nConfig): ModuleWithProviders { + return { + ngModule: PoI18nModule, + providers: [provideI18nConfig(config)] + }; + } +} + +export function provideI18nConfig(config: PoI18nConfig): Array { + return [ + { + provide: I18N_CONFIG, + useValue: config, + multi: true + }, + { + provide: PoI18nService, + useFactory: returnPoI18nService, + deps: [I18N_CONFIG, HttpClient, PoLanguageService] + } + ]; } -export function initializeLanguageDefault(config: PoI18nConfig, languageService: PoLanguageService) { +export function initializeLanguageDefault(configs: Array, languageService: PoLanguageService) { + // Recupera a primeira configuração que possui "default". + const config = configs.find(c => c.default); + // tslint:disable-next-line:prefer-immediate-return const setDefaultLanguage = () => { if (config.default.language) { diff --git a/projects/ui/src/lib/services/po-i18n/po-i18n.service.ts b/projects/ui/src/lib/services/po-i18n/po-i18n.service.ts index b27a4a84f..d5fda8cb8 100644 --- a/projects/ui/src/lib/services/po-i18n/po-i18n.service.ts +++ b/projects/ui/src/lib/services/po-i18n/po-i18n.service.ts @@ -1,10 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { all as deepmergeAll } from 'deepmerge'; import { PoLanguageService } from './../po-language/po-language.service'; - -import { PoI18nBaseService } from './po-i18n-base.service'; import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; +import { PoI18nBaseService } from './po-i18n-base.service'; /** * @docsExtends PoI18nBaseService @@ -14,6 +14,10 @@ import { PoI18nConfig } from './interfaces/po-i18n-config.interface'; export class PoI18nService extends PoI18nBaseService {} // Função usada para retornar instância para o módulo po-i18n.module -export function returnPoI18nService(config: PoI18nConfig, http: HttpClient, languageService: PoLanguageService) { - return new PoI18nService(config, http, languageService); +export function returnPoI18nService( + configs: Array, + http: HttpClient, + languageService: PoLanguageService +) { + return new PoI18nService(deepmergeAll(configs), http, languageService); }