src/app/core/config/configure-translations-popup/configure-translations-popup.component.ts
Edit the translations of one configurable text.
Works on the raw value and returns a raw value, never resolving anything, so no language can be lost. A plain string with no language added stays a string.
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-configure-translations-popup |
| standalone | true |
| imports |
MatDialogModule
MatFormFieldModule
MatInputModule
MatButtonModule
|
| styleUrls | ./configure-translations-popup.component.scss |
| templateUrl | ./configure-translations-popup.component.html |
No results matching.
MatDialogModule
MatFormFieldModule
MatInputModule
MatButtonModule
FormsModule
DialogCloseComponent
Properties |
|
Methods |
| onCancel |
onCancel()
|
|
Returns :
void
|
| onSave |
onSave()
|
|
Returns :
void
|
| Readonly data |
Type : unknown
|
Default value : inject<ConfigureTranslationsDialogData>(MAT_DIALOG_DATA)
|
| Readonly disabledCount |
Type : unknown
|
Default value : this.rows.filter((r) => !r.enabled).length
|
|
number of rows of disabled languages (hidden until expanded) |
| rows |
Type : TranslationRow[]
|
Default value : this.buildRows()
|
| Readonly showDisabled |
Type : unknown
|
Default value : signal(false)
|
|
whether rows of disabled languages are shown; they are hidden by default |
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from "@angular/core";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatButtonModule } from "@angular/material/button";
import { FormsModule } from "@angular/forms";
import { DialogCloseComponent } from "../../common-components/dialog-close/dialog-close.component";
import { availableLocales } from "../../language/languages";
import { SiteSettingsService } from "../../site-settings/site-settings.service";
import { ConfigurableEnumValue } from "../../basic-datatypes/configurable-enum/configurable-enum.types";
import {
getActiveLocale,
isLocaleTranslationMap,
} from "../../language/active-locale";
import { pickForLocale, TranslatableText } from "../multi-lingual-config";
export interface ConfigureTranslationsResult {
/** the new value, or undefined when every language was cleared */
value: TranslatableText | undefined;
}
export interface ConfigureTranslationsDialogData {
/** the raw configured value: a plain string or a per-language map */
value?: TranslatableText;
/** name of the setting being translated, shown in the dialog */
fieldLabel?: string;
/** edit each language in a multi-line textarea, so line breaks are kept */
multiline?: boolean;
/** number of visible lines of a multi-line text */
rows?: number;
}
interface TranslationRow {
locale: string;
localeLabel: string;
/** false if this language is disabled for the system, and only keeps existing text */
enabled: boolean;
text: string;
}
/**
* Edit the translations of one configurable text.
*
* Works on the *raw* value and returns a raw value, never resolving anything, so
* no language can be lost. A plain string with no language added stays a string.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-configure-translations-popup",
templateUrl: "./configure-translations-popup.component.html",
styleUrls: ["./configure-translations-popup.component.scss"],
imports: [
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
FormsModule,
DialogCloseComponent,
],
})
export class ConfigureTranslationsPopupComponent {
private readonly siteSettings = inject(SiteSettingsService);
readonly data = inject<ConfigureTranslationsDialogData>(MAT_DIALOG_DATA);
private readonly dialogRef =
inject<MatDialogRef<ConfigureTranslationsPopupComponent>>(MatDialogRef);
rows: TranslationRow[] = this.buildRows();
/** number of rows of disabled languages (hidden until expanded) */
readonly disabledCount = this.rows.filter((r) => !r.enabled).length;
/** whether rows of disabled languages are shown; they are hidden by default */
readonly showDisabled = signal(false);
/**
* One row per enabled language (the default language first, then in their order), followed by every
* other language this text already holds text for. {@link buildValue} rebuilds
* the value from the rows, so leaving one out would delete that text on save.
* Rows of disabled languages are only hidden in the template
* (see {@link showDisabled}), never left out of the rows.
*
* Existing text means: a key of a translation map (a base-language key like `en`
* counts for `en-US`), or, for a plain string, the active language, which
* {@link initialTextFor} files it under (so both cases are one check). A disabled language that holds
* no text gets no row, so admins are not invited to write texts in it.
*/
private buildRows(): TranslationRow[] {
const defaultId = this.siteSettings.defaultLocale()?.id;
// the default language always comes first, the others keep their order
const enabled = [...this.siteSettings.enabledLocales()].sort(
(a, b) => Number(b.id === defaultId) - Number(a.id === defaultId),
);
const isEnabled = (locale: ConfigurableEnumValue) =>
enabled.some(({ id }) => id === locale.id);
const existing = availableLocales.values.filter(
(locale) => !isEnabled(locale) && !!this.initialTextFor(locale.id),
);
return [
...enabled.map((locale) => ({ locale, enabled: true })),
...existing.map((locale) => ({ locale, enabled: false })),
].map(({ locale, enabled }) => ({
locale: locale.id,
localeLabel: locale.label,
enabled,
text: this.initialTextFor(locale.id),
}));
}
private initialTextFor(locale: string): string {
const value = this.data?.value;
if (isLocaleTranslationMap(value)) {
// also reads a hand-written base-language key (`en` for `en-US`), so that
// saving normalises it to the locale id instead of dropping it
return pickForLocale(value, locale) ?? "";
}
// a plain string becomes the translation of the language the admin is reading
// it in: on a German instance, filing it under English would show the wrong text
return locale === getActiveLocale() && typeof value === "string"
? value
: "";
}
onSave() {
this.dialogRef.close({ value: this.buildValue() });
}
onCancel() {
// no result at all, so the caller keeps the existing value
this.dialogRef.close(undefined);
}
/**
* Build the new raw value. Only the active language on its own collapses back to
* a plain string, and only if the dialog was opened with one: a map saved as a
* string would look unchanged to the config merge, which would then restore the
* translations just removed.
*/
private buildValue(): TranslatableText | undefined {
const filled = this.rows.filter((row) => !!row.text?.trim());
if (filled.length === 0) {
return undefined;
}
if (
filled.length === 1 &&
filled[0].locale === getActiveLocale() &&
!isLocaleTranslationMap(this.data?.value)
) {
return filled[0].text.trim();
}
const map: Record<string, string> = {};
for (const row of filled) {
map[row.locale] = row.text.trim();
}
return map;
}
}
<h1 matDialogTitle i18n="title of the translations popup dialog">
Configure translations
</h1>
<app-dialog-close (click)="onCancel()"></app-dialog-close>
<mat-dialog-content class="dialog-content">
@if (data.fieldLabel) {
<p i18n>Translations for "{{ data.fieldLabel }}"</p>
}
@for (row of rows; track row.locale) {
@if (row.enabled || showDisabled()) {
<mat-form-field class="full-width">
<mat-label>
@if (row.enabled) {
{{ row.localeLabel }}
} @else {
{{ row.localeLabel }}
<span class="disabled-marker" i18n>(disabled)</span>
}
</mat-label>
@if (data.multiline) {
<textarea
matInput
[(ngModel)]="row.text"
[name]="row.locale"
[rows]="data.rows ?? 3"
></textarea>
} @else {
<input matInput [(ngModel)]="row.text" [name]="row.locale" />
}
</mat-form-field>
}
}
@if (disabledCount > 0) {
<button
mat-button
type="button"
class="toggle-disabled"
(click)="showDisabled.set(!showDisabled())"
>
@if (showDisabled()) {
<span i18n>{disabledCount, plural,
=1 {Hide 1 disabled language}
other {Hide {{ disabledCount }} disabled languages}
}</span>
} @else {
<span i18n>{disabledCount, plural,
=1 {Show 1 disabled language}
other {Show {{ disabledCount }} disabled languages}
}</span>
}
</button>
}
<p class="hint-text" i18n>
Languages left empty fall back to the text of the default language.
</p>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button color="accent" (click)="onSave()" i18n>Save</button>
<button mat-raised-button (click)="onCancel()" i18n>Cancel</button>
</mat-dialog-actions>
./configure-translations-popup.component.scss
@use "variables/colors";
.dialog-content {
min-width: 350px;
padding-top: 5px;
}
.hint-text {
opacity: 0.7;
}
.toggle-disabled {
margin-bottom: 8px;
}
.disabled-marker {
color: colors.$warn;
}