mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +00:00
feat: add i18n infrastructure (#2854)
* feat: add i18n infrastructure * store langauge selection to patchDB ui section * feat: react to patchdb language change --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
1
web/package-lock.json
generated
1
web/package-lock.json
generated
@@ -125,6 +125,7 @@
|
|||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@noble/curves": "^1.4.0",
|
"@noble/curves": "^1.4.0",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
|
"deep-equality-data-structures": "^1.5.0",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
|||||||
import { Title } from '@angular/platform-browser'
|
import { Title } from '@angular/platform-browser'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||||
|
import { i18nService } from 'src/app/i18n/i18n.service'
|
||||||
import { ConnectionService } from './services/connection.service'
|
import { ConnectionService } from './services/connection.service'
|
||||||
import { PatchDataService } from './services/patch-data.service'
|
import { PatchDataService } from './services/patch-data.service'
|
||||||
import { DataModel } from './services/patch-db/data-model'
|
import { DataModel } from './services/patch-db/data-model'
|
||||||
@@ -15,6 +16,7 @@ import { PatchMonitorService } from './services/patch-monitor.service'
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
private readonly title = inject(Title)
|
private readonly title = inject(Title)
|
||||||
|
private readonly i18n = inject(i18nService)
|
||||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||||
|
|
||||||
readonly subscription = merge(
|
readonly subscription = merge(
|
||||||
@@ -37,9 +39,10 @@ export class AppComponent implements OnInit {
|
|||||||
startWith(true),
|
startWith(true),
|
||||||
)
|
)
|
||||||
|
|
||||||
async ngOnInit() {
|
ngOnInit() {
|
||||||
this.patch
|
this.patch.watch$('ui').subscribe(({ name, language }) => {
|
||||||
.watch$('ui', 'name')
|
this.title.setTitle(name || 'StartOS')
|
||||||
.subscribe(name => this.title.setTitle(name || 'StartOS'))
|
this.i18n.setLanguage(language)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy'
|
import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter, of, pairwise } from 'rxjs'
|
import { filter, of, pairwise } from 'rxjs'
|
||||||
|
import { I18N_PROVIDERS } from 'src/app/i18n/i18n.providers'
|
||||||
|
import { i18nService } from 'src/app/i18n/i18n.service'
|
||||||
import {
|
import {
|
||||||
PATCH_CACHE,
|
PATCH_CACHE,
|
||||||
PatchDbSource,
|
PatchDbSource,
|
||||||
@@ -43,6 +45,7 @@ const {
|
|||||||
|
|
||||||
export const APP_PROVIDERS: Provider[] = [
|
export const APP_PROVIDERS: Provider[] = [
|
||||||
NG_EVENT_PLUGINS,
|
NG_EVENT_PLUGINS,
|
||||||
|
I18N_PROVIDERS,
|
||||||
FilterPackagesPipe,
|
FilterPackagesPipe,
|
||||||
UntypedFormBuilder,
|
UntypedFormBuilder,
|
||||||
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }),
|
||||||
@@ -75,7 +78,6 @@ export const APP_PROVIDERS: Provider[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
deps: [StorageService, AuthService, ClientStorageService, Router],
|
|
||||||
useFactory: appInitializer,
|
useFactory: appInitializer,
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
@@ -100,16 +102,18 @@ export const APP_PROVIDERS: Provider[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function appInitializer(
|
export function appInitializer(): () => void {
|
||||||
storage: StorageService,
|
const storage = inject(StorageService)
|
||||||
auth: AuthService,
|
const auth = inject(AuthService)
|
||||||
localStorage: ClientStorageService,
|
const localStorage = inject(ClientStorageService)
|
||||||
router: Router,
|
const router = inject(Router)
|
||||||
): () => void {
|
const i18n = inject(i18nService)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
storage.migrate036()
|
storage.migrate036()
|
||||||
auth.init()
|
auth.init()
|
||||||
localStorage.init()
|
localStorage.init()
|
||||||
router.initialNavigation()
|
router.initialNavigation()
|
||||||
|
i18n.setLanguage(i18n.language)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
web/projects/ui/src/app/i18n/dictionaries/english.ts
Normal file
42
web/projects/ui/src/app/i18n/dictionaries/english.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export default {
|
||||||
|
ui: {
|
||||||
|
back: 'Back',
|
||||||
|
change: 'Change',
|
||||||
|
update: 'Update',
|
||||||
|
reset: 'Reset',
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
outlet: {
|
||||||
|
system: 'System',
|
||||||
|
general: 'General',
|
||||||
|
email: 'Email',
|
||||||
|
backup: 'Create Backup',
|
||||||
|
restore: 'Restore Backup',
|
||||||
|
interfaces: 'User Interface Addresses',
|
||||||
|
acme: 'ACME',
|
||||||
|
wifi: 'WiFi',
|
||||||
|
sessions: 'Active Sessions',
|
||||||
|
ssh: 'SSH',
|
||||||
|
password: 'Change Password',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: 'General Settings',
|
||||||
|
subtitle: 'Manage your overall setup and preferences',
|
||||||
|
update: 'Software Update',
|
||||||
|
restart: 'Restart to apply',
|
||||||
|
check: 'Check for updates',
|
||||||
|
tab: 'Browser Tab Title',
|
||||||
|
language: 'Language',
|
||||||
|
tor: 'Reset Tor',
|
||||||
|
daemon: 'Restart the Tor daemon on your server',
|
||||||
|
disk: 'Disk Repair',
|
||||||
|
attempt: 'Attempt automatic repair',
|
||||||
|
repair: 'Repair',
|
||||||
|
sync: {
|
||||||
|
title: 'Clock sync failure',
|
||||||
|
subtitle:
|
||||||
|
'This will cause connectivity issues. To resolve it, refer to the',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
44
web/projects/ui/src/app/i18n/dictionaries/spanish.ts
Normal file
44
web/projects/ui/src/app/i18n/dictionaries/spanish.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { i18n } from '../i18n.providers'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
ui: {
|
||||||
|
back: 'Atrás',
|
||||||
|
change: 'Cambiar',
|
||||||
|
update: 'Actualizar',
|
||||||
|
reset: 'Reiniciar',
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
outlet: {
|
||||||
|
system: 'Sistema',
|
||||||
|
general: 'General',
|
||||||
|
email: 'Correo Electrónico',
|
||||||
|
backup: 'Crear Copia de Seguridad',
|
||||||
|
restore: 'Restaurar Copia de Seguridad',
|
||||||
|
interfaces: 'Direcciones de Interfaz de Usuario',
|
||||||
|
acme: 'ACME',
|
||||||
|
wifi: 'WiFi',
|
||||||
|
sessions: 'Sesiones Activas',
|
||||||
|
ssh: 'SSH',
|
||||||
|
password: 'Cambiar Contraseña',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: 'Configuración General',
|
||||||
|
subtitle: 'Gestiona tu configuración general y preferencias',
|
||||||
|
update: 'Actualización de Software',
|
||||||
|
restart: 'Reiniciar para aplicar',
|
||||||
|
check: 'Buscar actualizaciones',
|
||||||
|
tab: 'Título de la Pestaña del Navegador',
|
||||||
|
language: 'Idioma',
|
||||||
|
tor: 'Reiniciar Tor',
|
||||||
|
daemon: 'Reiniciar el daemon de Tor en tu servidor',
|
||||||
|
disk: 'Reparación de Disco',
|
||||||
|
attempt: 'Intentar reparación automática',
|
||||||
|
repair: 'Reparar',
|
||||||
|
sync: {
|
||||||
|
title: 'Fallo en la sincronización del reloj',
|
||||||
|
subtitle:
|
||||||
|
'Esto causará problemas de conectividad. Para resolverlo, consulta la',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies i18n
|
||||||
23
web/projects/ui/src/app/i18n/i18n.pipe.ts
Normal file
23
web/projects/ui/src/app/i18n/i18n.pipe.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { inject, Pipe, PipeTransform } from '@angular/core'
|
||||||
|
import { I18N, i18n } from './i18n.providers'
|
||||||
|
|
||||||
|
type DeepKeyOf<T> = {
|
||||||
|
[K in keyof T & string]: T[K] extends {}
|
||||||
|
? T[K] extends string
|
||||||
|
? K
|
||||||
|
: `${K}.${DeepKeyOf<T[K]>}`
|
||||||
|
: never
|
||||||
|
}[keyof T & string]
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
standalone: true,
|
||||||
|
name: 'i18n',
|
||||||
|
pure: false,
|
||||||
|
})
|
||||||
|
export class i18nPipe implements PipeTransform {
|
||||||
|
private readonly i18n = inject(I18N)
|
||||||
|
|
||||||
|
transform(path: DeepKeyOf<i18n>): string {
|
||||||
|
return path.split('.').reduce((acc, part) => acc[part], this.i18n() as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
web/projects/ui/src/app/i18n/i18n.providers.ts
Normal file
38
web/projects/ui/src/app/i18n/i18n.providers.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { signal } from '@angular/core'
|
||||||
|
import { tuiCreateToken, tuiProvide } from '@taiga-ui/cdk'
|
||||||
|
import {
|
||||||
|
TuiLanguageName,
|
||||||
|
tuiLanguageSwitcher,
|
||||||
|
TuiLanguageSwitcherService,
|
||||||
|
} from '@taiga-ui/i18n'
|
||||||
|
import ENGLISH from './dictionaries/english'
|
||||||
|
import { i18nService } from './i18n.service'
|
||||||
|
|
||||||
|
export type i18n = typeof ENGLISH
|
||||||
|
|
||||||
|
export const I18N = tuiCreateToken(signal(ENGLISH))
|
||||||
|
export const I18N_LOADER =
|
||||||
|
tuiCreateToken<(lang: TuiLanguageName) => Promise<i18n>>()
|
||||||
|
|
||||||
|
export const I18N_PROVIDERS = [
|
||||||
|
tuiLanguageSwitcher(async (language: TuiLanguageName): Promise<unknown> => {
|
||||||
|
switch (language) {
|
||||||
|
case 'spanish':
|
||||||
|
return import('@taiga-ui/i18n/languages/spanish')
|
||||||
|
default:
|
||||||
|
return import('@taiga-ui/i18n/languages/english')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
provide: I18N_LOADER,
|
||||||
|
useValue: async (language: TuiLanguageName): Promise<unknown> => {
|
||||||
|
switch (language) {
|
||||||
|
case 'spanish':
|
||||||
|
return import('./dictionaries/spanish').then(v => v.default)
|
||||||
|
default:
|
||||||
|
return import('./dictionaries/english').then(v => v.default)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tuiProvide(TuiLanguageSwitcherService, i18nService),
|
||||||
|
]
|
||||||
30
web/projects/ui/src/app/i18n/i18n.service.ts
Normal file
30
web/projects/ui/src/app/i18n/i18n.service.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { inject, Injectable, signal } from '@angular/core'
|
||||||
|
import { TuiLanguageName, TuiLanguageSwitcherService } from '@taiga-ui/i18n'
|
||||||
|
import { I18N, I18N_LOADER } from './i18n.providers'
|
||||||
|
import { ApiService } from '../services/api/embassy-api.service'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class i18nService extends TuiLanguageSwitcherService {
|
||||||
|
private readonly i18n = inject(I18N)
|
||||||
|
private readonly i18nLoader = inject(I18N_LOADER)
|
||||||
|
private readonly api = inject(ApiService)
|
||||||
|
|
||||||
|
readonly loading = signal(false)
|
||||||
|
|
||||||
|
override setLanguage(language: TuiLanguageName): void {
|
||||||
|
if (this.language === language) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setLanguage(language)
|
||||||
|
this.loading.set(true)
|
||||||
|
this.api.setDbValue(['language'], language).then(() =>
|
||||||
|
this.i18nLoader(language).then(value => {
|
||||||
|
this.i18n.set(value)
|
||||||
|
this.loading.set(false)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
INJECTOR,
|
INJECTOR,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { toSignal } from '@angular/core/rxjs-interop'
|
import { toSignal } from '@angular/core/rxjs-interop'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
import { RouterLink } from '@angular/router'
|
import { RouterLink } from '@angular/router'
|
||||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
|
||||||
@@ -16,13 +17,21 @@ import {
|
|||||||
tuiFadeIn,
|
tuiFadeIn,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
tuiScaleIn,
|
tuiScaleIn,
|
||||||
|
TuiTextfield,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
} from '@taiga-ui/core'
|
} from '@taiga-ui/core'
|
||||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
import {
|
||||||
|
TUI_CONFIRM,
|
||||||
|
TuiButtonLoading,
|
||||||
|
TuiButtonSelect,
|
||||||
|
TuiDataListWrapper,
|
||||||
|
} from '@taiga-ui/kit'
|
||||||
import { TuiCell, tuiCellOptionsProvider, TuiHeader } from '@taiga-ui/layout'
|
import { TuiCell, tuiCellOptionsProvider, TuiHeader } from '@taiga-ui/layout'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { filter } from 'rxjs'
|
import { filter } from 'rxjs'
|
||||||
|
import { i18nPipe } from 'src/app/i18n/i18n.pipe'
|
||||||
|
import { i18nService } from 'src/app/i18n/i18n.service'
|
||||||
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
@@ -36,13 +45,19 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<ng-container *title>
|
<ng-container *title>
|
||||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">
|
||||||
General Settings
|
{{ 'ui.back' | i18n }}
|
||||||
|
</a>
|
||||||
|
{{ 'system.general.title' | i18n }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<header tuiHeader>
|
<header tuiHeader>
|
||||||
<hgroup tuiTitle>
|
<hgroup tuiTitle>
|
||||||
<h3>General</h3>
|
<h3>
|
||||||
<p tuiSubtitle>Manage your overall setup and preferences</p>
|
{{ 'system.general.title' | i18n }}
|
||||||
|
</h3>
|
||||||
|
<p tuiSubtitle>
|
||||||
|
{{ 'system.general.subtitle' | i18n }}
|
||||||
|
</p>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
</header>
|
</header>
|
||||||
@if (server(); as server) {
|
@if (server(); as server) {
|
||||||
@@ -52,7 +67,9 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.zap" />
|
<tui-icon icon="@tui.zap" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>Software Update</strong>
|
<strong>
|
||||||
|
{{ 'system.general.update' | i18n }}
|
||||||
|
</strong>
|
||||||
<span tuiSubtitle>{{ server.version }}</span>
|
<span tuiSubtitle>{{ server.version }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -62,12 +79,12 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
(click)="onUpdate()"
|
(click)="onUpdate()"
|
||||||
>
|
>
|
||||||
@if (server.statusInfo.updated) {
|
@if (server.statusInfo.updated) {
|
||||||
Restart to apply
|
{{ 'system.general.restart' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@if (eos.showUpdate$ | async) {
|
@if (eos.showUpdate$ | async) {
|
||||||
Update
|
{{ 'ui.update' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
Check for updates
|
{{ 'system.general.check' | i18n }}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@@ -75,36 +92,65 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.app-window" />
|
<tui-icon icon="@tui.app-window" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>Browser Tab Title</strong>
|
<strong>
|
||||||
|
{{ 'system.general.tab' | i18n }}
|
||||||
|
</strong>
|
||||||
<span tuiSubtitle>{{ name() }}</span>
|
<span tuiSubtitle>{{ name() }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button tuiButton (click)="onTitle()">Change</button>
|
<button tuiButton (click)="onTitle()">
|
||||||
|
{{ 'ui.change' | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.languages" />
|
<tui-icon icon="@tui.languages" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>Language</strong>
|
<strong>
|
||||||
<span tuiSubtitle>English</span>
|
{{ 'system.general.language' | i18n }}
|
||||||
|
</strong>
|
||||||
|
<span tuiSubtitle>{{ i18n.language }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button tuiButton>Change</button>
|
<button
|
||||||
|
tuiButtonSelect
|
||||||
|
tuiButton
|
||||||
|
[loading]="i18n.loading()"
|
||||||
|
[ngModel]="i18n.language"
|
||||||
|
(ngModelChange)="i18n.setLanguage($event)"
|
||||||
|
>
|
||||||
|
{{ 'ui.change' | i18n }}
|
||||||
|
<tui-data-list-wrapper
|
||||||
|
*tuiTextfieldDropdown
|
||||||
|
size="l"
|
||||||
|
[items]="languages"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div tuiCell tuiAppearance="outline-grayscale">
|
<div tuiCell tuiAppearance="outline-grayscale">
|
||||||
<tui-icon icon="@tui.circle-power" (click)="count = count + 1" />
|
<tui-icon icon="@tui.circle-power" (click)="count = count + 1" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>Reset Tor</strong>
|
<strong>
|
||||||
<span tuiSubtitle>Restart the Tor daemon on your server</span>
|
{{ 'system.general.tor' | i18n }}
|
||||||
|
</strong>
|
||||||
|
<span tuiSubtitle>
|
||||||
|
{{ 'system.general.daemon' | i18n }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button tuiButton appearance="glass" (click)="onReset()">Reset</button>
|
<button tuiButton appearance="glass" (click)="onReset()">
|
||||||
|
{{ 'ui.reset' | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@if (count > 4) {
|
@if (count > 4) {
|
||||||
<div tuiCell tuiAppearance="outline-grayscale" @tuiScaleIn @tuiFadeIn>
|
<div tuiCell tuiAppearance="outline-grayscale" @tuiScaleIn @tuiFadeIn>
|
||||||
<tui-icon icon="@tui.briefcase-medical" />
|
<tui-icon icon="@tui.briefcase-medical" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<strong>Disk Repair</strong>
|
<strong>
|
||||||
<span tuiSubtitle>Attempt automatic repair</span>
|
{{ 'system.general.disk' | i18n }}
|
||||||
|
</strong>
|
||||||
|
<span tuiSubtitle>
|
||||||
|
{{ 'system.general.attempt' | i18n }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button tuiButton appearance="glass" (click)="onRepair()">
|
<button tuiButton appearance="glass" (click)="onRepair()">
|
||||||
Repair
|
{{ 'system.general.repair' | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -122,6 +168,11 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
[tuiCell] {
|
[tuiCell] {
|
||||||
background: var(--tui-background-neutral-1);
|
background: var(--tui-background-neutral-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[tuiSubtitle],
|
||||||
|
tui-data-list-wrapper ::ng-deep [tuiOption] {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
providers: [tuiCellOptionsProvider({ height: 'spacious' })],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -130,14 +181,20 @@ import { SystemWipeComponent } from './wipe.component'
|
|||||||
imports: [
|
imports: [
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
|
i18nPipe,
|
||||||
TuiTitle,
|
TuiTitle,
|
||||||
TuiHeader,
|
TuiHeader,
|
||||||
TuiCell,
|
TuiCell,
|
||||||
TuiAppearance,
|
TuiAppearance,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
|
TuiIcon,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
SystemSyncComponent,
|
SystemSyncComponent,
|
||||||
TuiIcon,
|
TuiButtonLoading,
|
||||||
|
TuiButtonSelect,
|
||||||
|
TuiDataListWrapper,
|
||||||
|
TuiTextfield,
|
||||||
|
FormsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class SystemGeneralComponent {
|
export default class SystemGeneralComponent {
|
||||||
@@ -159,6 +216,8 @@ export default class SystemGeneralComponent {
|
|||||||
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
||||||
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
readonly name = toSignal(this.patch.watch$('ui', 'name'))
|
||||||
readonly eos = inject(EOSService)
|
readonly eos = inject(EOSService)
|
||||||
|
readonly i18n = inject(i18nService)
|
||||||
|
readonly languages = ['english', 'spanish']
|
||||||
|
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
if (this.server()?.statusInfo.updated) {
|
if (this.server()?.statusInfo.updated) {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||||
import { TuiLink, TuiNotification, TuiTitle } from '@taiga-ui/core'
|
import { TuiLink, TuiNotification, TuiTitle } from '@taiga-ui/core'
|
||||||
|
import { i18nPipe } from 'src/app/i18n/i18n.pipe'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'system-sync',
|
selector: 'system-sync',
|
||||||
template: `
|
template: `
|
||||||
<tui-notification appearance="warning">
|
<tui-notification appearance="warning">
|
||||||
<div tuiTitle>
|
<div tuiTitle>
|
||||||
Clock sync failure
|
{{ 'system.general.sync.title' | i18n }}
|
||||||
<div tuiSubtitle>
|
<div tuiSubtitle>
|
||||||
This will cause connectivity issues. Refer to the
|
{{ 'system.general.sync.subtitle' | i18n }}
|
||||||
<a
|
<a
|
||||||
tuiLink
|
tuiLink
|
||||||
iconEnd="@tui.external-link"
|
iconEnd="@tui.external-link"
|
||||||
@@ -17,13 +18,12 @@ import { TuiLink, TuiNotification, TuiTitle } from '@taiga-ui/core'
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
[textContent]="'StartOS docs'"
|
[textContent]="'StartOS docs'"
|
||||||
></a>
|
></a>
|
||||||
to resolve it
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</tui-notification>
|
</tui-notification>
|
||||||
`,
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TuiNotification, TuiTitle, TuiLink],
|
imports: [TuiNotification, TuiTitle, TuiLink, i18nPipe],
|
||||||
})
|
})
|
||||||
export class SystemSyncComponent {}
|
export class SystemSyncComponent {}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { RouterModule } from '@angular/router'
|
|||||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||||
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
import { TuiBadgeNotification } from '@taiga-ui/kit'
|
||||||
import { TuiCell } from '@taiga-ui/layout'
|
import { TuiCell } from '@taiga-ui/layout'
|
||||||
|
import { i18nPipe } from 'src/app/i18n/i18n.pipe'
|
||||||
import { BadgeService } from 'src/app/services/badge.service'
|
import { BadgeService } from 'src/app/services/badge.service'
|
||||||
import { TitleDirective } from 'src/app/services/title.service'
|
import { TitleDirective } from 'src/app/services/title.service'
|
||||||
import { SYSTEM_MENU } from './system.const'
|
import { SYSTEM_MENU } from './system.const'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
<span *title>System</span>
|
<span *title>{{ 'system.outlet.general' | i18n }}</span>
|
||||||
<aside class="g-aside">
|
<aside class="g-aside">
|
||||||
@for (cat of menu; track $index) {
|
@for (cat of menu; track $index) {
|
||||||
@if ($index) {
|
@if ($index) {
|
||||||
@@ -20,13 +21,13 @@ import { SYSTEM_MENU } from './system.const'
|
|||||||
<a
|
<a
|
||||||
tuiCell="s"
|
tuiCell="s"
|
||||||
routerLinkActive="active"
|
routerLinkActive="active"
|
||||||
[routerLink]="page.routerLink"
|
[routerLink]="page.item.split('.').at(-1)"
|
||||||
>
|
>
|
||||||
<tui-icon [icon]="page.icon" />
|
<tui-icon [icon]="page.icon" />
|
||||||
<span tuiTitle>
|
<span tuiTitle>
|
||||||
<span>
|
<span>
|
||||||
{{ page.title }}
|
{{ page.item | i18n }}
|
||||||
@if (page.routerLink === 'general' && badge()) {
|
@if (page.item === 'system.outlet.general' && badge()) {
|
||||||
<tui-badge-notification>{{ badge() }}</tui-badge-notification>
|
<tui-badge-notification>{{ badge() }}</tui-badge-notification>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@@ -106,6 +107,7 @@ import { SYSTEM_MENU } from './system.const'
|
|||||||
TuiTitle,
|
TuiTitle,
|
||||||
TitleDirective,
|
TitleDirective,
|
||||||
TuiBadgeNotification,
|
TuiBadgeNotification,
|
||||||
|
i18nPipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SystemComponent {
|
export class SystemComponent {
|
||||||
|
|||||||
@@ -1,60 +1,50 @@
|
|||||||
export const SYSTEM_MENU = [
|
export const SYSTEM_MENU = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
title: 'General',
|
|
||||||
icon: '@tui.settings',
|
icon: '@tui.settings',
|
||||||
routerLink: 'general',
|
item: 'system.outlet.general',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Email',
|
|
||||||
icon: '@tui.mail',
|
icon: '@tui.mail',
|
||||||
routerLink: 'email',
|
item: 'system.outlet.email',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
title: 'Create Backup',
|
|
||||||
icon: '@tui.copy-plus',
|
icon: '@tui.copy-plus',
|
||||||
routerLink: 'backup',
|
item: 'system.outlet.backup',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Restore Backup',
|
|
||||||
icon: '@tui.database-backup',
|
icon: '@tui.database-backup',
|
||||||
routerLink: 'restore',
|
item: 'system.outlet.restore',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
title: 'User Interface Addresses',
|
|
||||||
icon: '@tui.monitor',
|
icon: '@tui.monitor',
|
||||||
routerLink: 'interfaces',
|
item: 'system.outlet.interfaces',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ACME',
|
|
||||||
icon: '@tui.award',
|
icon: '@tui.award',
|
||||||
routerLink: 'acme',
|
item: 'system.outlet.acme',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'WiFi',
|
|
||||||
icon: '@tui.wifi',
|
icon: '@tui.wifi',
|
||||||
routerLink: 'wifi',
|
item: 'system.outlet.wifi',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
title: 'Active Sessions',
|
|
||||||
icon: '@tui.clock',
|
icon: '@tui.clock',
|
||||||
routerLink: 'sessions',
|
item: 'system.outlet.sessions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'SSH',
|
|
||||||
icon: '@tui.terminal',
|
icon: '@tui.terminal',
|
||||||
routerLink: 'ssh',
|
item: 'system.outlet.ssh',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Change Password',
|
|
||||||
icon: '@tui.key',
|
icon: '@tui.key',
|
||||||
routerLink: 'password',
|
item: 'system.outlet.password',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]
|
] as const
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const mockPatchData: DataModel = {
|
|||||||
highScore: 0,
|
highScore: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
language: 'english',
|
||||||
ackInstructions: {},
|
ackInstructions: {},
|
||||||
},
|
},
|
||||||
serverInfo: {
|
serverInfo: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type UIData = {
|
|||||||
}
|
}
|
||||||
ackInstructions: Record<string, boolean>
|
ackInstructions: Record<string, boolean>
|
||||||
theme: string
|
theme: string
|
||||||
|
language: 'english' | 'spanish'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UIMarketplaceData = {
|
export type UIMarketplaceData = {
|
||||||
|
|||||||
Reference in New Issue
Block a user