Skip to content

Commit

Permalink
ov-components: Refactored translate service
Browse files Browse the repository at this point in the history
  • Loading branch information
CSantosM committed Sep 26, 2024
1 parent 7336f4f commit 28a0574
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -244,7 +244,7 @@ export class LangDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set lang(value: string) {
@Input() set lang(value: AvailableLangs) {
this.update(value);
}

Expand Down Expand Up @@ -273,8 +273,8 @@ export class LangDirective implements OnDestroy {
/**
* @ignore
*/
update(value: string) {
this.translateService.setLanguage(value);
update(value: AvailableLangs) {
this.translateService.setCurrentLanguage(value);
}
}

Expand Down Expand Up @@ -343,7 +343,7 @@ export class LangOptionsDirective implements OnDestroy {
* @ignore
*/
update(value: LangOption[] | undefined) {
this.translateService.setLanguageOptions(value);
this.translateService.updateLanguageOptions(value);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
name: string;
lang: string;
lang: AvailableLangs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -119,6 +118,7 @@ const privateComponents = [
RemoteParticipantTracksPipe,
DurationFromSecondsPipe,
TrackPublishedTypesPipe,
TranslatePipe,
OpenViduComponentsDirectiveModule,
ApiDirectiveModule
],
Expand Down Expand Up @@ -150,7 +150,6 @@ const privateComponents = [
PlatformService,
RecordingService,
StorageService,
TranslateService,
VirtualBackgroundService,
provideHttpClient(withInterceptorsFromDi())
]
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<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: 'Español', lang: 'es' },
{ name: 'Deutsch', lang: 'de' },
Expand All @@ -33,78 +42,163 @@ export class TranslateService {
{ name: 'Dutch', lang: 'nl' },
{ name: 'Português', lang: 'pt' }
];
private currentLang: any;
langSelected: LangOption;
langSelectedObs: Observable<LangOption>;
private _langSelected: BehaviorSubject<LangOption> = new BehaviorSubject<LangOption>({ 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<LangOption> = new BehaviorSubject<LangOption>({ name: 'English', lang: 'en' });

// Observable that emits changes to the selected language option
selectedLanguageOption$: Observable<LangOption>;

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<AdditionalTranslationsType>): 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<void> {
// 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<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
// Try to find it in the assets/lang directory
try {
const response = await fetch(`assets/lang/${lang}.json`);
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];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

0 comments on commit 28a0574

Please sign in to comment.