Skip to content

Commit

Permalink
fix: localization panel freeze if too many translations
Browse files Browse the repository at this point in the history
Only create form for inputs to be displayed and limit the
number of translation shown on the panel.

Encourage the user to use the search bar to find their translation.
  • Loading branch information
cpaulve-1A committed Jun 18, 2024
1 parent 2390a8d commit 412f1c0
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, effect, inject, type OnDestroy, type Signal, untracked, viewChild, ViewEncapsulation } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DfTooltipModule } from '@design-factory/design-factory';
import { NgbAccordionDirective, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import {AsyncPipe} from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
type OnDestroy,
type Signal,
untracked,
viewChild,
ViewEncapsulation
} from '@angular/core';
import {toSignal} from '@angular/core/rxjs-interop';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {DfTooltipModule} from '@design-factory/design-factory';
import {NgbAccordionDirective, NgbAccordionModule} from '@ng-bootstrap/ng-bootstrap';
import type {
JSONLocalization,
LocalizationMetadata
} from '@o3r/localization';
import { Subscription } from 'rxjs';
import { map, throttleTime } from 'rxjs/operators';
import { ChromeExtensionConnectionService } from '../../services/connection.service';
import { LocalizationService, StateService } from '../../services';
import {Subscription} from 'rxjs';
import {map, throttleTime} from 'rxjs/operators';
import {ChromeExtensionConnectionService} from '../../services/connection.service';
import {LocalizationService, StateService} from '../../services';

const THROTTLE_TIME = 100;

Expand Down Expand Up @@ -44,6 +55,7 @@ export class LocalizationPanelPresComponent implements OnDestroy {
public readonly filteredLocalizations: Signal<LocalizationMetadata>;
public readonly languages = this.localizationService.languages;
public readonly hasSeveralLanguages: Signal<boolean>;
public readonly isTruncated: Signal<boolean>;
public readonly localizationActiveStateOverridesForCurrentLang = computed(() => {
const lang = this.currentLanguage();
if (!lang) {
Expand Down Expand Up @@ -89,18 +101,24 @@ export class LocalizationPanelPresComponent implements OnDestroy {
const search = toSignal(
this.form.controls.search.valueChanges.pipe(
map((text) => text?.toLowerCase() || ''),
throttleTime(THROTTLE_TIME, undefined, { trailing: true })
throttleTime(THROTTLE_TIME, undefined, {trailing: true})
),
{ initialValue: '' }
{initialValue: ''}
);
const searchMatch = computed(() => {
const searchText = search();
return searchText ?
this.localizations().filter(({ key, description, tags, ref }) =>
[key, description, ...(tags || []), ref].some((value) => value?.toLowerCase().includes(searchText))
) : this.localizations();
});

this.filteredLocalizations = computed(() => {
const searchText = search();
return searchText
? this.localizations().filter(({ key, description, tags, ref }) => [key, description, ...(tags || []), ref].some((value) => value?.toLowerCase().includes(searchText)))
: this.localizations();
return searchMatch().slice(0, 20);
});

this.isTruncated = computed(() => this.filteredLocalizations().length < searchMatch().length);

effect(() => {
const lang = this.currentLanguage();
if (lang) {
Expand All @@ -120,34 +138,13 @@ export class LocalizationPanelPresComponent implements OnDestroy {
})
);
effect(() => {
const translations = this.localizationService.translationsForCurrentLanguage();
const translations = this.filteredLocalizations();
const lang = untracked(this.currentLanguage);
if (!lang) {
return;
}
let langControl = this.form.controls.translations.controls[lang];
if (!langControl) {
langControl = new FormGroup<Record<string, TranslationControl>>({});
this.form.controls.translations.addControl(lang, langControl);
}
Object.entries(translations).forEach(([key, value]) => {
const control = langControl.controls[key];
const initialValue =
untracked(this.stateService.localState).localizations?.[this.form.value.lang || '']?.[key]
|| value
|| untracked(this.localizationService.localizationsMetadata).find((loc) => loc.key === key)?.value
|| '';
if (!control) {
const newControl = new FormControl<string>(initialValue);
langControl.addControl(key, newControl);
this.subscription.add(
newControl.valueChanges.pipe(
throttleTime(THROTTLE_TIME, undefined, { trailing: true })
).subscribe((newValue) => this.onLocalizationChange(key, newValue ?? ''))
);
} else {
control.setValue(initialValue);
}
translations.forEach(({key}) => {
this.upsertKeyForm(key, lang);
});
});
effect(() => {
Expand All @@ -166,14 +163,33 @@ export class LocalizationPanelPresComponent implements OnDestroy {
control.disable();
}
});
const locOverride = computed(() => this.stateService.localState().localizations || {});
effect(() => {
Object.entries(locOverride()).forEach(([lang, overridePerLang]) =>
Object.entries(overridePerLang).forEach(([key, value]) =>
this.form.controls.translations.controls[lang]?.controls[key]?.setValue(value)
)
}

private upsertKeyForm(key: string, lang: string) {
let langControl = this.form.controls.translations.controls[lang];
if (!langControl) {
langControl = new FormGroup<Record<string, TranslationControl>>({});
this.form.controls.translations.addControl(lang, langControl);
}
const control = langControl.controls[key];
const controlValue =
this.stateService.localState().localizations?.[this.form.value.lang || '']?.[key]
|| untracked(this.localizationService.localizationsMetadata).find((loc) => loc.key === key)?.value
|| '';
if (!control) {
const newControl = new FormControl<string>(controlValue);
langControl.addControl(key, newControl);
this.subscription.add(
newControl.valueChanges.pipe(
throttleTime(THROTTLE_TIME, undefined, {trailing: true})
).subscribe((newValue) => {
this.onLocalizationChange(key, newValue ?? '');
})
);
});
} else if (control.value !== controlValue) {
control.setValue(controlValue);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ <h3 ngbAccordionHeader>
} @empty {
<h3>No localization found for your search.</h3>
}
@if (isTruncated()) {
<span>Too many matches for this filter, please be more specific in your search.</span>
}
</div>
</ng-container>
} @else {
Expand Down
7 changes: 5 additions & 2 deletions apps/chrome-devtools/src/services/localization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ export class LocalizationService {

constructor() {
effect(() => {
this.connectionService.sendMessage('switchLanguage', { language: this.currentLanguage() });
this.connectionService.sendMessage('requestMessages', { only: ['getTranslationValuesContentMessage'] });
const currentLanguage = this.currentLanguage();
if (currentLanguage) {
this.connectionService.sendMessage('switchLanguage', { language: this.currentLanguage() });
this.connectionService.sendMessage('requestMessages', { only: ['getTranslationValuesContentMessage'] });
}
});
const externalSwitchLanguage = toSignal(
this.connectionService.message$.pipe(
Expand Down
13 changes: 4 additions & 9 deletions apps/chrome-devtools/src/services/state.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,21 @@ export class StateService {

effect(() => {
const state = this.activeState();
const languages = untracked(this.languages);
this.updateLocalState(state || {}, true);
// TODO reset configuration (is it possible? based on default value from metadata if present?)
// Reset all languages before applying override of the new state
untracked(this.languages).forEach((lang) => this.connectionService.sendMessage('reloadLocalizationKeys', { lang }));
// Reset all styling variables before applying override of the new state
this.connectionService.sendMessage('resetStylingVariables', {});
if (!state) {
languages.forEach((lang) => this.connectionService.sendMessage('reloadLocalizationKeys', { lang }));
this.connectionService.sendMessage('unselectState', {});
return;
}
languages.forEach((lang) => this.connectionService.sendMessage('reloadLocalizationKeys',
{ lang, ...state.localizations && state.localizations[lang] ? {overrides: state.localizations[lang]} : {} }));
Object.entries(state.configurations || {}).forEach(([id, configValue]) => {
this.connectionService.sendMessage('updateConfig', { id, configValue });
});
Object.entries(state.localizations || {}).forEach(([lang, overrides]) => {
Object.entries(overrides).forEach(([key, value]) => {
this.connectionService.sendMessage('updateLocalization', {
key, value, lang
});
});
});
if (state.stylingVariables && Object.keys(state.stylingVariables).length) {
this.connectionService.sendMessage('updateStylingVariables', {
variables: state.stylingVariables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface UpdateLocalizationContentMessage extends OtterMessageContent<'u
export interface ReloadLocalizationKeysContentMessage extends OtterMessageContent<'reloadLocalizationKeys'> {
/** Lang */
lang?: string;
overrides?: {[key: string]: string};
}

export interface IsTranslationDeactivationEnabledContentMessage extends OtterMessageContent<'isTranslationDeactivationEnabled'> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class LocalizationDevtoolsConsoleService implements DevtoolsServiceInterf
/**
* @inheritdoc
*/
public reloadLocalizationKeys(language?: string) {
return this.localizationDevtools.reloadLocalizationKeys(language);
public reloadLocalizationKeys(language?: string, overrides?: {[key: string]: string}) {
return this.localizationDevtools.reloadLocalizationKeys(language, overrides);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class LocalizationDevtoolsMessageService implements OnDestroy {
break;
}
case 'reloadLocalizationKeys': {
void this.localizationDevTools.reloadLocalizationKeys(message.lang);
void this.localizationDevTools.reloadLocalizationKeys(message.lang, message.overrides);
break;
}
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class OtterLocalizationDevtools {
* @see https://github.com/ngx-translate/core/blob/master/packages/core/lib/translate.service.ts#L490
* @param language language to reload
*/
public async reloadLocalizationKeys(language?: string) {
public async reloadLocalizationKeys(language?: string, overrides?: {[key: string]: string}) {
const lang = language || this.getCurrentLanguage();
if ((this.translateCompiler as TranslateMessageFormatLazyCompiler).clearCache) {
(this.translateCompiler as TranslateMessageFormatLazyCompiler).clearCache();
Expand All @@ -96,6 +96,9 @@ export class OtterLocalizationDevtools {
language || this.getCurrentLanguage(),
initialLocs
);
Object.entries(overrides || {}).forEach(([key, value]) => {
this.localizationService.getTranslateService().set(key, value, lang);
});
this.appRef.tick();
}
}
3 changes: 2 additions & 1 deletion packages/@o3r/styling/builders/style-extractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export default createBuilder(createBuilderWithMetricsIfInstalled<StyleExtractorB
cssVarList
.forEach((item) => {
acc.variables[item.name] = item;
delete (acc.variables[item.name] as any).name;
// Why do we delete this item name?
// delete (acc.variables[item.name] as any).name;
});
return acc;
}, previousMetadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ const isStylingMessage = (message: any): message is AvailableStylingMessageConte

const getCSSRulesAppliedOnRoot = () => Array.from(document.styleSheets)
.reverse()
.reduce((acc: CSSStyleRule[], styleSheet) => acc.concat(
Array.from(styleSheet.cssRules || styleSheet.rules)
.reverse()
.filter((rule): rule is CSSStyleRule => rule instanceof CSSStyleRule && /\b:root\b/.test(rule.selectorText))
), []);
.reduce((acc: CSSStyleRule[], styleSheet) => {
let rules;
try {
rules = styleSheet.cssRules || styleSheet.rules;
} catch {
console.debug(`Could not access CSS rule for ${JSON.stringify(styleSheet.href)}`);
}

return acc.concat(
Array.from(rules || [])
.reverse()
.filter((rule): rule is CSSStyleRule => rule instanceof CSSStyleRule && /\b:root\b/.test(rule.selectorText))
);
}, []);

const getCSSVariableValueInCSSStyleDeclaration = (variableName: string, style: CSSStyleDeclaration) =>
style.getPropertyValue(variableName).trim();
Expand Down

0 comments on commit 412f1c0

Please sign in to comment.