feat(project-editor): add Languages tab with generic locale sync

This commit is contained in:
sdarbinyan
2026-07-13 08:10:00 +04:00
parent 74e5099b04
commit b166920ad7
12 changed files with 237 additions and 1 deletions

View File

@@ -0,0 +1,119 @@
import { Injectable } from '@angular/core';
import { BootstrapConfig, FooterNavigationGroupConfig, NavigationItemConfig, NavigationLocalizedText } from '../../../shared/models/config';
type SyncMode = 'add' | 'remove';
@Injectable({ providedIn: 'root' })
export class LocaleSyncService {
addLocale(bootstrap: BootstrapConfig, locale: string): BootstrapConfig {
return this.sync(bootstrap, locale, 'add');
}
removeLocale(bootstrap: BootstrapConfig, locale: string): BootstrapConfig {
if (locale === bootstrap.localization.defaultLocale) {
return bootstrap;
}
return this.sync(bootstrap, locale, 'remove');
}
private sync(bootstrap: BootstrapConfig, rawLocale: string, mode: SyncMode): BootstrapConfig {
const locale = rawLocale.trim().toLowerCase();
if (!locale) {
return bootstrap;
}
const alreadyPresent = bootstrap.localization.supportedLocales.includes(locale);
if (mode === 'add' && alreadyPresent) {
return bootstrap;
}
if (mode === 'remove' && !alreadyPresent) {
return bootstrap;
}
const supportedLocales = mode === 'add'
? [...bootstrap.localization.supportedLocales, locale]
: bootstrap.localization.supportedLocales.filter(existing => existing !== locale);
return {
...bootstrap,
tenant: { ...bootstrap.tenant, supportedLocales },
localization: { ...bootstrap.localization, supportedLocales },
staticPages: this.syncStaticPages(bootstrap.staticPages, locale, mode),
navigation: {
...bootstrap.navigation,
header: this.syncNavItems(bootstrap.navigation.header, locale, mode),
footer: this.syncFooterNav(bootstrap.navigation.footer, locale, mode),
sidebar: bootstrap.navigation.sidebar ? this.syncNavItems(bootstrap.navigation.sidebar, locale, mode) : bootstrap.navigation.sidebar,
},
};
}
private syncStaticPages(staticPages: BootstrapConfig['staticPages'], locale: string, mode: SyncMode): BootstrapConfig['staticPages'] {
if (!staticPages || Array.isArray(staticPages)) {
return staticPages;
}
return Object.fromEntries(
Object.entries(staticPages).map(([id, page]) => [id, {
...page,
translations: this.syncRecord(page.translations, locale, mode, () => ({ title: '', html: '' })),
}])
);
}
private syncNavItems(items: NavigationItemConfig[], locale: string, mode: SyncMode): NavigationItemConfig[] {
return items.map(item => ({
...item,
label: this.syncLabel(item.label, locale, mode),
children: item.children ? this.syncNavItems(item.children, locale, mode) : item.children,
}));
}
private syncFooterNav(
footer: NavigationItemConfig[] | FooterNavigationGroupConfig[],
locale: string,
mode: SyncMode
): NavigationItemConfig[] | FooterNavigationGroupConfig[] {
if (footer.length === 0) {
return footer;
}
if ('items' in footer[0]) {
return (footer as FooterNavigationGroupConfig[]).map(group => ({
...group,
groupTitle: this.syncLabel(group.groupTitle, locale, mode),
items: group.items.map(item => ({ ...item, label: this.syncLabel(item.label, locale, mode) })),
}));
}
return this.syncNavItems(footer as NavigationItemConfig[], locale, mode);
}
private syncLabel(
label: string | NavigationLocalizedText | undefined,
locale: string,
mode: SyncMode
): string | NavigationLocalizedText | undefined {
if (label === undefined || typeof label === 'string') {
return label;
}
return this.syncRecord(label, locale, mode, () => Object.values(label)[0] ?? '');
}
private syncRecord<T>(
record: Record<string, T> | undefined,
locale: string,
mode: SyncMode,
createEmpty: () => T
): Record<string, T> {
const next: Record<string, T> = { ...(record ?? {}) };
if (mode === 'add') {
if (!(locale in next)) {
next[locale] = createEmpty();
}
} else {
delete next[locale];
}
return next;
}
}