Skip to content

feat(i18n): garantir merge correto de literais no PO UI e THF-Components #2411

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

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
1 change: 1 addition & 0 deletions projects/ui/src/lib/services/po-i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @description
*
* <a id="poI18nConfigContext"></a>
*
* 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 };
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PoI18nConfigDefault } from './po-i18n-config-default.interface';
import { PoI18nConfigContext } from './po-i18n-config-context.interface';

/**
* @description
Expand Down Expand Up @@ -76,5 +77,5 @@ export interface PoI18nConfig {
* ```
* > Caso a constante contenha alguma literal que o serviço não possua será utilizado a literal da constante.
*/
contexts: object;
contexts: PoI18nConfigContext;
}
189 changes: 120 additions & 69 deletions projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,39 @@ describe('PoI18nService:', () => {
});
});

it('should get specific literals passing parameters', done => {
it('should get specific literals passing parameters', () => {
service.getLiterals({ literals: ['text'] }).subscribe((literals: any) => {
expect(literals['text']).toBeTruthy();

done();
expect(literals['text']).toBe('texto');
});
});

it('should get specific literals from unexist language', done => {
// Procura em ingles, se não acho busca em pt-br
it('should get pt-br literals from unexist language', () => {
service.getLiterals({ literals: ['text'], language: 'en-us' }).subscribe((literals: any) => {
expect(literals['text']).toBeTruthy();
});
});

done();
it('should call getLiteralsFromContextService when servicesContext[context] exists', () => {
const observerMock = {
next: jasmine.createSpy('next'),
error: jasmine.createSpy('error'),
complete: jasmine.createSpy('complete')
};

service['servicesContext'] = { meuContexto: {} };

spyOn(service, <any>'getLiteralsFromContextService').and.callFake(() => {
observerMock.complete();
});

service.getLiterals({ context: 'meuContexto', language: 'pt' }).subscribe(observerMock);

expect(service['getLiteralsFromContextService']).toHaveBeenCalledWith(
'pt',
'meuContexto',
[],
jasmine.any(Object)
);
});

it('should get literals with specific context', () => {
Expand Down Expand Up @@ -321,74 +339,24 @@ describe('PoI18nService:', () => {

service = TestBed.inject(PoI18nService);
httpMock = TestBed.inject(HttpTestingController);
});

it('should get all literals from service', done => {
spyOn(service, 'getLanguage').and.returnValue('pt');

service.getLiterals().subscribe((literals: any) => {
expect(literals['developer']).toBeTruthy();
expect(literals['task']).toBeTruthy();

done();
spyOn(localStorage, 'getItem').and.callFake((key: string) => {
const mockStorage = {
'en-general-label1': 'Label 1',
'en-general-label2': 'Label 2'
};
return mockStorage[key] || null;
});

httpMock.expectOne((req: HttpRequest<any>) => req.method === 'GET').flush(mockResponse);
});

it('should return empty object when not found specific literals from service', done => {
spyOn(service, 'getLanguage').and.returnValue('pt');

service.getLiterals({ literals: ['teste'] }).subscribe((literals: any) => {
expect(Object.keys(literals).length).toBe(0);

done();
});

httpMock.expectOne((req: HttpRequest<any>) => req.method === 'GET').flush({});
});

it('should get specific literals from localStorage', done => {
const developerTranslation = 'desenvolvedor';
const taskTranslation = 'tarefa';

const language = 'en';

spyOn(service, 'getLanguage').and.returnValue(language);

localStorage.setItem(`${language}-general-developer`, developerTranslation);
localStorage.setItem(`${language}-general-task`, taskTranslation);

service.getLiterals({ literals: ['developer', 'task'] }).subscribe((literals: any) => {
expect(literals['developer']).toEqual(developerTranslation);
expect(literals['task']).toEqual(taskTranslation);

done();
});

localStorage.clear();
});

it('should get literals from localStorage, selecting context, literals and language', done => {
const carTranslation = 'carro';
const testTranslation = 'teste';

localStorage.setItem('pt-br-general-car', carTranslation);
localStorage.setItem('pt-br-another-test', testTranslation);

service
.getLiterals({ context: 'general', literals: ['car', 'test'], language: 'pt-br' })
.subscribe((literals: any) => {
expect(literals['car']).toEqual(carTranslation);
expect(literals['test']).toBeUndefined();

done();
describe('Methods: ', () => {
describe('getHttpService', () => {
it('should return a http servic', () => {
const httpService = service['getHttpService']('/', 'pt', ['text']);
expect(httpService).toBeTruthy();
});
});

localStorage.clear();
});

describe('Methods: ', () => {
describe('getLiteralsFromContextService', () => {
it(`should call 'observer.next' with translations if translations keys length is greater than 0
and call 'getLiteralsLocalStorageAndCache'`, () => {
Expand Down Expand Up @@ -420,6 +388,74 @@ describe('PoI18nService:', () => {
expect(spyMergeObject).toHaveBeenCalled();
expect(spyGetLiteralsLocalStorageAndCache).toHaveBeenCalled();
});

it('should assign languageAlternative to languageSearch when languageAlternative is provided', () => {
const language = 'en';
const languageAlternative = 'es';
const context = 'general';
const literals = ['label1', 'label2'];
const observer = { next: jasmine.createSpy('next') };
const translations = {};

spyOn(service as any, 'mergeObject').and.callThrough();
spyOn(service, 'searchInVarI18n' as keyof PoI18nService).and.returnValue('');
spyOn(service, 'countObject' as keyof PoI18nService).and.returnValue('0');
spyOn(service, 'getLiteralsLocalStorageAndCache' as keyof PoI18nService);

service['getLiteralsFromContextService'](
language,
context,
literals,
observer,
translations,
languageAlternative
);

expect(service['getLiteralsLocalStorageAndCache']).toHaveBeenCalledWith(
languageAlternative,
context,
literals,
observer,
translations,
languageAlternative
);
});
});

describe('searchInLocalStorage', () => {
it('should return translations when literals exist in localStorage', () => {
const language = 'en';
const context = 'general';
const literals = ['label1', 'label2'];

const result = service['searchInLocalStorage'](language, context, literals);

expect(result).toEqual({
label1: 'Label 1',
label2: 'Label 2'
});
});

it('should return an empty object when literals are not found in localStorage', () => {
const language = 'en';
const context = 'general';
const literals = ['label3', 'label4']; // Literais não presentes no mockStorage

const result = service['searchInLocalStorage'](language, context, literals);

expect(result).toEqual({});
});

it('should return an empty object when literals array is empty', () => {
const language = 'en';
const context = 'general';
const literals: Array<string> = [];

const result = service['searchInLocalStorage'](language, context, literals);

expect(result).toEqual({});
expect(localStorage.getItem).not.toHaveBeenCalled();
});
});

describe('getLiteralsLocalStorageAndCache', () => {
Expand Down Expand Up @@ -554,6 +590,21 @@ describe('PoI18nService:', () => {
expect(mergedObject.people).toBe(expectedPeopleTranslation);
expect(Object.keys(mergedObject).length).toBe(2);
});

it('updateLocalStorage: should store values in localStorage when useCache is true', () => {
service['useCache'] = true;

spyOn(localStorage, 'setItem').and.callFake(() => {});
const language = 'en';
const context = 'general';
const data = { label1: 'Label 1', label2: 'Label 2' };

service['updateLocalStorage'](language, context, data);

expect(localStorage.setItem).toHaveBeenCalledTimes(2);
expect(localStorage.setItem).toHaveBeenCalledWith('en-general-label1', 'Label 1');
expect(localStorage.setItem).toHaveBeenCalledWith('en-general-label2', 'Label 2');
});
});
});
});
19 changes: 19 additions & 0 deletions projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ import { I18N_CONFIG } from './po-i18n-config-injection-token';
* porém, nenhuma das propriedades são obrigatórias. Caso nenhum parâmetro seja passado, serão buscadas
* todas as literais do contexto definido com padrão, no idioma definido como padrão.
*
* * ## Alterações a partir da versão 19
* A partir da versão 19, para evitar conflitos com bibliotecas de terceiros que também utilizam i18n,
* é necessário passar explicitamente o contexto ao chamar `getLiterals`, garantindo a correta exibição das literais.
* Caso `getLiterals` seja chamado sem parâmetros, o retorno pode vir das configurações da biblioteca de terceiros.
*
* **Exemplo de chamada com contexto explícito:**
* ```typescript
* poI18nService.getLiterals({ context: 'general' }).subscribe(literals => console.log(literals));
* ```
*
* **Cenário de Contextos Iguais:**
* Caso tanto a aplicação quanto uma biblioteca de terceiros utilizem o mesmo nome de contexto,
* o PO UI fará um merge das literais, priorizando os valores definidos na aplicação cliente.
*
* **Recomendações:**
* - Sempre informar o contexto ao chamar `getLiterals` para evitar conflitos de literais.
* - Caso a aplicação utilize `lazy loading`, utilizar `setLanguage()` para garantir a correta configuração de idioma.
*
* Exemplos de requisição:
* ```
* literals = {};
Expand Down Expand Up @@ -144,6 +162,7 @@ import { I18N_CONFIG } from './po-i18n-config-injection-token';
* }));
*
* });
*
* ```
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { InjectionToken } from '@angular/core';

import { PoI18nConfig } from './interfaces/po-i18n-config.interface';

export const I18N_CONFIG = new InjectionToken<PoI18nConfig>('I18N_CONFIG');
export const I18N_CONFIG = new InjectionToken<Array<PoI18nConfig>>('I18N_CONFIG');
14 changes: 7 additions & 7 deletions projects/ui/src/lib/services/po-i18n/po-i18n.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ import { PoLanguageModule } from '../po-language/po-language.module';
* 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:
*
* - 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
* toda a aplicação;
* - Caso precise de módulos carregados via *lazy loading* com linguagens diferentes, utilize o
Expand All @@ -162,7 +161,8 @@ export class PoI18nModule {
providers: [
{
provide: I18N_CONFIG,
useValue: config
useValue: config,
multi: true
},
provideAppInitializer(() => {
const initializerFn = initializeLanguageDefault(inject(I18N_CONFIG), inject(PoLanguageService));
Expand All @@ -178,12 +178,12 @@ export class PoI18nModule {
}
}

export function initializeLanguageDefault(config: PoI18nConfig, languageService: PoLanguageService) {
// eslint-disable-next-line sonarjs/prefer-immediate-return
const setDefaultLanguage = () => {
if (config.default.language) {
export function initializeLanguageDefault(configs: Array<PoI18nConfig>, languageService: PoLanguageService) {
const config = configs.find(c => c.default); // Busca a configuração com `default`

return () => {
if (config?.default.language) {
languageService.setLanguageDefault(config.default.language);
}
};
return setDefaultLanguage;
}
Loading