Skip to content

Commit

Permalink
feat(chrome-ext): localization panel (#1513)
Browse files Browse the repository at this point in the history
## Proposed change

![image](https://github.com/AmadeusITGroup/otter/assets/52541061/e04504ed-037a-460c-b3ee-8a204d4d1f38)

## Related issues

- 🚀 Feature #1499

<!-- Please make sure to follow the contributing guidelines on
https://github.com/amadeus-digital/Otter/blob/main/CONTRIBUTING.md -->
  • Loading branch information
matthieu-crouzet committed Mar 21, 2024
2 parents a9afceb + b6c78ba commit b1ba3cc
Show file tree
Hide file tree
Showing 16 changed files with 529 additions and 57 deletions.
6 changes: 6 additions & 0 deletions apps/chrome-devtools/src/app-devtools/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
</ng-template>
</li>
<li [ngbNavItem]="5">
<a ngbNavLink>Localization</a>
<ng-template ngbNavContent>
<o3r-localization-panel-pres></o3r-localization-panel-pres>
</ng-template>
</li>
<li [ngbNavItem]="6">
<a ngbNavLink>Theming</a>
<ng-template ngbNavContent>
Coming soon...
Expand Down
3 changes: 3 additions & 0 deletions apps/chrome-devtools/src/app-devtools/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { RulesetHistoryService } from '../services/ruleset-history.service';
import { ConfigPanelPresComponent } from './config-panel/config-panel-pres.component';
import { DebugPanelPresComponent } from './debug-panel/debug-panel-pres.component';
import { DebugPanelService } from './debug-panel/debug-panel.service';
import { LocalizationPanelPresComponent } from './localization-panel/localization-panel-pres.component';


@Component({
selector: 'app-root',
Expand All @@ -23,6 +25,7 @@ import { DebugPanelService } from './debug-panel/debug-panel.service';
ConfigPanelPresComponent,
ComponentPanelPresComponent,
AppConnectionComponent,
LocalizationPanelPresComponent,
AsyncPipe
]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class ComponentPanelPresComponent implements OnDestroy {
);
this.connectionService.sendMessage(
'requestMessages',
{ only: 'isComponentSelectionAvailable' }
{ only: ['isComponentSelectionAvailable'] }
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class ConfigPanelPresComponent {
connectionService.sendMessage(
'requestMessages',
{
only: 'configurations'
only: ['configurations']
}
);
const configs$ = connectionService.message$.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@ export class DebugPanelPresComponent {
});
}

/**
* Toggle localization key display
* @param event
*/
public toggleLocalizationKey(event: UIEvent) {
this.connection.sendMessage('displayLocalizationKeys', {
toggle: (event.target as HTMLInputElement).checked
});
}

/**
* Toggle visual testing mode
* @param event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ <h4 class="d-inline-block">Information</h4>
<div>
<h4>Actions</h4>
<div>
<div class="form-check">
<input class="form-check-input" type="checkbox" (change)="toggleLocalizationKey($event)" id="displayLocalizationKey">
<label class="form-check-label" for="displayLocalizationKey">
Display localization keys
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" (change)="toggleVisualTestingRender($event)" id="toggleVisualTesting">
<label class="form-check-label" for="toggleVisualTesting">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, type OnDestroy, ViewEncapsulation } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import type {
GetTranslationValuesContentMessage,
IsTranslationDeactivationEnabledContentMessage,
LanguagesContentMessage,
LocalizationMetadata,
LocalizationsContentMessage,
SwitchLanguageContentMessage
} from '@o3r/localization';
import { combineLatest, Observable, Subscription } from 'rxjs';
import { filter, map, shareReplay, startWith, throttleTime } from 'rxjs/operators';
import { ChromeExtensionConnectionService } from '../../services/connection.service';

@Component({
selector: 'o3r-localization-panel-pres',
templateUrl: './localization-panel-pres.template.html',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
NgbAccordionModule,
ReactiveFormsModule,
FormsModule,
AsyncPipe
]
})
export class LocalizationPanelPresComponent implements OnDestroy {
public isTranslationDeactivationEnabled$: Observable<boolean>;
public localizations$: Observable<LocalizationMetadata>;
public filteredLocalizations$: Observable<LocalizationMetadata>;
public languages$: Observable<string[]>;
public form = new FormGroup({
search: new FormControl(''),
lang: new FormControl(''),
showKeys: new FormControl(false),
translations: new UntypedFormGroup({})
});

private readonly connectionService = inject(ChromeExtensionConnectionService);

private readonly subscription = new Subscription();

constructor() {
this.connectionService.sendMessage(
'requestMessages',
{
only: [
'localizations',
'languages',
'switchLanguage',
'isTranslationDeactivationEnabled'
]
}
);
this.languages$ = this.connectionService.message$.pipe(
filter((message): message is LanguagesContentMessage => message.dataType === 'languages'),
map((message) => message.languages),
shareReplay({bufferSize: 1, refCount: true})
);
this.localizations$ = this.connectionService.message$.pipe(
filter((message): message is LocalizationsContentMessage => message.dataType === 'localizations'),
map((message) => message.localizations.filter((localization) => !localization.dictionary)),
shareReplay({bufferSize: 1, refCount: true})
);
this.filteredLocalizations$ = combineLatest([
this.localizations$,
this.form.controls.search.valueChanges.pipe(
map((search) => search?.toLowerCase()),
throttleTime(500),
startWith('')
)
]).pipe(
map(([localizations, search]) => search
? localizations.filter(({ key, description, tags, ref }) =>
[key, description, ...(tags || []), ref].some((value) => value?.toLowerCase().includes(search))
)
: localizations
),
startWith([])
);
this.isTranslationDeactivationEnabled$ = this.connectionService.message$.pipe(
filter((message): message is IsTranslationDeactivationEnabledContentMessage => message.dataType === 'isTranslationDeactivationEnabled'),
map((message) => message.enabled),
shareReplay({bufferSize: 1, refCount: true})
);
const currLang$ = this.connectionService.message$.pipe(
filter((message): message is SwitchLanguageContentMessage => message.dataType === 'switchLanguage'),
map((message) => message.language),
shareReplay({bufferSize: 1, refCount: true})
);
this.subscription.add(currLang$.subscribe((lang) => {
this.form.controls.lang.setValue(lang);
this.connectionService.sendMessage('requestMessages', {
only: ['getTranslationValuesContentMessage']
});
}));
this.subscription.add(
this.form.controls.lang.valueChanges.subscribe((language) => {
if (language) {
this.connectionService.sendMessage('switchLanguage', { language });
this.connectionService.sendMessage(
'requestMessages',
{
only: [
'getTranslationValuesContentMessage'
]
}
);
}
})
);
this.subscription.add(
this.form.controls.showKeys.valueChanges.subscribe((value) => {
this.connectionService.sendMessage('displayLocalizationKeys', { toggle: !!value });
})
);
this.subscription.add(
this.connectionService.message$.pipe(
filter((message): message is GetTranslationValuesContentMessage => message.dataType === 'getTranslationValuesContentMessage'),
map((message) => message.translations)
).subscribe((translations) => {
const translationControl = this.form.controls.translations;
Object.entries(translations).forEach(([key, value]) => {
const control = translationControl.controls[key];
if (!control) {
const newControl = new FormControl<string>(value);
translationControl.addControl(key, newControl);
this.subscription.add(
newControl.valueChanges.pipe(
throttleTime(500)
).subscribe((newValue) => this.onLocalizationChange(key, newValue ?? ''))
);
} else {
control.setValue(value, { emitEvent: false });
}
});
})
);
this.subscription.add(
this.isTranslationDeactivationEnabled$.subscribe((enabled) => {
const control = this.form.controls.showKeys;
if (enabled) {
control.enable();
} else {
control.disable();
}
})
);
this.subscription.add(
this.languages$.subscribe((languages) => {
const control = this.form.controls.lang;
if (languages.length >= 2) {
control.enable();
} else {
control.disable();
}
})
);
}

/**
* Change localization key value
* @param localizationKey
* @param newValue
*/
private onLocalizationChange(localizationKey: string, newValue: string) {
this.connectionService.sendMessage('updateLocalization', {
key: localizationKey,
value: newValue
});
}

/**
* Reset localization change for current language
*/
public resetChange() {
this.connectionService.sendMessage('reloadLocalizationKeys', {});
this.connectionService.sendMessage(
'requestMessages',
{
only: [
'getTranslationValuesContentMessage'
]
}
);
}

public ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<form class="mb-2" [formGroup]="form">
<div class="mb-2 d-flex align-items-center gap-4">
@if (languages$ | async; as languages) {
<div class="input-group" [style.maxWidth]="'300px'">
<label class="input-group-text" for="language">Language</label>
<select class="form-select" id="language" formControlName="lang" [attr.aria-describedby]="languages.length < 2 ? 'language-hint' : null">
@for (lang of languages; track lang) {
<option [value]="lang">{{ lang }}</option>
}
</select>
@if (languages.length < 2) {
<div id="language-hint" class="form-text text-warning w-100">
You have only one language.
</div>
}
</div>
}
<div class="form-check d-flex flex-column gap-2">
<div>
<input class="form-check-input" type="checkbox" formControlName="showKeys" id="displayLocalizationKey" [attr.aria-describedby]="(isTranslationDeactivationEnabled$ | async) ? null : 'show-keys-hint'">
<label class="form-check-label" for="displayLocalizationKey">
Display localization keys
</label>
</div>
@if ((isTranslationDeactivationEnabled$ | async) === false) {
<div id="show-keys-hint" class="text-warning" [style.fontSize]="'.5em'" [style.marginLeft]="'-2.5em'">
Translation deactivation is not enabled. Please set the LocalizationConfiguration property "enableTranslationDeactivation" accordingly.
</div>
}
</div>
@if ((localizations$ | async)?.length) {
<button (click)="resetChange()" type="button" class="btn btn-outline-danger">
Reset change for {{ form.controls.lang.value }}
</button>
}
</div>
@if ((localizations$ | async)?.length) {
<div class="mb-2 input-group">
<label class="input-group-text" for="search-localization">
<i class="mx-1 icon-search" aria-label="Search"></i>
</label>
<input class="form-control" formControlName="search" type="text" id="search-localization" placeholder="Search for localization" />
</div>
}
</form>
@if ((localizations$ | async)?.length) {
@if (filteredLocalizations$ | async; as filteredLocalizations) {
@if (filteredLocalizations.length) {
<form [formGroup]="form.controls.translations">
<div ngbAccordion [closeOthers]="true" #acc="ngbAccordion">
@for (localization of filteredLocalizations; track localization.key) {
<div ngbAccordionItem>
<h3 ngbAccordionHeader>
<button ngbAccordionButton>{{localization.key}}</button>
</h3>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="form-group">
<input [attr.aria-describedby]="localization.key" class="form-control" type="text" [formControlName]="localization.key" />
<div [id]="localization.key" class="form-text">{{localization.description}}</div>
</div>
</ng-template>
</div>
</div>
</div>
}
</div>
</form>
} @else {
<h3>No localization found for your search.</h3>
}
}
} @else {
<h3>No metadata provided for localization.</h3>
<p>
To provide metadata you can read the following
<a href="https://github.com/AmadeusITGroup/otter/blob/main/docs/dev-tools/chrome-devtools.md#how-to-enable-more-features-by-providing-metadata-files" target="_blank" rel="noopener">
documentation
</a>
</p>
}
5 changes: 5 additions & 0 deletions apps/showcase/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"glob": "**/*.json",
"input": "apps/showcase/dev-resources/localizations",
"output": "/localizations"
},
{
"glob": "**/*.metadata.json",
"input": "apps/showcase",
"output": "/metadata"
}
],
"styles": [
Expand Down
19 changes: 18 additions & 1 deletion docs/dev-tools/chrome-devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,24 @@ export class AppComponent {
```

> [!TIP]
> The services can be also activated at bootstrap time by providing `isActivatedOnBootstrap: true` to their dedicated token `OTTER_<module>_DEVTOOLS_OPTIONS` (example: `{provide: 'OTTER_CONFIGURATION_DEVTOOLS_OPTIONS', useValue: {isActivatedOnBootstrap: true}}`).
> The services can be also activated at bootstrap time by providing `isActivatedOnBootstrap: true` to their dedicated token `OTTER_<module>_DEVTOOLS_OPTIONS` (example: `{provide: 'OTTER_CONFIGURATION_DEVTOOLS_OPTIONS', useValue: {isActivatedOnBootstrap: true}}`). The services need to be injected in the application.
> `platformBrowserDynamic().bootstrapModule(AppModule).then((m) => runInInjectionContext(m.injector, () => inject(ConfigurationDevtoolsConsoleService)))`
### How to enable more features by providing metadata files

In your `angular.json` or `project.json`, you can specify `assets` in the options of `@angular-devkit/build-angular:application`.
```json
{
"glob": "**/*.metadata.json",
"input": "path/to/your/app",
"output": "/metadata"
}
```
> [!CAUTION]
> We recommend to add this asset entry only for the development configuration.
> [!NOTE]
> For the showcase application, we are exposing the metadata in production mode, to be able to showcase the chrome extension features easily.
## How to install the extension

Expand Down

0 comments on commit b1ba3cc

Please sign in to comment.