import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { BulkUpdateFunctionNames, GetTranslationsFunctionNames, MachineTranslateBulkFunctionNames, MachineTranslateSingleFunctionNames, TranslationResources } from '@core/resources/translation.resources'; import { SpinnerService } from '@core/services/spinner.service'; import { TranslationService } from '@core/services/translation.service'; import { FooterState } from '@core/states/footer.state'; import { BaseTranslation, TranslatedRichTextTypes, Translation, ValidRichTextTypes } from '@core/typings/translation.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { AutoTableRepository, AutoTableRepositoryFactory, DebounceFactory, PaginationOptions, SelectOption, SimpleStringMap, TableDataFactory, TopLevelFilter, TypeSafeFormBuilder, TypeSafeFormGroup, ValueComparisonService } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { isUndefined } from 'lodash'; import { Subscription } from 'rxjs'; import { ExportTranslationsModalComponent } from '../export-translations-modal/export-translations-modal.component'; import { TranslateRichTextModalComponent } from '../translate-rich-text-modal/translate-rich-text-modal.component'; interface LangFormGroup { defaultLang: string; translationLang: string; } interface TranslationFormGroup { [x: string]: string; } @Component({ selector: 'gc-translate-page', templateUrl: './translate-page.component.html', styleUrls: ['./translate-page.component.scss'] }) export class TranslatePageComponent implements OnInit, OnDestroy { @Input() title = ''; @Input() subTitle = ''; @Input() repoName = 'TRANSLATIONS'; @Input() hasImport = true; @Input() hasExport = true; @Input() topLevelFilters: TopLevelFilter[] = []; @Input() noTranslationsMessage = this.i18n.translate( 'GLOBAL:textNoTranslationsFound', {}, 'No translations found.' ); @Input() richTextAttributeName = this.i18n.translate('common:textDescription'); @Input() richTextMapKey = ''; @Input() richTextMap: SimpleStringMap; @Input() ckEditorConfigForRichText: CKEDITOR.config; @Input() getTranslationsFunctionName: GetTranslationsFunctionNames = 'getFormTranslations'; @Input() bulkUpdateFunctionName: BulkUpdateFunctionNames = 'bulkUpdateFormTranslations'; @Input() machineTranslateSingleFunctionName: MachineTranslateSingleFunctionNames = 'machineTranslateForFormsSingle'; @Input() machineTranslateBulkFunctionName: MachineTranslateBulkFunctionNames = 'machineTranslateForFormsBulk'; @Input() dropdownColumnName = 'formId'; // column name of checkbox dropdown filter @Input() dropdownOptionsAttr = 'formIds'; // options attr for checkbox dropdown filter @Input() dropdownOptions: SelectOption[] = []; @Input() getDefaultLangOnFilterChange: (value: any[]) => string; // function to get default lang on change of checkbox dropdown filter tableDataFactory: TableDataFactory; repository: AutoTableRepository; repoFormGroup: TypeSafeFormGroup; langFormGroup: TypeSafeFormGroup; translationFormGroup: TypeSafeFormGroup; hiddenFilters: TopLevelFilter[] = []; langKeys = this.clientSettingsService.get('selectedLanguages'); langKeysForTranslation: string[]; sub = new Subscription(); loading = false; saveOnLangChange = false; constructor ( private autoTableFactory: AutoTableRepositoryFactory, private notifierService: NotifierService, private valueComparisonService: ValueComparisonService, private formBuilder: TypeSafeFormBuilder, private i18n: I18nService, private clientSettingsService: ClientSettingsService, private spinnerService: SpinnerService, private footerState: FooterState, private modalFactory: ModalFactory, private translationResources: TranslationResources, private translationService: TranslationService ) { this.setFooter(); } ngOnInit () { this.setLangFormGroup(); this.setFilters(); this.setTableDataFactory(); } async setFooter () { this.footerState .set('footerState', this.footerState.FOOTER_STATES.ACTION) .set( 'footerActionDisabled', this.translationFormGroup ? this.translationFormGroup.invalid : false ) .set('footerAction', async () => { this.footerState.set('footerActionDisabled', true); await this.saveChanges(); this.footerState.set('footerActionDisabled', false); }) .set('footerActionLabel', this.i18n.translate('common:btnSave', {}, 'Save')); } setLangFormGroup () { const translationLang = this.translationService.getDefaultLangSelectorValue(); this.langFormGroup = this.formBuilder.group({ defaultLang: this.clientSettingsService.defaultLanguage, translationLang }); this.setLangKeysForTranslation(); } setFilters () { this.hiddenFilters = [ new TopLevelFilter( 'text', 'defaultLanguage', this.langFormGroup.value.defaultLang, undefined ) ]; const existingRepo = this.autoTableFactory.getRepository(this.repoName); this.repoFormGroup = this.formBuilder.group( this.topLevelFilters.reduce((formGroup, filter) => ({ ...formGroup, [filter.column]: [filter.value] }), {}) ); if ( existingRepo && existingRepo.topLevelFormGroup !== this.repoFormGroup ) { this.repoFormGroup = existingRepo.topLevelFormGroup; } } setTableDataFactory () { this.tableDataFactory = DebounceFactory.createSimple( async (options: PaginationOptions) => { options = this.translationService.formatPaginationOptions( options, this.dropdownColumnName, this.dropdownOptionsAttr, this.dropdownOptions ); const result = await this.translationResources[this.getTranslationsFunctionName]( options, (options as any)[this.dropdownOptionsAttr] ); await this.setTranslationFormGroup(result.records); return { success: true, data: { recordCount: result.recordCount, records: result.records } }; } ); this.repository = this.autoTableFactory.create({ key: this.repoName, columns: [], notifier: this.notifierService, valueComparisonService: this.valueComparisonService, tableDataFactory: this.tableDataFactory, rowsPerPage: 15, formGroup: this.repoFormGroup, hiddenFilters: this.hiddenFilters, topLevelFilters: this.topLevelFilters }); this.repository.reset(); this.sub.add( this.repository.loading.subscribe((val) => { this.loading = val; }) ); } getRichTextAttributeName = (item: BaseTranslation) => { // Get the link text that will display for rich text attrs if (item.isRichText) { let richLinkText = this.richTextAttributeName; if (this.richTextMap && this.richTextMapKey) { let value = (item as any)[this.richTextMapKey]; const isArray = value && (value instanceof Array); if (isArray) { value = value.length === 1 ? value[0] : null; } if (value) { const mappedValue = this.richTextMap[value]; const keyNameAsArray = item.languageKeyName.split('.'); const type: TranslatedRichTextTypes = keyNameAsArray.slice(-1)[0] as TranslatedRichTextTypes; if (ValidRichTextTypes.includes(type)) { richLinkText = this.getTranslatedRichLinkText( type, mappedValue, value ); } else if (mappedValue) { richLinkText = mappedValue + ' ' + this.richTextAttributeName; } } } item.richLinkText = richLinkText; } }; getTranslatedRichLinkText ( type: TranslatedRichTextTypes, mappedValue: string, value: any ) { const defaultText = type.replace(/_/g, ' ').toLowerCase(); switch (type) { case 'Description': default: if (mappedValue) { return this.i18n.translate( 'GLOBAL:textDynamicDescription', { name: this.richTextMap[value] }, `__name__ ${defaultText}` ); } else { return this.i18n.translate( `common:textDescription`, {}, defaultText ); } case 'Eligibility_Pass_Message': if (mappedValue) { return this.i18n.translate( 'GLOBAL:textDynamicEligibility_Pass_Message', { name: this.richTextMap[value] }, `__name__ ${defaultText}` ); } else { return this.i18n.translate( `GLOBAL:textEligibility_Pass_Message`, {}, defaultText ); } case 'Eligibility_Fail_Message': if (mappedValue) { return this.i18n.translate( 'GLOBAL:textDynamicEligibility_Fail_Message', { name: this.richTextMap[value] }, `__name__ ${defaultText}` ); } else { return this.i18n.translate( `GLOBAL:textEligibility_Fail_Message`, {}, defaultText ); } } } onTopLevelFilterChange (filter: TopLevelFilter) { // When the checkbox dropdown changes, find the appropriate default lang based on filter value if ( this.getDefaultLangOnFilterChange && filter.column === this.dropdownColumnName ) { const defaultLang = this.getDefaultLangOnFilterChange(filter.value); this.updateLangFormGroup(defaultLang); } } updateLangFormGroup (lang: string) { const control = this.langFormGroup.get('defaultLang'); control.setValue(lang); } setLangKeysForTranslation () { this.langKeysForTranslation = this.langKeys.filter((lang) => { return lang !== this.langFormGroup.value.defaultLang; }); } onTranslationEntered () { this.saveOnLangChange = true; } async translationLangChanged () { // If changes were made to previous lang we need to save if (this.saveOnLangChange) { await this.saveChanges(); } this.saveOnLangChange = false; this.setTranslationFormGroup(this.repository.currentSet); } async setTranslationFormGroup (rows: BaseTranslation[]) { const lang = this.langFormGroup.value.translationLang; const formGroupValue: TranslationFormGroup = {}; rows.forEach((row) => { this.getRichTextAttributeName(row); const found = row.translations.find((item) => { return item.language === lang; }); if (!found) { row.translations = [ ...row.translations, { language: lang, translation: '' } ]; } const key = `${row.languageKeyId}-${lang}`; const value = found ? found.translation : ''; if ( this.translationFormGroup && this.translationFormGroup.get(key) ) { this.translationFormGroup.get(key).setValue(value); } else { formGroupValue[key] = value; } }); // Reset this because the above set value fired an on change this.saveOnLangChange = false; if (this.translationFormGroup) { Object.keys(formGroupValue).forEach((key) => { this.translationFormGroup.addControl( key, new FormControl(formGroupValue[key]) ); }); } else { this.translationFormGroup = this.formBuilder.group(formGroupValue); this.sub.add(this.translationFormGroup.valueChanges.subscribe(() => { this.footerState.set( 'footerActionDisabled', !this.translationFormGroup.valid ); })); } } updateHiddenFilter () { this.repository.setHiddenFilters([ new TopLevelFilter( 'equals', 'defaultLanguage', this.langFormGroup.value.defaultLang, undefined ) ]); this.setLangKeysForTranslation(); } async exportTranslations () { const proceed = await this.modalFactory.open( ExportTranslationsModalComponent, { } ); if (proceed) { await this.translationService.exportTranslations( this.langKeys, this.repoName, this.dropdownColumnName, this.dropdownOptionsAttr, this.getTranslationsFunctionName, this.dropdownOptions ); } } async importTranslations () { await this.translationService.importTranslations( this.langKeys, this.repoName, this.bulkUpdateFunctionName ); } async translateRichTextModal (item: BaseTranslation, isEdit = false) { const lang = isEdit ? this.langFormGroup.value.translationLang : this.langFormGroup.value.defaultLang; const modalSubHeader = this.translationService.translateLanguageName(lang); const key = item.languageKeyId + '-' + this.langFormGroup.value.translationLang; const response = await this.modalFactory.open( TranslateRichTextModalComponent, { item, modalHeader: item.richLinkText, modalSubHeader, isEdit, translationValue: isEdit ? this.translationFormGroup.value[key] : '', ckEditorConfig: this.ckEditorConfigForRichText } ); if ( isEdit && response && !isUndefined(response.newValue) ) { this.translationFormGroup.get(key).setValue(response.newValue); await this.saveChanges(); } } async saveChanges (skipToastr = false) { const map: { [x: string]: Translation[]; } = {}; Object.keys(this.translationFormGroup.controls).forEach((controlName) => { const control = this.translationFormGroup.get(controlName); const translation = control.value; // only save if a translation exists, OR the control has been touched if (!!translation || !control.pristine) { const vals = controlName.split('-'); const id = vals[0]; const language = `${vals[1]}-${vals[2]}`; map[id] = [ ...map[id] || [], { language, translation } ]; } }); const bulkUpdateInfo = Object.keys(map).map((key) => { return { languageKeyId: +key, translations: map[key] || [] }; }); await this.translationService.bulkUpdateTranslations( bulkUpdateInfo, this.repoName, this.bulkUpdateFunctionName, skipToastr ); } machineTranslateSingle = (item: BaseTranslation) => async () => { this.spinnerService.startSpinner(); await this.translationService.machineTranslateSingle( item.languageKeyId, this.langFormGroup.value.translationLang, item.defaultTranslationText, this.repoName, this.machineTranslateSingleFunctionName ); this.spinnerService.stopSpinner(); }; async machineTranslateBulk () { const translationLangKey = this.langFormGroup.value.translationLang; const langToTranslate = this.translationService.translateLanguageName( translationLangKey ); const deps = { modalHeader: this.i18n.translate( 'GLOBAL:hdrMachineTranslate' ), confirmButtonText: this.i18n.translate( 'GLOBAL:textMachineTranslate', {}, 'Machine translate' ), confirmText: this.i18n.translate( 'GLOBAL:textConfirmMachineTranslateAllEmptyToLanguage', { language: langToTranslate }, 'Are you sure you want to machine translate all empty fields in __language__?' ) }; const proceed = await this.modalFactory.open( ConfirmationModalComponent, deps ); if (proceed) { this.spinnerService.startSpinner(); await this.saveChanges(true); await this.translationService.machineTranslateBulk( [translationLangKey], this.repoName, this.machineTranslateBulkFunctionName ); this.spinnerService.stopSpinner(); } } ngOnDestroy () { this.sub.unsubscribe(); this.footerState.clearAll(); } }