ov-components: Refactored translate service

pull/847/head
Carlos Santos 2024-09-26 10:26:59 +02:00
parent 7336f4f609
commit 28a0574d99
7 changed files with 153 additions and 53 deletions

View File

@ -3,7 +3,7 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelect } from '@angular/material/select'; import { MatSelect } from '@angular/material/select';
import { StorageService } from '../../../services/storage/storage.service'; import { StorageService } from '../../../services/storage/storage.service';
import { TranslateService } from '../../../services/translate/translate.service'; import { TranslateService } from '../../../services/translate/translate.service';
import { LangOption } from '../../../models/lang.model'; import { AvailableLangs, LangOption } from '../../../models/lang.model';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
/** /**
@ -42,20 +42,20 @@ export class LangSelectorComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.subscribeToLangSelected(); this.subscribeToLangSelected();
this.languages = this.translateService.getLanguagesInfo(); this.languages = this.translateService.getAvailableLanguages();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.langSub?.unsubscribe(); this.langSub?.unsubscribe();
} }
onLangSelected(lang: string) { onLangSelected(lang: AvailableLangs) {
this.translateService.setLanguage(lang); this.translateService.setCurrentLanguage(lang);
this.storageSrv.setLang(lang); this.storageSrv.setLang(lang);
} }
subscribeToLangSelected() { subscribeToLangSelected() {
this.langSub = this.translateService.langSelectedObs.subscribe((lang) => { this.langSub = this.translateService.selectedLanguageOption$.subscribe((lang) => {
this.langSelected = lang; this.langSelected = lang;
this.onLangChanged.emit(lang); this.onLangChanged.emit(lang);
}); });

View File

@ -3,7 +3,7 @@ import { CaptionsLangOption } from '../../models/caption.model';
// import { CaptionService } from '../../services/caption/caption.service'; // import { CaptionService } from '../../services/caption/caption.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { TranslateService } from '../../services/translate/translate.service'; import { TranslateService } from '../../services/translate/translate.service';
import { LangOption } from '../../models/lang.model'; import { AvailableLangs, LangOption } from '../../models/lang.model';
import { StorageService } from '../../services/storage/storage.service'; import { StorageService } from '../../services/storage/storage.service';
/** /**
@ -244,7 +244,7 @@ export class LangDirective implements OnDestroy {
/** /**
* @ignore * @ignore
*/ */
@Input() set lang(value: string) { @Input() set lang(value: AvailableLangs) {
this.update(value); this.update(value);
} }
@ -273,8 +273,8 @@ export class LangDirective implements OnDestroy {
/** /**
* @ignore * @ignore
*/ */
update(value: string) { update(value: AvailableLangs) {
this.translateService.setLanguage(value); this.translateService.setCurrentLanguage(value);
} }
} }
@ -343,7 +343,7 @@ export class LangOptionsDirective implements OnDestroy {
* @ignore * @ignore
*/ */
update(value: LangOption[] | undefined) { update(value: LangOption[] | undefined) {
this.translateService.setLanguageOptions(value); this.translateService.updateLanguageOptions(value);
} }
} }

View File

@ -1,4 +1,8 @@
export type AvailableLangs = 'en' | 'es' | 'de' | 'fr' | 'cn' | 'hi' | 'it' | 'ja' | 'nl' | 'pt';
export type AdditionalTranslationsType = Record<AvailableLangs, Record<string, any>>;
export interface LangOption { export interface LangOption {
name: string; name: string;
lang: string; lang: AvailableLangs;
} }

View File

@ -63,7 +63,6 @@ import { OpenViduComponentsDirectiveModule } from './directives/template/openvid
import { AppMaterialModule } from './openvidu-components-angular.material.module'; import { AppMaterialModule } from './openvidu-components-angular.material.module';
import { VirtualBackgroundService } from './services/virtual-background/virtual-background.service'; import { VirtualBackgroundService } from './services/virtual-background/virtual-background.service';
import { BroadcastingService } from './services/broadcasting/broadcasting.service'; import { BroadcastingService } from './services/broadcasting/broadcasting.service';
import { TranslateService } from './services/translate/translate.service';
import { GlobalConfigService } from './services/config/global-config.service'; import { GlobalConfigService } from './services/config/global-config.service';
import { OpenViduComponentsConfigService } from './services/config/directive-config.service'; import { OpenViduComponentsConfigService } from './services/config/directive-config.service';
@ -119,6 +118,7 @@ const privateComponents = [
RemoteParticipantTracksPipe, RemoteParticipantTracksPipe,
DurationFromSecondsPipe, DurationFromSecondsPipe,
TrackPublishedTypesPipe, TrackPublishedTypesPipe,
TranslatePipe,
OpenViduComponentsDirectiveModule, OpenViduComponentsDirectiveModule,
ApiDirectiveModule ApiDirectiveModule
], ],
@ -150,7 +150,6 @@ const privateComponents = [
PlatformService, PlatformService,
RecordingService, RecordingService,
StorageService, StorageService,
TranslateService,
VirtualBackgroundService, VirtualBackgroundService,
provideHttpClient(withInterceptorsFromDi()) provideHttpClient(withInterceptorsFromDi())
] ]

View File

@ -1,4 +1,4 @@
import { Injectable, Inject, Injector, Type, Optional } from '@angular/core'; import { Injectable, Inject, Injector, Optional } from '@angular/core';
import { LayoutService } from '../layout/layout.service'; import { LayoutService } from '../layout/layout.service';
import { OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config'; import { OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config';

View File

@ -10,18 +10,27 @@ import * as ja from '../../lang/ja.json';
import * as nl from '../../lang/nl.json'; import * as nl from '../../lang/nl.json';
import * as pt from '../../lang/pt.json'; import * as pt from '../../lang/pt.json';
import { StorageService } from '../storage/storage.service'; import { StorageService } from '../storage/storage.service';
import { LangOption } from '../../models/lang.model'; import { AdditionalTranslationsType, AvailableLangs, LangOption } from '../../models/lang.model';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
/** /**
* @internal * Service responsible for managing translations for the application.
* This service provides methods to add additional translations and to translate keys into the currently selected language.
*
* The pipe {@link TranslatePipe} is used to translate keys in the templates.
*/ */
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TranslateService { export class TranslateService {
private availableLanguages = { en, es, de, fr, cn, hi, it, ja, nl, pt }; // Maps language codes to their respective translations
private langOptions: LangOption[] = [ private translationsByLanguage: Record<AvailableLangs, any> = { en, es, de, fr, cn, hi, it, ja, nl, pt };
// Stores additional translations provided by the application
private additionalTranslations: Record<AvailableLangs, any> | {} = {};
// List of available language options with their display names and language codes
private languageOptions: LangOption[] = [
{ name: 'English', lang: 'en' }, { name: 'English', lang: 'en' },
{ name: 'Español', lang: 'es' }, { name: 'Español', lang: 'es' },
{ name: 'Deutsch', lang: 'de' }, { name: 'Deutsch', lang: 'de' },
@ -33,68 +42,152 @@ export class TranslateService {
{ name: 'Dutch', lang: 'nl' }, { name: 'Dutch', lang: 'nl' },
{ name: 'Português', lang: 'pt' } { name: 'Português', lang: 'pt' }
]; ];
private currentLang: any;
langSelected: LangOption; // The currently active translations for the selected language
langSelectedObs: Observable<LangOption>; private activeTranslations: any;
private _langSelected: BehaviorSubject<LangOption> = new BehaviorSubject<LangOption>({ name: 'English', lang: 'en' });
// The currently selected language option
private selectedLanguageOption: LangOption;
// BehaviorSubject to manage the currently selected language option
private _selectedLanguageSubject: BehaviorSubject<LangOption> = new BehaviorSubject<LangOption>({ name: 'English', lang: 'en' });
// Observable that emits changes to the selected language option
selectedLanguageOption$: Observable<LangOption>;
constructor(private storageService: StorageService) { constructor(private storageService: StorageService) {
this.langSelectedObs = this._langSelected.asObservable(); this.selectedLanguageOption$ = this._selectedLanguageSubject.asObservable();
this.updateLangSelected(); this.refreshSelectedLanguage();
} }
async setLanguage(lang: string) { /**
const matchingLang = this.langOptions.find((l) => l.lang === lang); * Adds multiple translations to the additional translations storage.
* @param translations - A record where each key is a language code and the value is an object of translations for that language.
*/
addTranslations(translations: Partial<AdditionalTranslationsType>): void {
this.additionalTranslations = translations;
}
if (matchingLang) { /**
this.currentLang = await this.getLangData(lang); * Sets the current language based on the provided language code.
this.langSelected = matchingLang; * Updates the selected language and emits the change.
this._langSelected.next(this.langSelected); * @param lang - The language code to set.
*
* @internal
*/
async setCurrentLanguage(lang: AvailableLangs): Promise<void> {
// Find the language option that matches the provided language code
const selectedLanguageOption = this.languageOptions.find((option) => option.lang === lang);
if (selectedLanguageOption) {
// Fetch the language data and update the current language
this.activeTranslations = await this.fetchLanguageData(lang);
this.selectedLanguageOption = selectedLanguageOption;
this._selectedLanguageSubject.next(this.selectedLanguageOption);
// Notify subscribers of the language change
this._selectedLanguageSubject.next(this.selectedLanguageOption);
} }
} }
setLanguageOptions(options: LangOption[] | undefined) { /**
* Updates the available language options.
* @param options - The new language options to set.
*
* @internal
*/
updateLanguageOptions(options?: LangOption[]): void {
if (options && options.length > 0) { if (options && options.length > 0) {
this.langOptions = options; this.languageOptions = options;
this.updateLangSelected(); this.refreshSelectedLanguage();
} }
} }
getLangSelected(): LangOption { /**
return this.langSelected; * Retrieves the currently selected language option.
* @returns The currently selected language option.
*
* @internal
*/
getSelectedLanguage(): LangOption {
return this.selectedLanguageOption;
} }
getLanguagesInfo(): LangOption[] { /**
return this.langOptions; * Retrieves the list of all available language options.
* @returns An array of available language options.
*/
getAvailableLanguages(): LangOption[] {
return this.languageOptions;
} }
/**
* Translates a given key into the current language.
*
* This method first attempts to find the translation in the official translations.
* If the translation is not found, it then looks for the translation in the additional translations registered by the app.
*
* @param key - The key to be translated.
* @returns The translated string if found, otherwise an empty string.
*/
translate(key: string): string { translate(key: string): string {
let result = this.currentLang; // Attempt to find the translation in the official translations
let translation = this.findTranslation(this.activeTranslations, key);
key.split('.').forEach((prop) => { if (!translation) {
// If not found, look for the translation in the additional translations
const additionalLangTranslations = this.additionalTranslations[this.selectedLanguageOption.lang];
translation = this.findTranslation(additionalLangTranslations, key);
}
return translation || '';
}
/**
* Finds and returns a translation string from a nested translations source object based on a dot-separated key.
*
* @param translationsSource - The source object containing nested translation strings.
* @param key - A dot-separated string representing the path to the desired translation.
* @returns The translation string if found, otherwise `undefined`.
*/
private findTranslation(translationsSource: any, key: string): string | undefined {
let translation = translationsSource;
// Traverse the object tree based on the key structure
key.split('.').forEach((nestedKey) => {
try { try {
result = result[prop];
translation = translation[nestedKey];
} catch (error) { } catch (error) {
return '';
} }
}); });
return result;
return translation;
} }
private async updateLangSelected() { /**
const storageLang = this.storageService.getLang(); * Updates the currently selected language based on the stored language setting.
const langOpt = this.langOptions.find((opt) => opt.lang === storageLang); */
if (storageLang && langOpt) { private async refreshSelectedLanguage() {
this.langSelected = langOpt; const storedLang = this.storageService.getLang();
const matchingOption = this.languageOptions.find((option) => option.lang === storedLang);
if (storedLang && matchingOption) {
this.selectedLanguageOption = matchingOption;
} else { } else {
this.langSelected = this.langOptions[0]; // Default to the first language option if no language is found in storage
this.selectedLanguageOption = this.languageOptions[0];
} }
this.currentLang = await this.getLangData(this.langSelected.lang); this.activeTranslations = await this.fetchLanguageData(this.selectedLanguageOption.lang);
this._langSelected.next(this.langSelected); this._selectedLanguageSubject.next(this.selectedLanguageOption);
} }
private async getLangData(lang: string): Promise<void> { /**
if (!(lang in this.availableLanguages)) { * Fetches the language data from the source based on the provided language code.
* @param lang - The language code to fetch data for.
* @returns The language data associated with the provided language code.
*/
private async fetchLanguageData(lang: AvailableLangs): Promise<any> {
if (!(lang in this.translationsByLanguage)) {
// Language not found in default languages options // Language not found in default languages options
// Try to find it in the assets/lang directory // Try to find it in the assets/lang directory
try { try {
@ -102,9 +195,10 @@ export class TranslateService {
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error(`Not found ${lang}.json in assets/lang`, error); console.error(`Not found ${lang}.json in assets/lang`, error);
return {};
} }
} else { } else {
return this.availableLanguages[lang]; return this.translationsByLanguage[lang];
} }
} }
} }

View File

@ -38,10 +38,12 @@ export * from './lib/models/room.model';
export * from './lib/models/toolbar.model'; export * from './lib/models/toolbar.model';
export * from './lib/models/logger.model' export * from './lib/models/logger.model'
export * from './lib/models/storage.model'; export * from './lib/models/storage.model';
export * from './lib/models/lang.model';
export * from './lib/openvidu-components-angular.module'; export * from './lib/openvidu-components-angular.module';
// Pipes // Pipes
export * from './lib/pipes/participant.pipe'; export * from './lib/pipes/participant.pipe';
export * from './lib/pipes/recording.pipe'; export * from './lib/pipes/recording.pipe';
export * from './lib/pipes/translate.pipe';
// Services // Services
export * from './lib/services/action/action.service'; export * from './lib/services/action/action.service';
export * from './lib/services/broadcasting/broadcasting.service'; export * from './lib/services/broadcasting/broadcasting.service';
@ -54,5 +56,6 @@ export * from './lib/services/recording/recording.service';
export * from './lib/services/config/global-config.service'; export * from './lib/services/config/global-config.service';
export * from './lib/services/logger/logger.service'; export * from './lib/services/logger/logger.service';
export * from './lib/services/storage/storage.service'; export * from './lib/services/storage/storage.service';
export * from './lib/services/translate/translate.service';
export * from 'livekit-client'; export * from 'livekit-client';