diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.ts index 2aa021d032..0d166f2fdd 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.ts @@ -3,7 +3,7 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { MatSelect } from '@angular/material/select'; import { StorageService } from '../../../services/storage/storage.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'; /** @@ -42,20 +42,20 @@ export class LangSelectorComponent implements OnInit, OnDestroy { ngOnInit(): void { this.subscribeToLangSelected(); - this.languages = this.translateService.getLanguagesInfo(); + this.languages = this.translateService.getAvailableLanguages(); } ngOnDestroy(): void { this.langSub?.unsubscribe(); } - onLangSelected(lang: string) { - this.translateService.setLanguage(lang); + onLangSelected(lang: AvailableLangs) { + this.translateService.setCurrentLanguage(lang); this.storageSrv.setLang(lang); } subscribeToLangSelected() { - this.langSub = this.translateService.langSelectedObs.subscribe((lang) => { + this.langSub = this.translateService.selectedLanguageOption$.subscribe((lang) => { this.langSelected = lang; this.onLangChanged.emit(lang); }); diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts index 776c464009..795acc24f5 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts @@ -3,7 +3,7 @@ import { CaptionsLangOption } from '../../models/caption.model'; // import { CaptionService } from '../../services/caption/caption.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.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'; /** @@ -244,7 +244,7 @@ export class LangDirective implements OnDestroy { /** * @ignore */ - @Input() set lang(value: string) { + @Input() set lang(value: AvailableLangs) { this.update(value); } @@ -273,8 +273,8 @@ export class LangDirective implements OnDestroy { /** * @ignore */ - update(value: string) { - this.translateService.setLanguage(value); + update(value: AvailableLangs) { + this.translateService.setCurrentLanguage(value); } } @@ -343,7 +343,7 @@ export class LangOptionsDirective implements OnDestroy { * @ignore */ update(value: LangOption[] | undefined) { - this.translateService.setLanguageOptions(value); + this.translateService.updateLanguageOptions(value); } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/lang.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/lang.model.ts index 01572c1ad9..fe2200699a 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/lang.model.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/lang.model.ts @@ -1,4 +1,8 @@ +export type AvailableLangs = 'en' | 'es' | 'de' | 'fr' | 'cn' | 'hi' | 'it' | 'ja' | 'nl' | 'pt'; + +export type AdditionalTranslationsType = Record>; + export interface LangOption { name: string; - lang: string; + lang: AvailableLangs; } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular.module.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular.module.ts index 3270e77ff3..108945d869 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular.module.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/openvidu-components-angular.module.ts @@ -63,7 +63,6 @@ import { OpenViduComponentsDirectiveModule } from './directives/template/openvid import { AppMaterialModule } from './openvidu-components-angular.material.module'; import { VirtualBackgroundService } from './services/virtual-background/virtual-background.service'; import { BroadcastingService } from './services/broadcasting/broadcasting.service'; -import { TranslateService } from './services/translate/translate.service'; import { GlobalConfigService } from './services/config/global-config.service'; import { OpenViduComponentsConfigService } from './services/config/directive-config.service'; @@ -119,6 +118,7 @@ const privateComponents = [ RemoteParticipantTracksPipe, DurationFromSecondsPipe, TrackPublishedTypesPipe, + TranslatePipe, OpenViduComponentsDirectiveModule, ApiDirectiveModule ], @@ -150,7 +150,6 @@ const privateComponents = [ PlatformService, RecordingService, StorageService, - TranslateService, VirtualBackgroundService, provideHttpClient(withInterceptorsFromDi()) ] diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/service-config.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/service-config.service.ts index cc53be43b0..30119d58de 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/service-config.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/service-config.service.ts @@ -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 { OpenViduComponentsConfig } from '../../config/openvidu-components-angular.config'; diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/translate/translate.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/translate/translate.service.ts index 01d5a0bae4..1f1b3e6992 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/translate/translate.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/translate/translate.service.ts @@ -10,18 +10,27 @@ import * as ja from '../../lang/ja.json'; import * as nl from '../../lang/nl.json'; import * as pt from '../../lang/pt.json'; 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'; /** - * @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({ providedIn: 'root' }) export class TranslateService { - private availableLanguages = { en, es, de, fr, cn, hi, it, ja, nl, pt }; - private langOptions: LangOption[] = [ + // Maps language codes to their respective translations + private translationsByLanguage: Record = { en, es, de, fr, cn, hi, it, ja, nl, pt }; + + // Stores additional translations provided by the application + private additionalTranslations: Record | {} = {}; + + // List of available language options with their display names and language codes + private languageOptions: LangOption[] = [ { name: 'English', lang: 'en' }, { name: 'Español', lang: 'es' }, { name: 'Deutsch', lang: 'de' }, @@ -33,68 +42,152 @@ export class TranslateService { { name: 'Dutch', lang: 'nl' }, { name: 'Português', lang: 'pt' } ]; - private currentLang: any; - langSelected: LangOption; - langSelectedObs: Observable; - private _langSelected: BehaviorSubject = new BehaviorSubject({ name: 'English', lang: 'en' }); + + // The currently active translations for the selected language + private activeTranslations: any; + + // The currently selected language option + private selectedLanguageOption: LangOption; + + // BehaviorSubject to manage the currently selected language option + private _selectedLanguageSubject: BehaviorSubject = new BehaviorSubject({ name: 'English', lang: 'en' }); + + // Observable that emits changes to the selected language option + selectedLanguageOption$: Observable; constructor(private storageService: StorageService) { - this.langSelectedObs = this._langSelected.asObservable(); - this.updateLangSelected(); + this.selectedLanguageOption$ = this._selectedLanguageSubject.asObservable(); + this.refreshSelectedLanguage(); + } + + /** + * 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): void { + this.additionalTranslations = translations; } - async setLanguage(lang: string) { - const matchingLang = this.langOptions.find((l) => l.lang === lang); + /** + * Sets the current language based on the provided language code. + * Updates the selected language and emits the change. + * @param lang - The language code to set. + * + * @internal + */ + async setCurrentLanguage(lang: AvailableLangs): Promise { + // Find the language option that matches the provided language code + const selectedLanguageOption = this.languageOptions.find((option) => option.lang === lang); - if (matchingLang) { - this.currentLang = await this.getLangData(lang); - this.langSelected = matchingLang; - this._langSelected.next(this.langSelected); + 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) { - this.langOptions = options; - this.updateLangSelected(); + this.languageOptions = options; + 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 { - 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 { - result = result[prop]; + + translation = translation[nestedKey]; } catch (error) { - return ''; } }); - return result; + + return translation; } - private async updateLangSelected() { - const storageLang = this.storageService.getLang(); - const langOpt = this.langOptions.find((opt) => opt.lang === storageLang); - if (storageLang && langOpt) { - this.langSelected = langOpt; + /** + * Updates the currently selected language based on the stored language setting. + */ + private async refreshSelectedLanguage() { + const storedLang = this.storageService.getLang(); + const matchingOption = this.languageOptions.find((option) => option.lang === storedLang); + + if (storedLang && matchingOption) { + this.selectedLanguageOption = matchingOption; } 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._langSelected.next(this.langSelected); + this.activeTranslations = await this.fetchLanguageData(this.selectedLanguageOption.lang); + this._selectedLanguageSubject.next(this.selectedLanguageOption); } - private async getLangData(lang: string): Promise { - 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 { + if (!(lang in this.translationsByLanguage)) { // Language not found in default languages options // Try to find it in the assets/lang directory try { @@ -102,9 +195,10 @@ export class TranslateService { return await response.json(); } catch (error) { console.error(`Not found ${lang}.json in assets/lang`, error); + return {}; } } else { - return this.availableLanguages[lang]; + return this.translationsByLanguage[lang]; } } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/public-api.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/public-api.ts index 3911838c54..3ad7bf72c5 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/public-api.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/public-api.ts @@ -38,10 +38,12 @@ export * from './lib/models/room.model'; export * from './lib/models/toolbar.model'; export * from './lib/models/logger.model' export * from './lib/models/storage.model'; +export * from './lib/models/lang.model'; export * from './lib/openvidu-components-angular.module'; // Pipes export * from './lib/pipes/participant.pipe'; export * from './lib/pipes/recording.pipe'; +export * from './lib/pipes/translate.pipe'; // Services export * from './lib/services/action/action.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/logger/logger.service'; export * from './lib/services/storage/storage.service'; +export * from './lib/services/translate/translate.service'; export * from 'livekit-client';