Skip to content

Commit 84d01d1

Browse files
committed
feat(i18n): garantir merge correto de literais no PO UI e THF-Components
Ajustado `getLiterals()` para garantir que as literais sejam exibidas corretamente, independente da configuração utilizada. Agora, ao utilizar o PO UI e o THF-Components, o serviço i18n realiza o merge das literais da aplicação client e das bibliotecas de terceiros, evitando sobreposição indevida e garantindo a exibição esperada. Fixes DTHFUI-10854
1 parent 235b5ed commit 84d01d1

9 files changed

+405
-85
lines changed

projects/ui/src/lib/services/po-i18n/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './interfaces/po-i18n-config.interface';
2+
export * from './interfaces/po-i18n-config-context.interface';
23
export * from './interfaces/po-i18n-config-default.interface';
34
export * from './interfaces/po-i18n-literals.interface';
45
export * from './po-i18n.pipe';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @description
3+
*
4+
* <a id="poI18nConfigContext"></a>
5+
*
6+
* Interface para a configuração dos contextos do módulo `PoI18nModule`.
7+
*
8+
* @usedBy PoI18nModule
9+
*/
10+
export interface PoI18nConfigContext {
11+
[name: string]: { [language: string]: { [literal: string]: string } } | { url: string };
12+
}

projects/ui/src/lib/services/po-i18n/interfaces/po-i18n-config.interface.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PoI18nConfigDefault } from './po-i18n-config-default.interface';
2+
import { PoI18nConfigContext } from './po-i18n-config-context.interface';
23

34
/**
45
* @description
@@ -76,5 +77,5 @@ export interface PoI18nConfig {
7677
* ```
7778
* > Caso a constante contenha alguma literal que o serviço não possua será utilizado a literal da constante.
7879
*/
79-
contexts: object;
80+
contexts: PoI18nConfigContext;
8081
}

projects/ui/src/lib/services/po-i18n/po-i18n-base.service.spec.ts

+120-69
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,39 @@ describe('PoI18nService:', () => {
6262
});
6363
});
6464

65-
it('should get specific literals passing parameters', done => {
65+
it('should get specific literals passing parameters', () => {
6666
service.getLiterals({ literals: ['text'] }).subscribe((literals: any) => {
67-
expect(literals['text']).toBeTruthy();
68-
69-
done();
67+
expect(literals['text']).toBe('texto');
7068
});
7169
});
7270

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

78-
done();
77+
it('should call getLiteralsFromContextService when servicesContext[context] exists', () => {
78+
const observerMock = {
79+
next: jasmine.createSpy('next'),
80+
error: jasmine.createSpy('error'),
81+
complete: jasmine.createSpy('complete')
82+
};
83+
84+
service['servicesContext'] = { meuContexto: {} };
85+
86+
spyOn(service, <any>'getLiteralsFromContextService').and.callFake(() => {
87+
observerMock.complete();
7988
});
89+
90+
service.getLiterals({ context: 'meuContexto', language: 'pt' }).subscribe(observerMock);
91+
92+
expect(service['getLiteralsFromContextService']).toHaveBeenCalledWith(
93+
'pt',
94+
'meuContexto',
95+
[],
96+
jasmine.any(Object)
97+
);
8098
});
8199

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

322340
service = TestBed.inject(PoI18nService);
323341
httpMock = TestBed.inject(HttpTestingController);
324-
});
325342

326-
it('should get all literals from service', done => {
327-
spyOn(service, 'getLanguage').and.returnValue('pt');
328-
329-
service.getLiterals().subscribe((literals: any) => {
330-
expect(literals['developer']).toBeTruthy();
331-
expect(literals['task']).toBeTruthy();
332-
333-
done();
343+
spyOn(localStorage, 'getItem').and.callFake((key: string) => {
344+
const mockStorage = {
345+
'en-general-label1': 'Label 1',
346+
'en-general-label2': 'Label 2'
347+
};
348+
return mockStorage[key] || null;
334349
});
335-
336-
httpMock.expectOne((req: HttpRequest<any>) => req.method === 'GET').flush(mockResponse);
337-
});
338-
339-
it('should return empty object when not found specific literals from service', done => {
340-
spyOn(service, 'getLanguage').and.returnValue('pt');
341-
342-
service.getLiterals({ literals: ['teste'] }).subscribe((literals: any) => {
343-
expect(Object.keys(literals).length).toBe(0);
344-
345-
done();
346-
});
347-
348-
httpMock.expectOne((req: HttpRequest<any>) => req.method === 'GET').flush({});
349-
});
350-
351-
it('should get specific literals from localStorage', done => {
352-
const developerTranslation = 'desenvolvedor';
353-
const taskTranslation = 'tarefa';
354-
355-
const language = 'en';
356-
357-
spyOn(service, 'getLanguage').and.returnValue(language);
358-
359-
localStorage.setItem(`${language}-general-developer`, developerTranslation);
360-
localStorage.setItem(`${language}-general-task`, taskTranslation);
361-
362-
service.getLiterals({ literals: ['developer', 'task'] }).subscribe((literals: any) => {
363-
expect(literals['developer']).toEqual(developerTranslation);
364-
expect(literals['task']).toEqual(taskTranslation);
365-
366-
done();
367-
});
368-
369-
localStorage.clear();
370350
});
371351

372-
it('should get literals from localStorage, selecting context, literals and language', done => {
373-
const carTranslation = 'carro';
374-
const testTranslation = 'teste';
375-
376-
localStorage.setItem('pt-br-general-car', carTranslation);
377-
localStorage.setItem('pt-br-another-test', testTranslation);
378-
379-
service
380-
.getLiterals({ context: 'general', literals: ['car', 'test'], language: 'pt-br' })
381-
.subscribe((literals: any) => {
382-
expect(literals['car']).toEqual(carTranslation);
383-
expect(literals['test']).toBeUndefined();
384-
385-
done();
352+
describe('Methods: ', () => {
353+
describe('getHttpService', () => {
354+
it('should return a http servic', () => {
355+
const httpService = service['getHttpService']('/', 'pt', ['text']);
356+
expect(httpService).toBeTruthy();
386357
});
358+
});
387359

388-
localStorage.clear();
389-
});
390-
391-
describe('Methods: ', () => {
392360
describe('getLiteralsFromContextService', () => {
393361
it(`should call 'observer.next' with translations if translations keys length is greater than 0
394362
and call 'getLiteralsLocalStorageAndCache'`, () => {
@@ -420,6 +388,74 @@ describe('PoI18nService:', () => {
420388
expect(spyMergeObject).toHaveBeenCalled();
421389
expect(spyGetLiteralsLocalStorageAndCache).toHaveBeenCalled();
422390
});
391+
392+
it('should assign languageAlternative to languageSearch when languageAlternative is provided', () => {
393+
const language = 'en';
394+
const languageAlternative = 'es';
395+
const context = 'general';
396+
const literals = ['label1', 'label2'];
397+
const observer = { next: jasmine.createSpy('next') };
398+
const translations = {};
399+
400+
spyOn(service as any, 'mergeObject').and.callThrough();
401+
spyOn(service, 'searchInVarI18n' as keyof PoI18nService).and.returnValue('');
402+
spyOn(service, 'countObject' as keyof PoI18nService).and.returnValue('0');
403+
spyOn(service, 'getLiteralsLocalStorageAndCache' as keyof PoI18nService);
404+
405+
service['getLiteralsFromContextService'](
406+
language,
407+
context,
408+
literals,
409+
observer,
410+
translations,
411+
languageAlternative
412+
);
413+
414+
expect(service['getLiteralsLocalStorageAndCache']).toHaveBeenCalledWith(
415+
languageAlternative,
416+
context,
417+
literals,
418+
observer,
419+
translations,
420+
languageAlternative
421+
);
422+
});
423+
});
424+
425+
describe('searchInLocalStorage', () => {
426+
it('should return translations when literals exist in localStorage', () => {
427+
const language = 'en';
428+
const context = 'general';
429+
const literals = ['label1', 'label2'];
430+
431+
const result = service['searchInLocalStorage'](language, context, literals);
432+
433+
expect(result).toEqual({
434+
label1: 'Label 1',
435+
label2: 'Label 2'
436+
});
437+
});
438+
439+
it('should return an empty object when literals are not found in localStorage', () => {
440+
const language = 'en';
441+
const context = 'general';
442+
const literals = ['label3', 'label4']; // Literais não presentes no mockStorage
443+
444+
const result = service['searchInLocalStorage'](language, context, literals);
445+
446+
expect(result).toEqual({});
447+
});
448+
449+
it('should return an empty object when literals array is empty', () => {
450+
const language = 'en';
451+
const context = 'general';
452+
const literals: Array<string> = [];
453+
454+
const result = service['searchInLocalStorage'](language, context, literals);
455+
456+
expect(result).toEqual({});
457+
expect(localStorage.getItem).not.toHaveBeenCalled();
458+
});
423459
});
424460

425461
describe('getLiteralsLocalStorageAndCache', () => {
@@ -554,6 +590,21 @@ describe('PoI18nService:', () => {
554590
expect(mergedObject.people).toBe(expectedPeopleTranslation);
555591
expect(Object.keys(mergedObject).length).toBe(2);
556592
});
593+
594+
it('updateLocalStorage: should store values in localStorage when useCache is true', () => {
595+
service['useCache'] = true;
596+
597+
spyOn(localStorage, 'setItem').and.callFake(() => {});
598+
const language = 'en';
599+
const context = 'general';
600+
const data = { label1: 'Label 1', label2: 'Label 2' };
601+
602+
service['updateLocalStorage'](language, context, data);
603+
604+
expect(localStorage.setItem).toHaveBeenCalledTimes(2);
605+
expect(localStorage.setItem).toHaveBeenCalledWith('en-general-label1', 'Label 1');
606+
expect(localStorage.setItem).toHaveBeenCalledWith('en-general-label2', 'Label 2');
607+
});
557608
});
558609
});
559610
});

projects/ui/src/lib/services/po-i18n/po-i18n-base.service.ts

+19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ import { I18N_CONFIG } from './po-i18n-config-injection-token';
3636
* porém, nenhuma das propriedades são obrigatórias. Caso nenhum parâmetro seja passado, serão buscadas
3737
* todas as literais do contexto definido com padrão, no idioma definido como padrão.
3838
*
39+
* * ## Alterações a partir da versão 19
40+
* A partir da versão 19, para evitar conflitos com bibliotecas de terceiros que também utilizam i18n,
41+
* é necessário passar explicitamente o contexto ao chamar `getLiterals`, garantindo a correta exibição das literais.
42+
* Caso `getLiterals` seja chamado sem parâmetros, o retorno pode vir das configurações da biblioteca de terceiros.
43+
*
44+
* **Exemplo de chamada com contexto explícito:**
45+
* ```typescript
46+
* poI18nService.getLiterals({ context: 'general' }).subscribe(literals => console.log(literals));
47+
* ```
48+
*
49+
* **Cenário de Contextos Iguais:**
50+
* Caso tanto a aplicação quanto uma biblioteca de terceiros utilizem o mesmo nome de contexto,
51+
* o PO UI fará um merge das literais, priorizando os valores definidos na aplicação cliente.
52+
*
53+
* **Recomendações:**
54+
* - Sempre informar o contexto ao chamar `getLiterals` para evitar conflitos de literais.
55+
* - Caso a aplicação utilize `lazy loading`, utilizar `setLanguage()` para garantir a correta configuração de idioma.
56+
*
3957
* Exemplos de requisição:
4058
* ```
4159
* literals = {};
@@ -144,6 +162,7 @@ import { I18N_CONFIG } from './po-i18n-config-injection-token';
144162
* }));
145163
*
146164
* });
165+
*
147166
* ```
148167
*/
149168

projects/ui/src/lib/services/po-i18n/po-i18n-config-injection-token.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ import { InjectionToken } from '@angular/core';
22

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

5-
export const I18N_CONFIG = new InjectionToken<PoI18nConfig>('I18N_CONFIG');
5+
export const I18N_CONFIG = new InjectionToken<Array<PoI18nConfig>>('I18N_CONFIG');

projects/ui/src/lib/services/po-i18n/po-i18n.module.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ import { PoLanguageModule } from '../po-language/po-language.module';
144144
* Para aplicações que utilizem a abordagem de módulos com carregamento *lazy loading*, caso seja
145145
* definida outra configuração do `PoI18nModule`, deve-se atentar os seguintes detalhes:
146146
*
147-
* - Caso existam literais comuns na aplicação, estas devem ser reimportadas;
148147
* - Não defina outra *default language* para este módulo. Caso for definida, será sobreposta para
149148
* toda a aplicação;
150149
* - Caso precise de módulos carregados via *lazy loading* com linguagens diferentes, utilize o
@@ -162,7 +161,8 @@ export class PoI18nModule {
162161
providers: [
163162
{
164163
provide: I18N_CONFIG,
165-
useValue: config
164+
useValue: config,
165+
multi: true
166166
},
167167
provideAppInitializer(() => {
168168
const initializerFn = initializeLanguageDefault(inject(I18N_CONFIG), inject(PoLanguageService));
@@ -178,12 +178,12 @@ export class PoI18nModule {
178178
}
179179
}
180180

181-
export function initializeLanguageDefault(config: PoI18nConfig, languageService: PoLanguageService) {
182-
// eslint-disable-next-line sonarjs/prefer-immediate-return
183-
const setDefaultLanguage = () => {
184-
if (config.default.language) {
181+
export function initializeLanguageDefault(configs: Array<PoI18nConfig>, languageService: PoLanguageService) {
182+
const config = configs.find(c => c.default); // Busca a configuração com `default`
183+
184+
return () => {
185+
if (config?.default.language) {
185186
languageService.setLanguageDefault(config.default.language);
186187
}
187188
};
188-
return setDefaultLanguage;
189189
}

0 commit comments

Comments
 (0)