diff --git a/web/README.md b/web/README.md index f6e43db19..affc8510c 100644 --- a/web/README.md +++ b/web/README.md @@ -91,3 +91,51 @@ cp proxy.conf-sample.json proxy.conf.json ```sh npm run start:ui:proxy ``` + +## Updating translations + +### Adding a new translation + +When prompting AI to translate the English dictionary, it is recommended to only give it 50-100 entries at a time. Beyond that it struggles. Remember to sanity check the results and ensure keys/values align in the resulting dictionary. + +#### Sample AI prompt + +Translate the English dictionary below into ``. Format the result as a javascript object with the numeric values of the English dictionary as keys in the translated dictionary. These translations are for the web UI of StartOS, a graphical server operating system optimized for self-hosting. Comments may be included in the English dictionary to provide additional context. + +#### Adding to StartOS + +- In the `shared` project: + + 1. Create a new file (`language.ts`) in `src/i18n/dictionaries` + 1. Export the dictionary in `src/public-api.ts` + 1. Update the `I18N_PROVIDERS` array in `src/i18n/i18n.providers.ts` (2 places) + 1. Update the `languages` array in `/src/i18n/i18n.service.ts` + +- Here in this README: + + 1. Add the language to the list of supported languages below + +### Updating the English dictionary + +#### Sample AI prompt + +Translate `` into the languages below. Return the translations as a JSON object with the languages as keys. + +- Spanish +- Polish +- German + + +#### Adding to StartOS + +In the `shared` project, copy/past the translations into their corresponding dictionaries in `/src/i18n/dictionaries`. diff --git a/web/projects/install-wizard/src/app/app.component.ts b/web/projects/install-wizard/src/app/app.component.ts index 6a4a61f2c..b24dd1ff0 100644 --- a/web/projects/install-wizard/src/app/app.component.ts +++ b/web/projects/install-wizard/src/app/app.component.ts @@ -1,6 +1,6 @@ import { TUI_CONFIRM } from '@taiga-ui/kit' import { Component, inject } from '@angular/core' -import { DiskInfo, LoadingService, toGuid } from '@start9labs/shared' +import { DiskInfo, i18nKey, LoadingService, toGuid } from '@start9labs/shared' import { TuiDialogService } from '@taiga-ui/core' import { filter, from } from 'rxjs' import { SUCCESS, toWarning } from 'src/app/app.utils' @@ -25,7 +25,7 @@ export class AppComponent { } async install(overwrite = false) { - const loader = this.loader.open('Installing StartOS...').subscribe() + const loader = this.loader.open('Installing StartOS' as i18nKey).subscribe() const logicalname = this.selected?.logicalname || '' try { @@ -55,7 +55,7 @@ export class AppComponent { ) .subscribe({ complete: async () => { - const loader = this.loader.open('').subscribe() + const loader = this.loader.open('' as i18nKey).subscribe() try { await this.api.reboot() diff --git a/web/projects/install-wizard/src/app/app.module.ts b/web/projects/install-wizard/src/app/app.module.ts index 14aabf21d..02b529d3a 100644 --- a/web/projects/install-wizard/src/app/app.module.ts +++ b/web/projects/install-wizard/src/app/app.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { DriveComponent, + i18nPipe, RELATIVE_URL, WorkspaceConfig, } from '@start9labs/shared' @@ -38,6 +39,7 @@ const { TuiIcon, TuiSurface, TuiTitle, + i18nPipe, ], providers: [ NG_EVENT_PLUGINS, diff --git a/web/projects/marketplace/src/pages/list/item/item.module.ts b/web/projects/marketplace/src/pages/list/item/item.module.ts index 9822ab966..8a3888783 100644 --- a/web/projects/marketplace/src/pages/list/item/item.module.ts +++ b/web/projects/marketplace/src/pages/list/item/item.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' -import { SharedPipesModule, TickerModule } from '@start9labs/shared' +import { SharedPipesModule, TickerComponent } from '@start9labs/shared' import { TuiLet } from '@taiga-ui/cdk' import { ItemComponent } from './item.component' @@ -12,7 +12,7 @@ import { ItemComponent } from './item.component' CommonModule, RouterModule, SharedPipesModule, - TickerModule, + TickerComponent, TuiLet, ], }) diff --git a/web/projects/marketplace/src/pages/show/hero/hero.component.ts b/web/projects/marketplace/src/pages/show/hero/hero.component.ts index 915d5aaad..0cc9614c2 100644 --- a/web/projects/marketplace/src/pages/show/hero/hero.component.ts +++ b/web/projects/marketplace/src/pages/show/hero/hero.component.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { SharedPipesModule, TickerModule } from '@start9labs/shared' -import { TuiLet } from '@taiga-ui/cdk' +import { SharedPipesModule, TickerComponent } from '@start9labs/shared' @Component({ selector: 'marketplace-package-hero', @@ -142,7 +141,7 @@ import { TuiLet } from '@taiga-ui/cdk' ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, SharedPipesModule, TickerModule, TuiLet], + imports: [CommonModule, SharedPipesModule, TickerComponent], }) export class MarketplacePackageHeroComponent { @Input({ required: true }) diff --git a/web/projects/setup-wizard/src/app/components/cifs.component.ts b/web/projects/setup-wizard/src/app/components/cifs.component.ts index 07ef633fb..7aba29c63 100644 --- a/web/projects/setup-wizard/src/app/components/cifs.component.ts +++ b/web/projects/setup-wizard/src/app/components/cifs.component.ts @@ -7,7 +7,7 @@ import { ReactiveFormsModule, Validators, } from '@angular/forms' -import { LoadingService, StartOSDiskInfo } from '@start9labs/shared' +import { i18nKey, LoadingService, StartOSDiskInfo } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiButton, @@ -132,7 +132,7 @@ export class CifsComponent { async submit(): Promise { const loader = this.loader - .open('Connecting to shared folder...') + .open('Connecting to shared folder' as i18nKey) .subscribe() try { diff --git a/web/projects/setup-wizard/src/app/pages/attach.page.ts b/web/projects/setup-wizard/src/app/pages/attach.page.ts index 15e0f5e6a..47cdd3769 100644 --- a/web/projects/setup-wizard/src/app/pages/attach.page.ts +++ b/web/projects/setup-wizard/src/app/pages/attach.page.ts @@ -4,6 +4,7 @@ import { DiskInfo, DriveComponent, ErrorService, + i18nKey, LoadingService, toGuid, } from '@start9labs/shared' @@ -84,7 +85,9 @@ export default class AttachPage { } private async attachDrive(guid: string, password: string) { - const loader = this.loader.open('Connecting to drive...').subscribe() + const loader = this.loader + .open('Connecting to drive' as i18nKey) + .subscribe() try { await this.stateService.importDrive(guid, password) diff --git a/web/projects/setup-wizard/src/app/pages/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover.page.ts index 95d0878a4..198ad9f70 100644 --- a/web/projects/setup-wizard/src/app/pages/recover.page.ts +++ b/web/projects/setup-wizard/src/app/pages/recover.page.ts @@ -1,4 +1,3 @@ -import { DatePipe } from '@angular/common' import { Component, inject } from '@angular/core' import { Router } from '@angular/router' import { ErrorService, ServerComponent } from '@start9labs/shared' @@ -64,7 +63,6 @@ import { StateService } from 'src/app/services/state.service' TuiCell, TuiIcon, TuiTitle, - DatePipe, ServerComponent, PasswordDirective, ], diff --git a/web/projects/setup-wizard/src/app/pages/storage.page.ts b/web/projects/setup-wizard/src/app/pages/storage.page.ts index e79123ce4..2e45aea9a 100644 --- a/web/projects/setup-wizard/src/app/pages/storage.page.ts +++ b/web/projects/setup-wizard/src/app/pages/storage.page.ts @@ -4,6 +4,7 @@ import { DiskInfo, DriveComponent, ErrorService, + i18nKey, LoadingService, toGuid, } from '@start9labs/shared' @@ -156,7 +157,9 @@ export default class StoragePage { logicalname: string, password: string, ): Promise { - const loader = this.loader.open('Connecting to drive...').subscribe() + const loader = this.loader + .open('Connecting to drive' as i18nKey) + .subscribe() try { await this.stateService.setupEmbassy(logicalname, password) diff --git a/web/projects/shared/assets/img/background_dark.jpeg b/web/projects/shared/assets/img/background_dark.jpeg index c59176dfe..74679772a 100644 Binary files a/web/projects/shared/assets/img/background_dark.jpeg and b/web/projects/shared/assets/img/background_dark.jpeg differ diff --git a/web/projects/shared/src/components/initializing/initializing.component.ts b/web/projects/shared/src/components/initializing.component.ts similarity index 86% rename from web/projects/shared/src/components/initializing/initializing.component.ts rename to web/projects/shared/src/components/initializing.component.ts index 470bc6759..1930a8301 100644 --- a/web/projects/shared/src/components/initializing/initializing.component.ts +++ b/web/projects/shared/src/components/initializing.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { TuiProgress } from '@taiga-ui/kit' import { LogsWindowComponent } from './logs-window.component' +import { i18nPipe } from '../i18n/i18n.pipe' @Component({ standalone: true, @@ -9,10 +10,10 @@ import { LogsWindowComponent } from './logs-window.component' template: `

- Setting up your server + {{ 'Setting up your server' | i18n }}

- Progress: {{ (progress.total * 100).toFixed(0) }}% + {{ 'Progress' | i18n }}: {{ (progress.total * 100).toFixed(0) }}%
', - styleUrls: ['./loading.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiLoader], -}) -export class LoadingComponent { - readonly content = injectContext<{ content: PolymorpheusContent }>().content -} diff --git a/web/projects/shared/src/components/loading/loading.service.ts b/web/projects/shared/src/components/loading/loading.service.ts deleted file mode 100644 index 46cbfbb65..000000000 --- a/web/projects/shared/src/components/loading/loading.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TuiPopoverService } from '@taiga-ui/cdk' -import { Injectable } from '@angular/core' -import { TUI_DIALOGS } from '@taiga-ui/core' - -import { LoadingComponent } from './loading.component' - -@Injectable({ - providedIn: `root`, - useFactory: () => new LoadingService(TUI_DIALOGS, LoadingComponent), -}) -export class LoadingService extends TuiPopoverService {} diff --git a/web/projects/shared/src/components/initializing/logs-window.component.ts b/web/projects/shared/src/components/logs-window.component.ts similarity index 95% rename from web/projects/shared/src/components/initializing/logs-window.component.ts rename to web/projects/shared/src/components/logs-window.component.ts index 04f793409..1af00c2a9 100644 --- a/web/projects/shared/src/components/initializing/logs-window.component.ts +++ b/web/projects/shared/src/components/logs-window.component.ts @@ -7,7 +7,7 @@ import { } from '@ng-web-apis/intersection-observer' import { WaMutationObserver } from '@ng-web-apis/mutation-observer' import { NgDompurifyModule } from '@tinkoff/ng-dompurify' -import { SetupLogsService } from '../../services/setup-logs.service' +import { SetupLogsService } from '../services/setup-logs.service' @Component({ standalone: true, diff --git a/web/projects/ui/src/app/routes/portal/modals/prompt.component.ts b/web/projects/shared/src/components/prompt.component.ts similarity index 86% rename from web/projects/ui/src/app/routes/portal/modals/prompt.component.ts rename to web/projects/shared/src/components/prompt.component.ts index f3fc85acf..559c5d619 100644 --- a/web/projects/ui/src/app/routes/portal/modals/prompt.component.ts +++ b/web/projects/shared/src/components/prompt.component.ts @@ -1,13 +1,15 @@ -import { TuiTextfieldControllerModule, TuiInputModule } from '@taiga-ui/legacy' -import { TuiAutoFocus } from '@taiga-ui/cdk' import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { FormsModule } from '@angular/forms' -import { TuiDialogContext, TuiButton } from '@taiga-ui/core' +import { TuiAutoFocus } from '@taiga-ui/cdk' +import { TuiButton, TuiDialogContext } from '@taiga-ui/core' +import { TuiInputModule, TuiTextfieldControllerModule } from '@taiga-ui/legacy' import { POLYMORPHEUS_CONTEXT, PolymorpheusComponent, } from '@taiga-ui/polymorpheus' +import { i18nPipe } from '../i18n/i18n.pipe' +import { i18nKey } from '../i18n/i18n.providers' @Component({ standalone: true, @@ -37,10 +39,10 @@ import { appearance="secondary" (click)="cancel()" > - Cancel + {{ 'Cancel' | i18n }} @@ -81,6 +83,7 @@ import { TuiButton, TuiTextfieldControllerModule, TuiAutoFocus, + i18nPipe, ], changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -111,11 +114,11 @@ export class PromptModal { export const PROMPT = new PolymorpheusComponent(PromptModal) export interface PromptOptions { - message: string - label?: string - warning?: string - buttonText?: string - placeholder?: string + message: i18nKey + label?: i18nKey + warning?: i18nKey + buttonText?: i18nKey + placeholder?: i18nKey required?: boolean useMask?: boolean initialValue?: string | null diff --git a/web/projects/shared/src/components/server.component.ts b/web/projects/shared/src/components/server.component.ts index 5ef396dd0..c1ffac257 100644 --- a/web/projects/shared/src/components/server.component.ts +++ b/web/projects/shared/src/components/server.component.ts @@ -1,6 +1,6 @@ import { DatePipe } from '@angular/common' -import { Component, inject, input } from '@angular/core' -import { TuiDialogService, TuiIcon, TuiTitle } from '@taiga-ui/core' +import { Component, input } from '@angular/core' +import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiCell } from '@taiga-ui/layout' import { StartOSDiskInfo } from '../types/api' diff --git a/web/projects/shared/src/components/ticker/ticker.component.ts b/web/projects/shared/src/components/ticker.component.ts similarity index 64% rename from web/projects/shared/src/components/ticker/ticker.component.ts rename to web/projects/shared/src/components/ticker.component.ts index f8de4c93a..0f1652a90 100644 --- a/web/projects/shared/src/components/ticker/ticker.component.ts +++ b/web/projects/shared/src/components/ticker.component.ts @@ -7,9 +7,24 @@ import { } from '@angular/core' @Component({ + standalone: true, selector: '[ticker]', template: '', - styleUrls: ['./ticker.component.scss'], + styles: ` + :host { + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: text-indent 1s; + + &:hover { + text-indent: var(--indent, 0); + text-overflow: clip; + cursor: default; + } + } + `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class TickerComponent { diff --git a/web/projects/shared/src/components/ticker/ticker.component.scss b/web/projects/shared/src/components/ticker/ticker.component.scss deleted file mode 100644 index 49dbd0a28..000000000 --- a/web/projects/shared/src/components/ticker/ticker.component.scss +++ /dev/null @@ -1,13 +0,0 @@ -:host { - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: text-indent 1s; - - &:hover { - text-indent: var(--indent, 0); - text-overflow: clip; - cursor: default; - } -} diff --git a/web/projects/shared/src/components/ticker/ticker.module.ts b/web/projects/shared/src/components/ticker/ticker.module.ts deleted file mode 100644 index c2bd46f15..000000000 --- a/web/projects/shared/src/components/ticker/ticker.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NgModule } from '@angular/core' - -import { TickerComponent } from './ticker.component' - -@NgModule({ - declarations: [TickerComponent], - exports: [TickerComponent], -}) -export class TickerModule {} diff --git a/web/projects/shared/src/i18n/dictionaries/english.ts b/web/projects/shared/src/i18n/dictionaries/english.ts new file mode 100644 index 000000000..413af7be7 --- /dev/null +++ b/web/projects/shared/src/i18n/dictionaries/english.ts @@ -0,0 +1,489 @@ +// prettier-ignore +export const ENGLISH = { + 'Change': 1, // verb + 'Update': 2, // verb + 'Reset': 3, // verb + 'System': 4, // as in, system preferences + 'General': 5, // as in, general settings + 'Email': 6, + 'Create Backup': 7, // create a backup + 'Restore Backup': 8, // restore from backup + 'Go to login': 9, + 'Test': 10, // verb + 'Skip': 11, // as in, skip this step + 'Active Sessions': 12, + 'Change Password': 13, + 'General Settings': 14, + 'Manage your overall setup and preferences': 15, + 'Browser Tab Title': 16, + 'Language': 17, + 'Disk Repair': 18, + 'Attempt automatic repair': 19, + 'Repair': 20, + 'Root Certificate Authority': 21, + 'Download your Root CA': 22, + 'Download': 23, + 'Reset Tor': 24, + 'Restart the Tor daemon on your server': 25, + 'Software Update': 26, + 'Restart to apply': 27, + 'Check for updates': 28, + 'This value will be displayed as the title of your browser tab.': 29, + 'Device Name': 30, + 'StartOS': 31, + 'Save': 32, + 'Saving': 33, + 'Warning': 34, + 'Confirm': 35, + 'Cancel': 36, + 'This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.': 37, + 'Delete': 38, + 'Tor reset in progress': 39, + 'Resetting Tor': 40, + 'Checking for updates': 41, + 'Beginning restart': 42, + 'You are on the latest version of StartOS.': 43, + 'Up to date!': 44, + 'Release Notes': 45, + 'Begin Update': 46, + 'Beginning update': 47, + 'You are currently connected over Tor. If you reset the Tor daemon, you will lose connectivity until it comes back online.': 48, + 'Reset Tor?': 49, + 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.': 50, + 'Wipe state': 51, + 'Saving high score': 52, + 'Score': 53, + 'High score': 54, + 'Save and quit': 55, + 'Bookmark this page': 56, + 'Beginning shutdown': 57, + 'Add': 58, + 'Ok': 59, + 'Are you sure you want to delete this entry?': 60, + 'This value cannot be changed once set': 61, + 'Continue': 62, + 'Click or drop file here': 63, + 'Drop file here': 64, + 'Disabled': 65, + 'Version': 66, + 'Copy': 67, // as in, copy to clipboard + 'About this server': 68, + 'System Settings': 69, + 'Restart': 70, + 'Shutdown': 71, + 'Logout': 72, + 'User manual': 73, + 'Contact support': 74, + 'Donate to Start9': 75, + 'Are you sure you want to restart your server? It can take several minutes to come back online.': 76, + 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in.': 77, + 'Services': 78, // as in, server-side software applications + 'Marketplace': 79, + 'Sideload': 80, // installing a service manually + 'Updates': 81, + 'Metrics': 82, // system info such as CPU, RAM, and storage usage + 'Logs': 83, // as in, application logs + 'Notifications': 84, + 'Launch UI': 85, + 'Show QR': 86, + 'Copy URL': 87, + 'Actions': 88, // as in, actions available to the user + 'not recommended': 89, + 'Root CA Trusted!': 90, + 'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.': 91, + 'Learn more': 92, + 'Make public': 93, + 'Make private': 94, + 'No public addresses': 95, + 'Add domain': 96, + 'Removing': 97, + 'Making public': 98, + 'Making private': 99, + 'Unsaved changes': 100, + 'You have unsaved changes. Are you sure you want to leave?': 101, + 'Leave': 102, + 'Are you sure?': 103, + 'Select Domain': 104, + 'Local': 105, + 'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.': 106, + 'Learn More': 107, + 'Public': 108, + 'Private': 109, + 'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.': 110, + 'No onion addresses': 111, + 'New Onion Address': 112, + 'Private Key (optional)': 113, + 'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.': 114, + 'Processing 10,000 logs': 115, + 'Loading older logs': 116, + 'Waiting for network connectivity': 117, + 'Reconnecting': 118, + 'Loading logs': 119, + 'Scroll to bottom': 120, + 'Reconnected': 121, + 'Disconnected': 122, + 'More': 123, + 'The following modifications were made': 124, + 'added': 125, + 'removed': 126, + 'changed from': 127, + 'to': 128, // as in, from [blank] to [blank] + 'entry': 129, // as in, "a list entry" + 'list': 130, + 'new entry': 131, + 'new list': 132, + 'Submit': 133, + 'Close': 134, + 'OS Logs': 135, + 'Kernel Logs': 136, + 'Tor Logs': 137, + 'Raw, unfiltered operating system logs': 138, + 'Diagnostics for drivers and other kernel processes': 139, + 'Diagnostic logs for the Tor daemon on StartOS': 140, + 'Downgrade': 141, + 'Reinstall': 142, + 'View Installed': 143, + 'Switch': 144, + 'Install': 145, + 'Beginning install': 146, + 'Change Registry': 147, + 'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.': 148, + 'Services from this registry are packaged and maintained by members of the Start9 community. Install at your own risk. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.': 149, + 'Services from this registry are undergoing beta testing and may contain bugs. Install at your own risk.': 150, + 'Services from this registry are undergoing alpha testing. They are expected to contain bugs and could damage your system. Install at your own risk.': 151, + 'This is a Custom Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they could damage your system. Install at your own risk.': 152, + 'Default Registries': 153, + 'Custom Registries': 154, + 'Add custom registry': 155, + 'Save for later': 156, + 'Save and connect': 157, + 'Deleting': 158, + 'Changing registry': 159, + 'Loading': 160, + 'Registry already added': 161, + 'Validating registry': 162, + 'Are you sure you want to delete this registry?': 163, + 'Add Custom Registry': 164, + 'A fully-qualified URL of the custom registry': 165, + 'Must be a valid URL': 166, + 'installed from': 167, + 'sideloaded': 168, // as in, the application was installed by sideloading + 'This service was originally': 169, + 'but you are currently connected to': 170, + 'To install from': 171, + 'anyway, click "Continue".': 172, + 'As a result of this update, the following services will no longer work properly and may crash': 173, + 'Alert': 174, + 'Percentage used': 175, + 'User space': 176, + 'Kernel space': 177, + 'Idle': 178, // a CPU metric + 'I/O wait': 179, + 'ACME': 180, + 'Total': 181, + 'Used': 182, + 'Available': 183, + 'zram used': 184, + 'zram total': 185, + 'zram available': 186, + 'System Time': 187, + 'Uptime': 188, + 'Temperature': 189, + 'Memory': 190, // as in, computer memory + 'Storage': 191, + 'Capacity': 192, // as in, disk capacity + 'Clock sync failure': 193, + 'the docs': 194, // as in, the documentation + 'Days': 195, + 'Hours': 196, + 'Minutes': 197, + 'Seconds': 198, + 'View full': 199, + 'View report': 200, + 'Batch action': 201, + 'Mark seen': 202, + 'Mark unseen': 203, + 'Date': 204, + 'Title': 205, // as in, the title of a book + 'Service': 206, // as in, server-side software application + 'Message': 207, + 'No notifications': 208, + 'Required': 209, + 'Optional': 210, + 'No reason provided': 211, + 'Tasks': 212, + 'Type': 213, + 'Description': 214, + 'All tasks complete': 215, + 'Start': 216, + 'Stop': 217, + 'Dependencies': 218, + 'Satisfied': 219, + 'No dependencies': 220, + 'Not installed': 221, + 'Incorrect version': 222, + 'Not running': 223, + 'Action required': 224, + 'Required health check not passing': 225, + 'Dependency has a dependency issue': 226, + 'Unknown error': 227, + 'Error': 228, + '"Rebuild container" is a harmless action that and only takes a few seconds to complete. It will likely resolve this issue.': 229, + '"Uninstall service" is a dangerous action that will remove the service from StartOS and wipe all its data.': 230, + 'Rebuild container': 231, + 'Uninstall service': 232, + 'View full message': 233, + 'Service error': 234, + 'Awaiting result': 235, + 'Starting': 236, + 'Success': 237, + 'Health Checks': 238, + 'No health checks': 239, + 'Name': 240, + 'Status': 241, + 'Open': 242, // verb + 'Interfaces': 243, // as in user interface or application program interface + 'Hosting': 244, + 'Installing': 245, + 'See below': 246, + 'Controls': 247, + 'No services installed': 248, + 'Running': 249, + 'Stopped': 250, + 'Task Required': 251, + 'Updating': 252, + 'Stopping': 253, + 'Trust your Root CA': 254, + 'Backing Up': 255, + 'Restarting': 256, + 'Back': 257, + 'Restoring': 258, + 'Unknown': 259, + 'Reveal/Hide': 260, + 'Reveal': 261, + 'Scan this QR': 262, + 'Reset defaults': 263, + 'As a result of this change, the following services will no longer work properly and may crash': 264, + 'Service Launch Error': 265, + 'Issue': 266, + 'Failure': 267, + 'Healthy': 268, + 'finalizing': 269, + 'unknown %': 270, + 'Not provided': 271, + 'Links': 272, + 'Git Hash': 273, + 'License': 274, + 'Installed From': 275, + 'Service Repository': 276, + 'Package Repository': 277, + 'Marketing Site': 278, + 'Support Site': 279, + 'Donation Link': 280, + 'Standard Actions': 281, + 'Rebuild Service': 282, // as in, rebuild a software container + 'Rebuilds the service container. Only necessary in there is a bug in StartOS': 283, + 'Uninstall': 284, + 'Uninstalls this service from StartOS and delete all data permanently.': 285, + 'Dashboard': 286, + 'dashboard': 287, + 'actions': 288, + 'instructions': 289, + 'logs': 290, // as in, "application logs" + 'about': 291, // as in, "about this server" + 'Starting upload': 292, + 'Try again': 293, + 'Upload .s9pk package file': 294, + 'Warning: package upload will be slow over Tor. Switch to local for a better experience.': 295, + 'Upload': 296, + 'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.': 297, + 'Invalid package file': 298, + 'Add ACME providers in order to generate SSL (https) certificates for clearnet access.': 299, + 'View instructions': 300, + 'Saved Providers': 301, // as in, ACME service provider, such as Let's Encrypt + 'Add Provider': 302, + 'Contact': 303, // as in, "contact us" + 'Edit': 304, + 'Add ACME Provider': 305, + 'Edit ACME Provider': 306, + 'Contact Emails': 307, + 'Needed to obtain a certificate from a Certificate Authority': 308, + 'Toggle all': 309, + 'Done': 310, + 'Master Password Needed': 311, + 'Enter your master password to encrypt this backup.': 312, + 'Master Password': 313, + 'Enter master password': 314, + 'Original Password Needed': 315, + 'This backup was created with a different password. Enter the original password that was used to encrypt this backup.': 316, + 'Original Password': 317, + 'Enter original password': 318, + 'Beginning backup': 319, + 'Back up StartOS and service data by connecting to a device on your local network or a physical drive connected to your server.': 320, + 'Restore StartOS and service data from a device on your local network or a physical drive connected to your server that contains an existing backup.': 321, + 'Last Backup': 322, // as in, the last time the server was backed up + 'A folder on another computer that is connected to the same network as your Start9 server.': 323, + 'A physical drive that is plugged directly into your Start9 Server.': 324, + 'Select Services to Back Up': 325, + 'Select server backup': 326, + 'Network Folders': 327, + 'Open New': 328, + 'Hostname': 329, + 'Path': 330, // as in, a URL path + 'URL': 331, + 'Network Interface': 332, + 'Protocol': 333, // as in, http protocol + 'Model': 334, // as in, a product model + 'User Agent': 335, + 'Platform': 336, // as in, OS platform, such as iOS, Android, Linux, etc + 'Last Active': 337, + 'Created At': 338, + 'Algorithm': 339, // as in, the encryption algorithm + 'Fingerprint': 340, // as in, a fingerprint hash + 'Package Hash': 341, + 'Published': 342, + 'New Network Folder': 343, + 'Update Network Folder': 344, + 'Testing connectivity to shared folder': 345, + 'Ensure (1) target computer is connected to the same LAN as your Start9 Server, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.': 346, + 'Unable to connect': 347, + 'Network Folder does not contain a valid backup': 348, + 'Connect': 349, + 'Username': 350, + 'Password': 351, + 'The hostname of your target device on the Local Area Network.': 352, + 'On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder). On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).': 353, + 'On Linux, this is the samba username you created when sharing the folder. On Mac and Windows, this is the username of the user who is sharing the folder.': 354, + 'On Linux, this is the samba password you created when sharing the folder. On Mac and Windows, this is the password of the user who is sharing the folder.': 355, + 'Physical Drives': 356, + 'No drives detected': 357, + 'Refresh': 358, + 'Drive partition does not contain a valid backup': 359, + 'Backup Progress': 360, + 'Complete': 361, + 'Backing up': 362, + 'Waiting': 363, + 'Backup made': 364, + 'Restore selected': 365, + 'Initializing': 366, + 'Unavailable. Backup was made on a newer version of StartOS.': 367, + 'Unavailable. Service is already installed.': 368, + 'Ready to restore': 369, + 'Local Hostname': 370, + 'Created': 371, + 'Password Required': 372, + 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.': 373, + 'Decrypting drive': 374, + 'Select services to restore': 375, + 'Available for backup': 376, + 'StartOS backups detected': 377, + 'No StartOS backups detected': 378, + 'StartOS Version': 379, + 'Connecting an external SMTP server allows StartOS and your installed services to send you emails.': 380, + 'SMTP Credentials': 381, + 'Send test email': 382, + 'Send': 383, + 'Sending email': 384, + 'A test email has been sent to': 385, + 'Check your spam folder and mark as not spam.': 386, + 'The web user interface for your StartOS server, accessible from any browser.': 387, + 'Change your StartOS master password.': 388, + 'You will still need your current password to decrypt existing backups!': 389, + 'New passwords do not match': 390, + 'New password must be 12 characters or greater': 391, + 'New password must be less than 65 characters': 392, + 'Current password is invalid': 393, + 'Password changed': 394, + 'Current Password': 395, + 'New Password': 396, + 'Retype New Password': 397, + 'A session is a device that is currently logged into StartOS. For best security, terminate sessions you do not recognize or no longer use.': 398, + 'Current session': 399, + 'Other sessions': 400, + 'Terminate selected': 401, + 'Terminating sessions': 402, + 'No sessions': 403, + 'Password Needed': 404, + 'Connected': 405, + 'Forget': 406, // as in, delete or remove + 'WiFi Credentials': 407, + 'Deprecated': 408, + 'WiFi support will be removed in StartOS v0.4.1. If you do not have access to Ethernet, you can use a WiFi extender to connect to the local network, then connect your server to the extender via Ethernet. Please contact Start9 support with any questions or concerns.': 409, + 'Known Networks': 410, + 'Other Networks': 411, + 'WiFi is disabled': 412, + 'No wireless interface detected': 413, + 'Enabling WiFi': 414, + 'Disabling WiFi': 415, + 'Connecting. This could take a while': 416, + 'Retry': 417, + 'Show more': 418, + 'Release notes': 419, + 'View listing': 420, + 'Services that depend on': 421, + 'will no longer work properly and may crash.': 422, + 'Request failed': 423, + 'All services are up to date!': 424, + 'Run': 425, // as in, run a piece of software + 'Action can only be executed when service is': 426, + 'Forbidden': 427, + 'may temporarily experiences issues': 428, + 'has unmet dependencies. It will not work as expected.': 429, + 'Rebuilding container': 430, + 'Beginning uninstall': 431, + 'will permanently delete its data.': 432, + 'Uninstalling': 433, + 'Trying to reach server': 434, + 'Connection restored': 435, + 'State unknown': 436, + 'Server connected': 437, + 'No Internet': 438, + 'Connecting': 439, + 'Shutting down': 440, + 'Versions': 441, + 'New notifications': 442, + 'View': 443, + 'Reloading PWA': 444, + 'Completed': 445, + 'System data': 446, + 'Not attempted': 447, + 'Failed': 448, + 'Succeeded': 449, + 'Restart your server for these updates to take effect. It can take several minutes to come back online.': 450, + 'StartOS download complete': 451, + 'Unknown storage drive detected': 452, + 'To use a different storage drive, replace the current one and click RESTART SERVER below. To use the current storage drive, click USE CURRENT DRIVE below, then follow instructions. No data will be erased during this process.': 453, + 'Storage drive not found': 454, + 'Insert your StartOS storage drive and click RESTART SERVER below.': 455, + 'Storage drive corrupted. This could be the result of data corruption or physical damage.': 456, + 'It may or may not be possible to re-use this drive by reformatting and recovering from backup. To enter recovery mode, click ENTER RECOVERY MODE below, then follow instructions. No data will be erased during this step.': 457, + 'Filesystem error': 458, + 'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.': 459, + 'Disk management error': 460, + 'Please contact support': 461, + 'Diagnostic Mode': 462, + 'launch error': 463, + 'View logs': 464, + 'Possible solutions': 465, + 'Setup current drive': 466, + 'Enter recovery mode': 467, + 'Server is restarting': 468, + 'Wait for the server to restart, then refresh this page.': 469, + 'Restart server': 470, + 'Repair drive': 471, + 'Setting up your server': 472, + 'Progress': 473, + 'Login to StartOS': 474, + 'Login': 475, + 'Logging in': 476, + 'Password must be less than 65 characters': 477, + 'Invalid password': 478, + 'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.': 479, + 'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.': 480, + 'You have successfully trusted your Root CA and may now log in securely.': 481, + 'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.': 482, + 'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.': 483, + 'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.': 484, + 'StartOS UI': 485, + 'WiFi': 486, +} as const diff --git a/web/projects/shared/src/i18n/dictionaries/german.ts b/web/projects/shared/src/i18n/dictionaries/german.ts new file mode 100644 index 000000000..b8e979529 --- /dev/null +++ b/web/projects/shared/src/i18n/dictionaries/german.ts @@ -0,0 +1,490 @@ +import type { i18n } from '../i18n.providers' + +export default { + 1: 'Ändern', + 2: 'Aktualisieren', + 3: 'Zurücksetzen', + 4: 'System', + 5: 'Allgemein', + 6: 'E-Mail', + 7: 'Sicherung erstellen', + 8: 'Sicherung wiederherstellen', + 9: 'Zum Login gehen', + 10: 'Testen', + 11: 'Überspringen', + 12: 'Aktive Sitzungen', + 13: 'Passwort ändern', + 14: 'Allgemeine Einstellungen', + 15: 'Verwalten Sie Ihre Gesamteinrichtung und Einstellungen', + 16: 'Browser-Tab Titel', + 17: 'Sprache', + 18: 'Festplattenreparatur', + 19: 'Automatische Reparatur versuchen', + 20: 'Reparieren', + 21: 'Stammzertifizierungsstelle (Root-CA)', + 22: 'Laden Sie Ihre Root-CA herunter', + 23: 'Herunterladen', + 24: 'Tor zurücksetzen', + 25: 'Tor-Daemon auf Ihrem Server neu starten', + 26: 'Software-Aktualisierung', + 27: 'Neustart erforderlich', + 28: 'Nach Updates suchen', + 29: 'Dieser Wert wird als Titel Ihres Browser-Tabs angezeigt.', + 30: 'Gerätename', + 31: 'StartOS', + 32: 'Speichern', + 33: 'Wird gespeichert', + 34: 'Warnung', + 35: 'Bestätigen', + 36: 'Abbrechen', + 37: 'Diese Aktion sollte nur auf Anweisung eines Start9-Supportmitarbeiters ausgeführt werden. Wir empfehlen, vor dem Fortfahren eine Sicherung Ihres Geräts zu erstellen. Wenn während des Neustarts etwas schiefgeht, z. B. Stromausfall oder das Trennen des Laufwerks, kann das Dateisystem irreparabel beschädigt werden. Bitte fahren Sie mit Vorsicht fort.', + 38: 'Löschen', + 39: 'Tor-Reset läuft', + 40: 'Tor wird zurückgesetzt', + 41: 'Suche nach Updates', + 42: 'Neustart wird eingeleitet', + 43: 'Sie verwenden die neueste Version von StartOS.', + 44: 'Auf dem neuesten Stand!', + 45: 'Versionshinweise', + 46: 'Update starten', + 47: 'Update wird gestartet', + 48: 'Sie sind derzeit über Tor verbunden. Wenn Sie den Tor-Daemon zurücksetzen, verlieren Sie die Verbindung, bis dieser wieder online ist.', + 49: 'Tor zurücksetzen?', + 50: 'Optional Zustand löschen, um neue Guard-Nodes zu erzwingen. Es wird empfohlen, zuerst ohne das Löschen zu versuchen.', + 51: 'Zustand löschen', + 52: 'Bestenstand wird gespeichert', + 53: 'Punktzahl', + 54: 'Bestpunktzahl', + 55: 'Speichern und beenden', + 56: 'Diese Seite als Lesezeichen speichern', + 57: 'Herunterfahren wird eingeleitet', + 58: 'Hinzufügen', + 59: 'Ok', + 60: 'Möchten Sie diesen Eintrag wirklich löschen?', + 61: 'Dieser Wert kann nach dem Festlegen nicht geändert werden', + 62: 'Fortfahren', + 63: 'Klicken oder Datei hierher ziehen', + 64: 'Datei hierher ziehen', + 65: 'Deaktiviert', + 66: 'Version', + 67: 'Kopieren', + 68: 'Über diesen Server', + 69: 'Systemeinstellungen', + 70: 'Neustarten', + 71: 'Herunterfahren', + 72: 'Abmelden', + 73: 'Benutzerhandbuch', + 74: 'Support kontaktieren', + 75: 'An Start9 spenden', + 76: 'Möchten Sie den Server wirklich neu starten? Es kann einige Minuten dauern, bis er wieder online ist.', + 77: 'Möchten Sie den Server wirklich herunterfahren? Dies kann einige Minuten dauern, und der Server wird nicht automatisch wieder eingeschaltet. Um ihn erneut einzuschalten, müssen Sie das Gerät physisch vom Stromnetz trennen und wieder anschließen.', + 78: 'Dienste', + 79: 'Marktplatz', + 80: 'Manuell installieren', + 81: 'Updates', + 82: 'Metriken', + 83: 'Protokolle', + 84: 'Benachrichtigungen', + 85: 'UI starten', + 86: 'QR-Code anzeigen', + 87: 'URL kopieren', + 88: 'Aktionen', + 89: 'nicht empfohlen', + 90: 'Root-CA ist vertrauenswürdig!', + 91: 'Fügen Sie eine Clearnet-Adresse hinzu, um diese Oberfläche im Internet verfügbar zu machen. Clearnet-Adressen sind vollständig öffentlich und nicht anonym.', + 92: 'Mehr erfahren', + 93: 'Öffentlich machen', + 94: 'Privat machen', + 95: 'Keine öffentlichen Adressen', + 96: 'Domain hinzufügen', + 97: 'Wird entfernt', + 98: 'Wird öffentlich gemacht', + 99: 'Wird privat gemacht', + 100: 'Nicht gespeicherte Änderungen', + 101: 'Sie haben nicht gespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?', + 102: 'Verlassen', + 103: 'Sind Sie sicher?', + 104: 'Domain auswählen', + 105: 'Lokal', + 106: 'Lokale Adressen sind nur von Geräten erreichbar, die direkt oder über VPN mit demselben LAN wie Ihr Server verbunden sind.', + 107: 'Mehr erfahren', + 108: 'Öffentlich', + 109: 'Privat', + 110: 'Fügen Sie eine Onion-Adresse hinzu, um dieses Interface anonym im Darknet verfügbar zu machen. Onion-Adressen sind nur über das Tor-Netzwerk erreichbar.', + 111: 'Keine Onion-Adressen', + 112: 'Neue Onion-Adresse', + 113: 'Privater Schlüssel (optional)', + 114: 'Optional können Sie einen base64-codierten ed25519-Schlüssel angeben, um die Tor V3 (.onion)-Adresse zu generieren. Wenn nicht angegeben, wird ein zufälliger Schlüssel erstellt.', + 115: 'Verarbeite 10.000 Logs', + 116: 'Ältere Logs werden geladen', + 117: 'Warten auf Netzwerkverbindung', + 118: 'Wiederverbindung läuft', + 119: 'Lade Logs', + 120: 'Zum Ende scrollen', + 121: 'Wieder verbunden', + 122: 'Verbindung getrennt', + 123: 'Mehr', + 124: 'Die folgenden Änderungen wurden vorgenommen', + 125: 'hinzugefügt', + 126: 'entfernt', + 127: 'geändert von', + 128: 'zu', + 129: 'Eintrag', + 130: 'Liste', + 131: 'neuer Eintrag', + 132: 'neue Liste', + 133: 'Absenden', + 134: 'Schließen', + 135: 'Betriebssystem-Logs', + 136: 'Kernel-Logs', + 137: 'Tor-Logs', + 138: 'Rohdatenprotokolle des Betriebssystems ohne Filter', + 139: 'Diagnose für Treiber und andere Kernel-Prozesse', + 140: 'Diagnose-Logs des Tor-Daemons unter StartOS', + 141: 'Downgrade', + 142: 'Neu installieren', + 143: 'Installierte anzeigen', + 144: 'Wechseln', + 145: 'Installieren', + 146: 'Installation wird gestartet', + 147: 'Register wechseln', + 148: 'Dienste in diesem Register werden vom Start9-Team gepackt und gepflegt. Bei Problemen oder Fragen hilft Ihnen unser Support-Team gerne weiter.', + 149: 'Dienste in diesem Register werden von der Start9-Community gepflegt. Die Installation erfolgt auf eigenes Risiko. Bei Problemen wenden Sie sich bitte an den Paketentwickler.', + 150: 'Dienste in diesem Register befinden sich in der Betaphase und können Fehler enthalten. Die Installation erfolgt auf eigenes Risiko.', + 151: 'Dienste in diesem Register befinden sich in der Alphaphase. Sie enthalten voraussichtlich Fehler und könnten Ihr System beschädigen. Installation auf eigenes Risiko.', + 152: 'Dies ist ein benutzerdefiniertes Register. Start9 kann die Integrität oder Funktionalität der darin enthaltenen Dienste nicht garantieren. Installation auf eigenes Risiko.', + 153: 'Standard-Register', + 154: 'Benutzerdefinierte Register', + 155: 'Benutzerdefiniertes Register hinzufügen', + 156: 'Für später speichern', + 157: 'Speichern und verbinden', + 158: 'Wird gelöscht', + 159: 'Register wird gewechselt', + 160: 'Lade...', + 161: 'Register bereits hinzugefügt', + 162: 'Register wird überprüft', + 163: 'Möchten Sie dieses Register wirklich löschen?', + 164: 'Benutzerdefiniertes Register hinzufügen', + 165: 'Vollständige URL des benutzerdefinierten Registers', + 166: 'Muss eine gültige URL sein', + 167: 'installiert von', + 168: 'manuell installiert', + 169: 'Dieser Dienst wurde ursprünglich', + 170: 'aber derzeit sind Sie verbunden mit', + 171: 'Um von', + 172: 'zu installieren, klicken Sie trotzdem auf „Weiter“.', + 173: 'Durch dieses Update funktionieren die folgenden Dienste möglicherweise nicht mehr richtig und könnten abstürzen:', + 174: 'Warnung', + 175: 'Prozent verwendet', + 176: 'Benutzerspeicher', + 177: 'Kernelspeicher', + 178: 'Leerlauf', + 179: 'I/O-Wartezeit', + 180: 'ACME', + 181: 'Gesamt', + 182: 'Verwendet', + 183: 'Verfügbar', + 184: 'zram verwendet', + 185: 'zram gesamt', + 186: 'zram verfügbar', + 187: 'Systemzeit', + 188: 'Betriebszeit', + 189: 'Temperatur', + 190: 'Arbeitsspeicher', + 191: 'Speicher', + 192: 'Kapazität', + 193: 'Fehler bei Uhrzeitsynchronisierung', + 194: 'die Dokumentation', + 195: 'Tage', + 196: 'Stunden', + 197: 'Minuten', + 198: 'Sekunden', + 199: 'Vollständig anzeigen', + 200: 'Bericht anzeigen', + 201: 'Stapelaktion', + 202: 'Als gelesen markieren', + 203: 'Als ungelesen markieren', + 204: 'Datum', + 205: 'Titel', + 206: 'Dienst', + 207: 'Nachricht', + 208: 'Keine Benachrichtigungen', + 209: 'Erforderlich', + 210: 'Optional', + 211: 'Kein Grund angegeben', + 212: 'Aufgaben', + 213: 'Typ', + 214: 'Beschreibung', + 215: 'Alle Aufgaben abgeschlossen', + 216: 'Starten', + 217: 'Stoppen', + 218: 'Abhängigkeiten', + 219: 'Erfüllt', + 220: 'Keine Abhängigkeiten', + 221: 'Nicht installiert', + 222: 'Falsche Version', + 223: 'Nicht aktiv', + 224: 'Aktion erforderlich', + 225: 'Erforderlicher Gesundheitscheck fehlgeschlagen', + 226: 'Abhängigkeit hat ein Problem mit einer weiteren Abhängigkeit', + 227: 'Unbekannter Fehler', + 228: 'Fehler', + 229: '"Container neu bauen" ist eine harmlose Aktion, die nur wenige Sekunden dauert. Sie wird dieses Problem wahrscheinlich beheben.', + 230: '"Dienst deinstallieren" ist eine gefährliche Aktion, die den Dienst aus StartOS entfernt und alle zugehörigen Daten dauerhaft löscht.', + 231: 'Container neu bauen', + 232: 'Dienst deinstallieren', + 233: 'Vollständige Nachricht anzeigen', + 234: 'Dienstfehler', + 235: 'Warte auf Ergebnis', + 236: 'Wird gestartet', + 237: 'Erfolg', + 238: 'Gesundheitschecks', + 239: 'Keine Gesundheitschecks', + 240: 'Name', + 241: 'Status', + 242: 'Öffnen', + 243: 'Schnittstellen', + 244: 'Hosting', + 245: 'Installation läuft', + 246: 'Siehe unten', + 247: 'Steuerelemente', + 248: 'Keine Dienste installiert', + 249: 'Läuft', + 250: 'Gestoppt', + 251: 'Aufgabe erforderlich', + 252: 'Wird aktualisiert', + 253: 'Wird gestoppt', + 254: 'Vertrauen Sie Ihrer Root-CA', + 255: 'Sicherung läuft', + 256: 'Wird neu gestartet', + 257: 'Zurück', + 258: 'Wiederherstellen', + 259: 'Unbekannt', + 260: 'Anzeigen/Verbergen', + 261: 'Anzeigen', + 262: 'Diesen QR-Code scannen', + 263: 'Auf Standard zurücksetzen', + 264: 'Durch diese Änderung funktionieren die folgenden Dienste möglicherweise nicht mehr richtig und könnten abstürzen', + 265: 'Fehler beim Starten des Dienstes', + 266: 'Problem', + 267: 'Fehlgeschlagen', + 268: 'Gesund', + 269: 'Wird abgeschlossen', + 270: 'unbekannt %', + 271: 'Nicht angegeben', + 272: 'Links', + 273: 'Git-Hash', + 274: 'Lizenz', + 275: 'Installiert von', + 276: 'Dienst-Repository', + 277: 'Paket-Repository', + 278: 'Marketing-Website', + 279: 'Support-Website', + 280: 'Spendenlink', + 281: 'Standardaktionen', + 282: 'Dienst neu bauen', + 283: 'Baut den Dienst-Container neu. Nur erforderlich, wenn ein Fehler in StartOS vorliegt.', + 284: 'Deinstallieren', + 285: 'Deinstalliert diesen Dienst aus StartOS und löscht alle Daten dauerhaft.', + 286: 'Dashboard', + 287: 'dashboard', + 288: 'aktionen', + 289: 'anleitungen', + 290: 'logs', + 291: 'über', + 292: 'Upload wird gestartet', + 293: 'Erneut versuchen', + 294: '.s9pk-Paketdatei hochladen', + 295: 'Warnung: Der Upload über Tor ist langsam. Wechseln Sie für bessere Leistung ins lokale Netzwerk.', + 296: 'Hochladen', + 297: 'Version 1 s9pk erkannt. Dieses Format ist veraltet. Falls nötig, kann ein V1 s9pk über start-cli installiert werden.', + 298: 'Ungültige Paketdatei', + 299: 'Fügen Sie ACME-Anbieter hinzu, um SSL-(https)-Zertifikate für den Clearnet-Zugriff zu generieren.', + 300: 'Anleitung anzeigen', + 301: 'Gespeicherte Anbieter', + 302: 'Anbieter hinzufügen', + 303: 'Kontakt', + 304: 'Bearbeiten', + 305: 'ACME-Anbieter hinzufügen', + 306: 'ACME-Anbieter bearbeiten', + 307: 'Kontakt-E-Mails', + 308: 'Erforderlich, um ein Zertifikat von einer Zertifizierungsstelle zu erhalten', + 309: 'Alle umschalten', + 310: 'Fertig', + 311: 'Master-Passwort erforderlich', + 312: 'Geben Sie Ihr Master-Passwort ein, um diese Sicherung zu verschlüsseln.', + 313: 'Master-Passwort', + 314: 'Master-Passwort eingeben', + 315: 'Originalpasswort erforderlich', + 316: 'Diese Sicherung wurde mit einem anderen Passwort erstellt. Bitte geben Sie das ursprüngliche Passwort ein, das zur Verschlüsselung verwendet wurde.', + 317: 'Originalpasswort', + 318: 'Originalpasswort eingeben', + 319: 'Sicherung wird gestartet', + 320: 'Sichern Sie StartOS und Dienstdaten, indem Sie sich mit einem Gerät im lokalen Netzwerk oder einem physischen Laufwerk verbinden, das an Ihren Server angeschlossen ist.', + 321: 'Stellen Sie StartOS und Dienstdaten von einem Gerät im lokalen Netzwerk oder einem physischen Laufwerk mit vorhandener Sicherung wieder her.', + 322: 'Letzte Sicherung', + 323: 'Ein Ordner auf einem anderen Computer, der mit demselben Netzwerk wie Ihr Start9-Server verbunden ist.', + 324: 'Ein physisches Laufwerk, das direkt an Ihren Start9-Server angeschlossen ist.', + 325: 'Dienste für Sicherung auswählen', + 326: 'Serversicherung auswählen', + 327: 'Netzwerkordner', + 328: 'Neuen öffnen', + 329: 'Hostname', + 330: 'Pfad', + 331: 'URL', + 332: 'Netzwerkschnittstelle', + 333: 'Protokoll', + 334: 'Modell', + 335: 'User-Agent', + 336: 'Plattform', + 337: 'Zuletzt aktiv', + 338: 'Erstellt am', + 339: 'Algorithmus', + 340: 'Fingerabdruck', + 341: 'Paket-Hash', + 342: 'Veröffentlicht', + 343: 'Neuer Netzwerkordner', + 344: 'Netzwerkordner aktualisieren', + 345: 'Verbindung zum freigegebenen Ordner wird getestet', + 346: 'Stellen Sie sicher, dass (1) das Zielgerät im selben LAN ist wie Ihr Start9-Server, (2) der Zielordner freigegeben ist und (3) Hostname, Pfad und Anmeldedaten korrekt sind.', + 347: 'Verbindung fehlgeschlagen', + 348: 'Netzwerkordner enthält keine gültige Sicherung', + 349: 'Verbinden', + 350: 'Benutzername', + 351: 'Passwort', + 352: 'Der Hostname Ihres Zielgeräts im lokalen Netzwerk.', + 353: 'Unter Windows ist dies der vollständige Pfad zum freigegebenen Ordner (z. B. /Desktop/mein-ordner). Unter Linux und Mac ist es der tatsächliche Name des Ordners (z. B. mein-freigabe-ordner).', + 354: 'Unter Linux ist dies der Samba-Benutzername, den Sie beim Freigeben erstellt haben. Unter Mac und Windows ist es der Benutzername des Freigebenden.', + 355: 'Unter Linux ist dies das Samba-Passwort. Unter Mac und Windows ist es das Passwort des freigebenden Benutzers.', + 356: 'Physische Laufwerke', + 357: 'Keine Laufwerke erkannt', + 358: 'Aktualisieren', + 359: 'Die Partition enthält keine gültige Sicherung', + 360: 'Sicherungsfortschritt', + 361: 'Abgeschlossen', + 362: 'Sicherung läuft', + 363: 'Warten', + 364: 'Sicherung erstellt', + 365: 'Wiederherstellung ausgewählt', + 366: 'Initialisierung', + 367: 'Nicht verfügbar. Sicherung wurde mit einer neueren Version von StartOS erstellt.', + 368: 'Nicht verfügbar. Dienst ist bereits installiert.', + 369: 'Bereit zur Wiederherstellung', + 370: 'Lokaler Hostname', + 371: 'Erstellt', + 372: 'Passwort erforderlich', + 373: 'Geben Sie das Master-Passwort ein, das zur Verschlüsselung dieser Sicherung verwendet wurde. Im nächsten Schritt wählen Sie die Dienste aus, die wiederhergestellt werden sollen.', + 374: 'Laufwerk wird entschlüsselt', + 375: 'Dienste zur Wiederherstellung auswählen', + 376: 'Für Sicherung verfügbar', + 377: 'StartOS-Sicherungen erkannt', + 378: 'Keine StartOS-Sicherungen erkannt', + 379: 'StartOS-Version', + 380: 'Die Verbindung zu einem externen SMTP-Server ermöglicht es StartOS und seinen Diensten, E-Mails zu senden.', + 381: 'SMTP-Zugangsdaten', + 382: 'Test-E-Mail senden', + 383: 'Senden', + 384: 'E-Mail wird gesendet', + 385: 'Eine Test-E-Mail wurde gesendet an', + 386: 'Prüfen Sie Ihren Spam-Ordner und markieren Sie die Nachricht als „kein Spam“.', + 387: 'Die Web-Benutzeroberfläche Ihres StartOS-Servers, zugänglich über jeden Browser.', + 388: 'Ändern Sie Ihr Master-Passwort für StartOS.', + 389: 'Sie benötigen weiterhin Ihr aktuelles Passwort, um bestehende Sicherungen zu entschlüsseln!', + 390: 'Neue Passwörter stimmen nicht überein', + 391: 'Neues Passwort muss mindestens 12 Zeichen lang sein', + 392: 'Neues Passwort darf höchstens 64 Zeichen haben', + 393: 'Aktuelles Passwort ist ungültig', + 394: 'Passwort wurde geändert', + 395: 'Aktuelles Passwort', + 396: 'Neues Passwort', + 397: 'Neues Passwort erneut eingeben', + 398: 'Eine Sitzung ist ein Gerät, das aktuell bei StartOS angemeldet ist. Beenden Sie Sitzungen, die Sie nicht kennen oder nicht mehr verwenden.', + 399: 'Aktuelle Sitzung', + 400: 'Weitere Sitzungen', + 401: 'Ausgewählte beenden', + 402: 'Sitzungen werden beendet', + 403: 'Keine Sitzungen', + 404: 'Passwort erforderlich', + 405: 'Verbunden', + 406: 'Vergessen', + 407: 'WiFi-Zugangsdaten', + 408: 'Veraltet', + 409: 'Die WLAN-Unterstützung wird in StartOS v0.4.1 entfernt. Wenn Sie keinen Zugriff auf Ethernet haben, können Sie einen WLAN-Extender verwenden, um sich mit dem lokalen Netzwerk zu verbinden und dann Ihren Server über Ethernet an den Extender anschließen. Bitte wenden Sie sich bei Fragen an den Start9-Support.', + 410: 'Bekannte Netzwerke', + 411: 'Weitere Netzwerke', + 412: 'WiFi ist deaktiviert', + 413: 'Keine drahtlose Schnittstelle erkannt', + 414: 'WiFi wird aktiviert', + 415: 'WiFi wird deaktiviert', + 416: 'Verbindung wird hergestellt. Dies kann einen Moment dauern', + 417: 'Erneut versuchen', + 418: 'Mehr anzeigen', + 419: 'Versionshinweise', + 420: 'Eintrag anzeigen', + 421: 'Dienste, die von folgendem abhängen:', + 422: 'werden nicht mehr ordnungsgemäß funktionieren und könnten abstürzen.', + 423: 'Anfrage fehlgeschlagen', + 424: 'Alle Dienste sind auf dem neuesten Stand!', + 425: 'Ausführen', + 426: 'Aktion kann nur ausgeführt werden, wenn der Dienst', + 427: 'Verboten', + 428: 'kann vorübergehend Probleme verursachen', + 429: 'hat unerfüllte Abhängigkeiten. Es wird nicht wie erwartet funktionieren.', + 430: 'Container wird neu gebaut', + 431: 'Deinstallation wird gestartet', + 432: 'wird alle Daten dauerhaft löschen.', + 433: 'Deinstallation läuft', + 434: 'Versuche, den Server zu erreichen', + 435: 'Verbindung wiederhergestellt', + 436: 'Unbekannter Status', + 437: 'Server verbunden', + 438: 'Kein Internet', + 439: 'Verbindung wird hergestellt', + 440: 'Fährt herunter', + 441: 'Versionen', + 442: 'Neue Benachrichtigungen', + 443: 'Anzeigen', + 444: 'PWA wird neu geladen', + 445: 'Abgeschlossen', + 446: 'Systemdaten', + 447: 'Nicht versucht', + 448: 'Fehlgeschlagen', + 449: 'Erfolgreich', + 450: 'Starten Sie Ihren Server neu, damit diese Updates wirksam werden. Es kann mehrere Minuten dauern, bis er wieder online ist.', + 451: 'StartOS-Download abgeschlossen', + 452: 'Unbekanntes Speicherlaufwerk erkannt', + 453: 'Um ein anderes Speicherlaufwerk zu verwenden, ersetzen Sie das aktuelle und klicken Sie unten auf SERVER NEUSTARTEN. Um das aktuelle Speicherlaufwerk zu verwenden, klicken Sie auf AKTUELLES LAUFWERK VERWENDEN und folgen Sie den Anweisungen. Während dieses Vorgangs werden keine Daten gelöscht.', + 454: 'Speicherlaufwerk nicht gefunden', + 455: 'Fügen Sie Ihr StartOS-Speicherlaufwerk ein und klicken Sie unten auf SERVER NEUSTARTEN.', + 456: 'Speicherlaufwerk beschädigt. Dies kann durch Datenkorruption oder physische Beschädigung verursacht worden sein.', + 457: 'Es kann möglich sein, dieses Laufwerk durch Neuformatierung und Wiederherstellung aus einer Sicherung erneut zu verwenden. Um den Wiederherstellungsmodus zu starten, klicken Sie unten auf WIEDERHERSTELLUNGSMODUS STARTEN und folgen Sie den Anweisungen. In diesem Schritt werden keine Daten gelöscht.', + 458: 'Dateisystemfehler', + 459: 'Die Reparatur der Festplatte könnte das Problem beheben. Bitte NICHT das Laufwerk oder den Server währenddessen trennen – dies kann die Situation verschlimmern.', + 460: 'Fehler bei der Laufwerksverwaltung', + 461: 'Bitte kontaktieren Sie den Support', + 462: 'Diagnosemodus', + 463: 'Startfehler', + 464: 'Logs anzeigen', + 465: 'Mögliche Lösungen', + 466: 'Aktuelles Laufwerk einrichten', + 467: 'Wiederherstellungsmodus starten', + 468: 'Server wird neu gestartet', + 469: 'Warten Sie, bis der Server neu gestartet ist, und aktualisieren Sie dann diese Seite.', + 470: 'Server neu starten', + 471: 'Laufwerk reparieren', + 472: 'Ihr Server wird eingerichtet', + 473: 'Fortschritt', + 474: 'Bei StartOS anmelden', + 475: 'Anmelden', + 476: 'Anmeldung läuft', + 477: 'Passwort darf nicht länger als 64 Zeichen sein', + 478: 'Ungültiges Passwort', + 479: 'Laden Sie Ihre Root-Zertifizierungsstelle herunter und vertrauen Sie ihr, um eine sichere (HTTPS-)Verbindung herzustellen. Dies müssen Sie auf jedem Gerät wiederholen, mit dem Sie eine Verbindung zum Server herstellen.', + 480: 'Speichern Sie diese Seite, damit Sie später darauf zugreifen können. Sie finden diese Adresse auch in der Datei, die am Ende der Erstinstallation heruntergeladen wurde.', + 481: 'Sie haben Ihrer Root-Zertifizierungsstelle erfolgreich vertraut und können sich jetzt sicher anmelden.', + 482: 'Ihr Server verwendet seine Root-Zertifizierungsstelle, um SSL/TLS-Zertifikate für sich selbst und installierte Dienste zu erstellen. Diese Zertifikate werden verwendet, um die Netzwerkverbindung mit Ihren Geräten zu verschlüsseln.', + 483: 'Befolgen Sie die Anweisungen für Ihr Betriebssystem. Wenn Sie Ihrer Root-Zertifizierungsstelle vertrauen, kann Ihr Gerät die Echtheit der verschlüsselten Kommunikation mit Ihrem Server überprüfen.', + 484: 'Laden Sie die Seite neu. Wenn das nicht funktioniert, beenden Sie Ihren Browser und öffnen Sie ihn erneut, um diese Seite erneut zu besuchen.', + 485: 'StartOS-Benutzeroberfläche', + 486: 'WiFi', +} satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/polish.ts b/web/projects/shared/src/i18n/dictionaries/polish.ts new file mode 100644 index 000000000..677497b90 --- /dev/null +++ b/web/projects/shared/src/i18n/dictionaries/polish.ts @@ -0,0 +1,490 @@ +import type { i18n } from '../i18n.providers' + +export default { + 1: 'Zmień', + 2: 'Aktualizuj', + 3: 'Zresetuj', + 4: 'System', + 5: 'Ogólne', + 6: 'E-mail', + 7: 'Utwórz kopię zapasową', + 8: 'Przywróć kopię zapasową', + 9: 'Przejdź do logowania', + 10: 'Testuj', + 11: 'Pomiń', + 12: 'Aktywne sesje', + 13: 'Zmień hasło', + 14: 'Ustawienia ogólne', + 15: 'Zarządzaj swoim ogólnym ustawieniem i preferencjami', + 16: 'Tytuł zakładki przeglądarki', + 17: 'Język', + 18: 'Naprawa dysku', + 19: 'Spróbuj automatycznej naprawy', + 20: 'Napraw', + 21: 'Główne Centrum Certyfikacji (Root CA)', + 22: 'Pobierz swoje Root CA', + 23: 'Pobierz', + 24: 'Zresetuj Tor', + 25: 'Zrestartuj usługę Tor na swoim serwerze', + 26: 'Aktualizacja oprogramowania', + 27: 'Uruchom ponownie, aby zastosować', + 28: 'Sprawdź dostępność aktualizacji', + 29: 'Ta wartość będzie wyświetlana jako tytuł zakładki w przeglądarce.', + 30: 'Nazwa urządzenia', + 31: 'StartOS', + 32: 'Zapisz', + 33: 'Zapisywanie', + 34: 'Ostrzeżenie', + 35: 'Potwierdź', + 36: 'Anuluj', + 37: 'Tę akcję należy wykonać tylko na polecenie specjalisty wsparcia Start9. Zalecamy wykonanie kopii zapasowej urządzenia przed kontynuacją. Jeśli podczas ponownego uruchamiania urządzenia dojdzie do przerwy w zasilaniu lub odłączenia dysku, system plików może ulec trwałemu uszkodzeniu. Proszę zachować ostrożność.', + 38: 'Usuń', + 39: 'Trwa resetowanie Tora', + 40: 'Resetowanie Tora', + 41: 'Sprawdzanie aktualizacji', + 42: 'Rozpoczynanie restartu', + 43: 'Używasz najnowszej wersji StartOS.', + 44: 'Aktualne!', + 45: 'Informacje o wydaniu', + 46: 'Rozpocznij aktualizację', + 47: 'Rozpoczynanie aktualizacji', + 48: 'Obecnie jesteś połączony przez Tor. Jeśli zresetujesz usługę Tor, utracisz połączenie aż do momentu ponownego nawiązania.', + 49: 'Zresetować Tora?', + 50: 'Opcjonalnie wyczyść stan, aby wymusić pozyskanie nowych węzłów strażniczych. Zaleca się najpierw spróbować bez czyszczenia stanu.', + 51: 'Wyczyść stan', + 52: 'Zapisywanie najlepszego wyniku', + 53: 'Wynik', + 54: 'Najlepszy wynik', + 55: 'Zapisz i wyjdź', + 56: 'Dodaj stronę do zakładek', + 57: 'Rozpoczynanie wyłączania', + 58: 'Dodaj', + 59: 'Ok', + 60: 'Czy na pewno chcesz usunąć ten wpis?', + 61: 'Tej wartości nie można zmienić po jej ustawieniu', + 62: 'Kontynuuj', + 63: 'Kliknij lub upuść plik tutaj', + 64: 'Upuść plik tutaj', + 65: 'Wyłączone', + 66: 'Wersja', + 67: 'Kopiuj', + 68: 'O tym serwerze', + 69: 'Ustawienia systemowe', + 70: 'Restartuj', + 71: 'Wyłącz', + 72: 'Wyloguj się', + 73: 'Podręcznik użytkownika', + 74: 'Skontaktuj się ze wsparciem', + 75: 'Wesprzyj Start9', + 76: 'Czy na pewno chcesz zrestartować serwer? Może to potrwać kilka minut, zanim znów będzie dostępny.', + 77: 'Czy na pewno chcesz wyłączyć serwer? Może to potrwać kilka minut, a serwer nie włączy się automatycznie. Aby go uruchomić ponownie, musisz fizycznie odłączyć i ponownie podłączyć zasilanie.', + 78: 'Usługi', + 79: 'Rynek', + 80: 'Ręczna instalacja', + 81: 'Aktualizacje', + 82: 'Metryki', + 83: 'Logi', + 84: 'Powiadomienia', + 85: 'Uruchom interfejs', + 86: 'Pokaż kod QR', + 87: 'Kopiuj URL', + 88: 'Akcje', + 89: 'niezalecane', + 90: 'Zaufano CA głównemu!', + 91: 'Dodaj adres clearnet, aby udostępnić ten interfejs w Internecie. Adresy clearnet są całkowicie publiczne i nieanonimowe.', + 92: 'Dowiedz się więcej', + 93: 'Upublicznij', + 94: 'Uprywatnij', + 95: 'Brak publicznych adresów', + 96: 'Dodaj domenę', + 97: 'Usuwanie', + 98: 'Upublicznianie', + 99: 'Prywatna konfiguracja', + 100: 'Niezapisane zmiany', + 101: 'Masz niezapisane zmiany. Czy na pewno chcesz opuścić stronę?', + 102: 'Opuść', + 103: 'Czy jesteś pewien?', + 104: 'Wybierz domenę', + 105: 'Lokalna', + 106: 'Adresy lokalne są dostępne tylko dla urządzeń podłączonych do tej samej sieci LAN co Twój serwer — bezpośrednio lub przez VPN.', + 107: 'Dowiedz się więcej', + 108: 'Publiczna', + 109: 'Prywatna', + 110: 'Dodaj adres onion, aby anonimowo udostępnić ten interfejs w sieci Tor. Adresy onion są dostępne tylko przez sieć Tor.', + 111: 'Brak adresów onion', + 112: 'Nowy adres Onion', + 113: 'Klucz prywatny (opcjonalnie)', + 114: 'Opcjonalnie podaj klucz prywatny ed25519 zakodowany w base64, aby wygenerować adres Tor V3 (.onion). Jeśli nie zostanie podany, zostanie wygenerowany losowy klucz.', + 115: 'Przetwarzanie 10 000 logów', + 116: 'Ładowanie starszych logów', + 117: 'Oczekiwanie na połączenie z siecią', + 118: 'Ponowne łączenie', + 119: 'Ładowanie logów', + 120: 'Przewiń na dół', + 121: 'Połączono ponownie', + 122: 'Rozłączono', + 123: 'Więcej', + 124: 'Wprowadzono następujące zmiany', + 125: 'dodano', + 126: 'usunięto', + 127: 'zmieniono z', + 128: 'na', + 129: 'wpis', + 130: 'lista', + 131: 'nowy wpis', + 132: 'nowa lista', + 133: 'Wyślij', + 134: 'Zamknij', + 135: 'Logi systemowe', + 136: 'Logi jądra', + 137: 'Logi Tor', + 138: 'Surowe, nieprzefiltrowane logi systemu operacyjnego', + 139: 'Diagnostyka sterowników i innych procesów jądra', + 140: 'Logi diagnostyczne demona Tor na StartOS', + 141: 'Obniż wersję', + 142: 'Zainstaluj ponownie', + 143: 'Zobacz zainstalowane', + 144: 'Przełącz', + 145: 'Zainstaluj', + 146: 'Rozpoczynanie instalacji', + 147: 'Zmień rejestr', + 148: 'Usługi z tego rejestru są pakietowane i utrzymywane przez zespół Start9. Jeśli napotkasz problem lub masz pytania dotyczące usługi z tego rejestru, nasz zespół wsparcia z przyjemnością Ci pomoże.', + 149: 'Usługi z tego rejestru są tworzone i utrzymywane przez członków społeczności Start9. Instalujesz je na własne ryzyko. W przypadku problemów lub pytań skontaktuj się z twórcą pakietu.', + 150: 'Usługi z tego rejestru są w fazie beta i mogą zawierać błędy. Instalujesz je na własne ryzyko.', + 151: 'Usługi z tego rejestru są w fazie testów alfa. Mogą zawierać błędy i potencjalnie uszkodzić system. Instalujesz je na własne ryzyko.', + 152: 'To jest rejestr niestandardowy. Start9 nie może zweryfikować integralności ani działania usług z tego rejestru i mogą one uszkodzić Twój system. Instalujesz je na własne ryzyko.', + 153: 'Rejestry domyślne', + 154: 'Rejestry niestandardowe', + 155: 'Dodaj niestandardowy rejestr', + 156: 'Zapisz na później', + 157: 'Zapisz i połącz', + 158: 'Usuwanie', + 159: 'Zmiana rejestru', + 160: 'Ładowanie', + 161: 'Rejestr już dodany', + 162: 'Weryfikowanie rejestru', + 163: 'Czy na pewno chcesz usunąć ten rejestr?', + 164: 'Dodaj niestandardowy rejestr', + 165: 'Pełny URL niestandardowego rejestru', + 166: 'Musi być poprawnym adresem URL', + 167: 'zainstalowano z', + 168: 'zainstalowano ręcznie', + 169: 'Ta usługa została pierwotnie', + 170: 'ale obecnie jesteś połączony z', + 171: 'Aby zainstalować z', + 172: 'mimo to kliknij „Kontynuuj”.', + 173: 'W wyniku tej aktualizacji poniższe usługi mogą przestać działać poprawnie lub się zawiesić', + 174: 'Alert', + 175: 'Procent użycia', + 176: 'Przestrzeń użytkownika', + 177: 'Przestrzeń jądra', + 178: 'Bezczynność', + 179: 'Oczekiwanie na I/O', + 180: 'ACME', + 181: 'Łącznie', + 182: 'Użyte', + 183: 'Dostępne', + 184: 'zram użyte', + 185: 'zram łącznie', + 186: 'zram dostępne', + 187: 'Czas systemowy', + 188: 'Czas działania', + 189: 'Temperatura', + 190: 'Pamięć', + 191: 'Pamięć masowa', + 192: 'Pojemność', + 193: 'Błąd synchronizacji zegara', + 194: 'dokumentacja', + 195: 'Dni', + 196: 'Godziny', + 197: 'Minuty', + 198: 'Sekundy', + 199: 'Zobacz całość', + 200: 'Zobacz raport', + 201: 'Operacja zbiorcza', + 202: 'Oznacz jako przeczytane', + 203: 'Oznacz jako nieprzeczytane', + 204: 'Data', + 205: 'Tytuł', + 206: 'Usługa', + 207: 'Wiadomość', + 208: 'Brak powiadomień', + 209: 'Wymagane', + 210: 'Opcjonalne', + 211: 'Nie podano powodu', + 212: 'Zadania', + 213: 'Typ', + 214: 'Opis', + 215: 'Wszystkie zadania zakończone', + 216: 'Uruchom', + 217: 'Zatrzymaj', + 218: 'Zależności', + 219: 'Spełnione', + 220: 'Brak zależności', + 221: 'Nie zainstalowano', + 222: 'Nieprawidłowa wersja', + 223: 'Nie działa', + 224: 'Wymagana akcja', + 225: 'Wymagana kontrola stanu nie powiodła się', + 226: 'Zależność ma problem z zależnością', + 227: 'Nieznany błąd', + 228: 'Błąd', + 229: '„Odbuduj kontener” to nieszkodliwa akcja, która trwa tylko kilka sekund. Może rozwiązać ten problem.', + 230: '„Odinstaluj usługę” to niebezpieczna akcja, która usunie usługę ze StartOS i usunie wszystkie jej dane.', + 231: 'Odbuduj kontener', + 232: 'Odinstaluj usługę', + 233: 'Zobacz pełną wiadomość', + 234: 'Błąd usługi', + 235: 'Oczekiwanie na wynik', + 236: 'Uruchamianie', + 237: 'Sukces', + 238: 'Kontrole stanu', + 239: 'Brak kontroli stanu', + 240: 'Nazwa', + 241: 'Status', + 242: 'Otwórz', + 243: 'Interfejsy', + 244: 'Hosting', + 245: 'Instalowanie', + 246: 'Zobacz poniżej', + 247: 'Sterowanie', + 248: 'Brak zainstalowanych usług', + 249: 'Działa', + 250: 'Zatrzymano', + 251: 'Wymagane zadanie', + 252: 'Aktualizowanie', + 253: 'Zatrzymywanie', + 254: 'Zaufaj swojemu CA głównemu', + 255: 'Tworzenie kopii zapasowej', + 256: 'Ponowne uruchamianie', + 257: 'Wstecz', + 258: 'Przywracanie', + 259: 'Nieznane', + 260: 'Pokaż/Ukryj', + 261: 'Pokaż', + 262: 'Zeskanuj ten kod QR', + 263: 'Przywróć domyślne ustawienia', + 264: 'W wyniku tej zmiany następujące usługi mogą przestać działać poprawnie lub się zawiesić', + 265: 'Błąd uruchamiania usługi', + 266: 'Problem', + 267: 'Niepowodzenie', + 268: 'Zdrowa', + 269: 'finalizowanie', + 270: 'nieznany %', + 271: 'Nie podano', + 272: 'Linki', + 273: 'Hash Git', + 274: 'Licencja', + 275: 'Zainstalowano z', + 276: 'Repozytorium usługi', + 277: 'Repozytorium pakietu', + 278: 'Strona marketingowa', + 279: 'Strona wsparcia', + 280: 'Link do darowizny', + 281: 'Standardowe akcje', + 282: 'Odbuduj usługę', + 283: 'Odbudowuje kontener usługi. Konieczne tylko w przypadku błędu w StartOS', + 284: 'Odinstaluj', + 285: 'Odinstalowuje tę usługę z StartOS i trwale usuwa wszystkie dane.', + 286: 'Panel', + 287: 'panel', + 288: 'akcje', + 289: 'instrukcje', + 290: 'logi', + 291: 'informacje', + 292: 'Rozpoczynanie przesyłania', + 293: 'Spróbuj ponownie', + 294: 'Prześlij plik pakietu .s9pk', + 295: 'Uwaga: przesyłanie pakietu przez Tor będzie powolne. Przełącz na sieć lokalną dla lepszej wydajności.', + 296: 'Prześlij', + 297: 'Wykryto pakiet s9pk w wersji 1. Ten format jest przestarzały. Możesz zainstalować V1 s9pk ręcznie przez start-cli, jeśli to konieczne.', + 298: 'Nieprawidłowy plik pakietu', + 299: 'Dodaj dostawców ACME, aby wygenerować certyfikaty SSL (https) dla dostępu przez clearnet.', + 300: 'Zobacz instrukcje', + 301: 'Zapisani dostawcy', + 302: 'Dodaj dostawcę', + 303: 'Kontakt', + 304: 'Edytuj', + 305: 'Dodaj dostawcę ACME', + 306: 'Edytuj dostawcę ACME', + 307: 'Adresy e-mail kontaktowe', + 308: 'Wymagane do uzyskania certyfikatu od Urzędu Certyfikacji', + 309: 'Przełącz wszystkie', + 310: 'Gotowe', + 311: 'Wymagana hasło główne', + 312: 'Wprowadź swoje hasło główne, aby zaszyfrować tę kopię zapasową.', + 313: 'Hasło główne', + 314: 'Wprowadź hasło główne', + 315: 'Wymagane oryginalne hasło', + 316: 'Ta kopia zapasowa została utworzona z innym hasłem. Wprowadź oryginalne hasło użyte do szyfrowania tej kopii.', + 317: 'Oryginalne hasło', + 318: 'Wprowadź oryginalne hasło', + 319: 'Rozpoczynanie tworzenia kopii zapasowej', + 320: 'Utwórz kopię zapasową StartOS i danych usług, podłączając się do urządzenia w sieci lokalnej lub do fizycznego dysku podłączonego do serwera.', + 321: 'Przywróć StartOS i dane usług z urządzenia w sieci lokalnej lub z fizycznego dysku podłączonego do serwera zawierającego istniejącą kopię zapasową.', + 322: 'Ostatnia kopia zapasowa', + 323: 'Folder na innym komputerze podłączonym do tej samej sieci co Twój serwer Start9.', + 324: 'Fizyczny dysk podłączony bezpośrednio do Twojego serwera Start9.', + 325: 'Wybierz usługi do kopii zapasowej', + 326: 'Wybierz kopię zapasową serwera', + 327: 'Foldery sieciowe', + 328: 'Otwórz nowy', + 329: 'Nazwa hosta', + 330: 'Ścieżka', + 331: 'URL', + 332: 'Interfejs sieciowy', + 333: 'Protokół', + 334: 'Model', + 335: 'Agent użytkownika', + 336: 'Platforma', + 337: 'Ostatnia aktywność', + 338: 'Utworzono', + 339: 'Algorytm', + 340: 'Odcisk palca', + 341: 'Suma kontrolna pakietu', + 342: 'Opublikowano', + 343: 'Nowy folder sieciowy', + 344: 'Zaktualizuj folder sieciowy', + 345: 'Testowanie połączenia z folderem współdzielonym', + 346: 'Upewnij się, że (1) komputer docelowy jest podłączony do tej samej sieci LAN co Twój serwer Start9, (2) folder docelowy jest udostępniony i (3) nazwa hosta, ścieżka i dane logowania są poprawne.', + 347: 'Nie udało się połączyć', + 348: 'Folder sieciowy nie zawiera prawidłowej kopii zapasowej', + 349: 'Połącz', + 350: 'Nazwa użytkownika', + 351: 'Hasło', + 352: 'Nazwa hosta urządzenia docelowego w lokalnej sieci (LAN).', + 353: 'W systemie Windows jest to pełna ścieżka do folderu współdzielonego (np. /Desktop/moj-folder). W systemach Linux i Mac jest to dosłowna nazwa folderu (np. moj-folder).', + 354: 'W systemie Linux to nazwa użytkownika samba, którą utworzyłeś przy udostępnianiu folderu. W systemach Mac i Windows to nazwa użytkownika osoby udostępniającej folder.', + 355: 'W systemie Linux to hasło samba, które ustawiłeś przy udostępnianiu folderu. W Mac i Windows to hasło użytkownika udostępniającego folder.', + 356: 'Dyski fizyczne', + 357: 'Nie wykryto dysków', + 358: 'Odśwież', + 359: 'Partycja dysku nie zawiera prawidłowej kopii zapasowej', + 360: 'Postęp tworzenia kopii zapasowej', + 361: 'Zakończono', + 362: 'Tworzenie kopii zapasowej', + 363: 'Oczekiwanie', + 364: 'Utworzono kopię zapasową', + 365: 'Wybrano przywracanie', + 366: 'Inicjalizacja', + 367: 'Niedostępne. Kopia zapasowa została wykonana na nowszej wersji StartOS.', + 368: 'Niedostępne. Usługa jest już zainstalowana.', + 369: 'Gotowe do przywrócenia', + 370: 'Lokalna nazwa hosta', + 371: 'Utworzono', + 372: 'Wymagane hasło', + 373: 'Wprowadź hasło główne użyte do zaszyfrowania tej kopii zapasowej. Na następnym ekranie wybierzesz usługi do przywrócenia.', + 374: 'Odszyfrowywanie dysku', + 375: 'Wybierz usługi do przywrócenia', + 376: 'Dostępne do kopii zapasowej', + 377: 'Wykryto kopie zapasowe StartOS', + 378: 'Nie wykryto kopii zapasowych StartOS', + 379: 'Wersja StartOS', + 380: 'Podłączenie zewnętrznego serwera SMTP pozwala StartOS i zainstalowanym usługom wysyłać wiadomości e-mail.', + 381: 'Dane logowania SMTP', + 382: 'Wyślij e-mail testowy', + 383: 'Wyślij', + 384: 'Wysyłanie e-maila', + 385: 'Wiadomość testowa została wysłana do', + 386: 'Sprawdź folder spam i oznacz wiadomość jako „nie spam”.', + 387: 'Webowy interfejs użytkownika Twojego serwera StartOS, dostępny z każdej przeglądarki.', + 388: 'Zmień swoje hasło główne StartOS.', + 389: 'Wciąż będziesz potrzebować aktualnego hasła, aby odszyfrować istniejące kopie zapasowe!', + 390: 'Nowe hasła nie są zgodne', + 391: 'Nowe hasło musi mieć co najmniej 12 znaków', + 392: 'Nowe hasło musi mieć mniej niż 65 znaków', + 393: 'Bieżące hasło jest nieprawidłowe', + 394: 'Hasło zostało zmienione', + 395: 'Bieżące hasło', + 396: 'Nowe hasło', + 397: 'Wpisz nowe hasło ponownie', + 398: 'Sesja to urządzenie aktualnie zalogowane do StartOS. Dla bezpieczeństwa zakończ sesje, których nie rozpoznajesz lub już nie używasz.', + 399: 'Obecna sesja', + 400: 'Inne sesje', + 401: 'Zakończ wybrane', + 402: 'Zamykanie sesji', + 403: 'Brak sesji', + 404: 'Wymagane hasło', + 405: 'Połączono', + 406: 'Zapomnij', + 407: 'Dane logowania WiFi', + 408: 'Przestarzałe', + 409: 'Obsługa WiFi zostanie usunięta w StartOS v0.4.1. Jeśli nie masz dostępu do sieci Ethernet, możesz użyć wzmacniacza WiFi, aby połączyć się z siecią lokalną, a następnie podłączyć serwer do wzmacniacza przez Ethernet. Skontaktuj się ze wsparciem Start9 w razie pytań.', + 410: 'Znane sieci', + 411: 'Inne sieci', + 412: 'WiFi jest wyłączone', + 413: 'Nie wykryto interfejsu bezprzewodowego', + 414: 'Włączanie WiFi', + 415: 'Wyłączanie WiFi', + 416: 'Łączenie... To może chwilę potrwać', + 417: 'Spróbuj ponownie', + 418: 'Pokaż więcej', + 419: 'Informacje o wydaniu', + 420: 'Zobacz ofertę', + 421: 'Usługi zależne od', + 422: 'mogą przestać działać poprawnie i ulec awarii.', + 423: 'Żądanie nie powiodło się', + 424: 'Wszystkie usługi są aktualne!', + 425: 'Uruchom', + 426: 'Działanie można wykonać tylko, gdy usługa jest', + 427: 'Zabronione', + 428: 'może tymczasowo napotkać problemy', + 429: 'ma niespełnione zależności. Nie będzie działać zgodnie z oczekiwaniami.', + 430: 'Odbudowywanie kontenera', + 431: 'Rozpoczynanie odinstalowania', + 432: 'usunie wszystkie dane na stałe.', + 433: 'Odinstalowywanie', + 434: 'Próba połączenia z serwerem', + 435: 'Połączenie przywrócone', + 436: 'Nieznany stan', + 437: 'Połączono z serwerem', + 438: 'Brak Internetu', + 439: 'Łączenie', + 440: 'Wyłączanie', + 441: 'Wersje', + 442: 'Nowe powiadomienia', + 443: 'Zobacz', + 444: 'Przeładowywanie PWA', + 445: 'Zakończono', + 446: 'Dane systemowe', + 447: 'Nie podjęto próby', + 448: 'Niepowodzenie', + 449: 'Powodzenie', + 450: 'Zrestartuj serwer, aby zastosować te aktualizacje. Może to potrwać kilka minut.', + 451: 'Pobieranie StartOS zakończone', + 452: 'Wykryto nieznany dysk', + 453: 'Aby użyć innego dysku, zamień obecny i kliknij RESTARTUJ SERWER poniżej. Aby użyć obecnego dysku, kliknij UŻYJ OBECNEGO DYSKU i postępuj zgodnie z instrukcjami. Dane nie zostaną usunięte w tym procesie.', + 454: 'Nie znaleziono dysku', + 455: 'Włóż dysk StartOS i kliknij RESTARTUJ SERWER poniżej.', + 456: 'Dysk uszkodzony. Może to być wynikiem uszkodzenia danych lub fizycznego uszkodzenia.', + 457: 'Możliwe, że dysk da się ponownie wykorzystać po jego sformatowaniu i odzyskaniu danych z kopii zapasowej. Aby wejść w tryb odzyskiwania, kliknij WEJDŹ W TRYB ODZYSKIWANIA i postępuj zgodnie z instrukcjami. Dane nie zostaną usunięte na tym etapie.', + 458: 'Błąd systemu plików', + 459: 'Naprawa dysku może pomóc rozwiązać problem. NIE odłączaj dysku ani serwera w tym czasie — może to pogorszyć sytuację.', + 460: 'Błąd zarządzania dyskiem', + 461: 'Skontaktuj się z pomocą techniczną', + 462: 'Tryb diagnostyczny', + 463: 'błąd uruchomienia', + 464: 'Zobacz logi', + 465: 'Możliwe rozwiązania', + 466: 'Skonfiguruj obecny dysk', + 467: 'Wejdź w tryb odzyskiwania', + 468: 'Serwer się restartuje', + 469: 'Poczekaj na ponowne uruchomienie serwera, a następnie odśwież tę stronę.', + 470: 'Restartuj serwer', + 471: 'Napraw dysk', + 472: 'Konfigurowanie serwera', + 473: 'Postęp', + 474: 'Zaloguj się do StartOS', + 475: 'Zaloguj się', + 476: 'Logowanie...', + 477: 'Hasło musi mieć mniej niż 65 znaków', + 478: 'Nieprawidłowe hasło', + 479: 'Pobierz i zaufaj swojej głównej CA, aby ustanowić bezpieczne połączenie (HTTPS). Musisz powtórzyć to na każdym urządzeniu, z którego łączysz się z serwerem.', + 480: 'Zapisz tę stronę, aby móc do niej wrócić. Ten adres znajdziesz też w pliku pobranym na końcu konfiguracji.', + 481: 'Zaufano Twojej głównej CA — możesz teraz bezpiecznie się zalogować.', + 482: 'Twój serwer używa głównej CA do generowania certyfikatów SSL/TLS dla siebie i zainstalowanych usług. Te certyfikaty są wykorzystywane do szyfrowania ruchu sieciowego z urządzeniami.', + 483: 'Postępuj zgodnie z instrukcjami dla swojego systemu. Po zaufaniu głównej CA, Twoje urządzenie będzie mogło weryfikować autentyczność szyfrowanej komunikacji z serwerem.', + 484: 'Odśwież stronę. Jeśli to nie pomoże, zamknij i ponownie otwórz przeglądarkę, a następnie wróć do tej strony.', + 485: 'Interfejs StartOS', + 486: 'WiFi', +} satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/spanish.ts b/web/projects/shared/src/i18n/dictionaries/spanish.ts new file mode 100644 index 000000000..209a667fa --- /dev/null +++ b/web/projects/shared/src/i18n/dictionaries/spanish.ts @@ -0,0 +1,490 @@ +import type { i18n } from '../i18n.providers' + +export default { + 1: 'Cambiar', + 2: 'Actualizar', + 3: 'Restablecer', + 4: 'Sistema', + 5: 'General', + 6: 'Correo electrónico', + 7: 'Crear copia de seguridad', + 8: 'Restaurar copia de seguridad', + 9: 'Ir a inicio de sesión', + 10: 'Probar', + 11: 'Omitir', + 12: 'Sesiones activas', + 13: 'Cambiar contraseña', + 14: 'Configuración general', + 15: 'Administra tu configuración y preferencias generales', + 16: 'Título de la pestaña del navegador', + 17: 'Idioma', + 18: 'Reparación de disco', + 19: 'Intentar reparación automática', + 20: 'Reparar', + 21: 'Autoridad certificadora raíz', + 22: 'Descargar tu CA raíz', + 23: 'Descargar', + 24: 'Restablecer Tor', + 25: 'Reiniciar el servicio Tor en tu servidor', + 26: 'Actualización de software', + 27: 'Reiniciar para aplicar', + 28: 'Buscar actualizaciones', + 29: 'Este valor se mostrará como el título de la pestaña de tu navegador.', + 30: 'Nombre del dispositivo', + 31: 'StartOS', + 32: 'Guardar', + 33: 'Guardando', + 34: 'Advertencia', + 35: 'Confirmar', + 36: 'Cancelar', + 37: 'Esta acción solo debe realizarse si es indicada por un especialista de soporte de Start9. Recomendamos hacer una copia de seguridad del dispositivo antes de ejecutar esta acción. Si ocurre algo durante el reinicio, como un corte de energía o desconectar el disco, el sistema de archivos quedará en un estado irrecuperable. Procede con precaución.', + 38: 'Eliminar', + 39: 'Restablecimiento de Tor en curso', + 40: 'Restableciendo Tor', + 41: 'Buscando actualizaciones', + 42: 'Iniciando reinicio', + 43: 'Estás usando la última versión de StartOS.', + 44: '¡Actualizado!', + 45: 'Notas de la versión', + 46: 'Iniciar actualización', + 47: 'Iniciando actualización', + 48: 'Actualmente estás conectado a través de Tor. Si restableces el servicio Tor, perderás la conexión hasta que vuelva a estar en línea.', + 49: '¿Restablecer Tor?', + 50: 'Opcionalmente borra el estado para forzar la adquisición de nuevos nodos de entrada. Se recomienda intentar sin borrar el estado primero.', + 51: 'Borrar estado', + 52: 'Guardando puntuación máxima', + 53: 'Puntuación', + 54: 'Puntuación máxima', + 55: 'Guardar y salir', + 56: 'Marcar esta página', + 57: 'Iniciando apagado', + 58: 'Agregar', + 59: 'Ok', + 60: '¿Estás seguro de que deseas eliminar esta entrada?', + 61: 'Este valor no se puede cambiar una vez establecido', + 62: 'Continuar', + 63: 'Haz clic o suelta el archivo aquí', + 64: 'Suelta el archivo aquí', + 65: 'Deshabilitado', + 66: 'Versión', + 67: 'Copiar', + 68: 'Acerca de este servidor', + 69: 'Configuración del sistema', + 70: 'Reiniciar', + 71: 'Apagar', + 72: 'Cerrar sesión', + 73: 'Manual del usuario', + 74: 'Contactar soporte', + 75: 'Donar a Start9', + 76: '¿Estás seguro de que deseas reiniciar tu servidor? Puede tardar varios minutos en volver a estar en línea.', + 77: '¿Estás seguro de que deseas apagar tu servidor? Esto puede tardar varios minutos, y tu servidor no volverá a encenderse automáticamente. Para encenderlo nuevamente, deberás desconectarlo y volverlo a conectar físicamente.', + 78: 'Servicios', + 79: 'Mercado', + 80: 'Instalación manual', + 81: 'Actualizaciones', + 82: 'Métricas', + 83: 'Registros', + 84: 'Notificaciones', + 85: 'Abrir interfaz', + 86: 'Mostrar QR', + 87: 'Copiar URL', + 88: 'Acciones', + 89: 'no recomendado', + 90: '¡CA raíz confiable!', + 91: 'Agrega una dirección clearnet para exponer esta interfaz en Internet. Las direcciones clearnet son totalmente públicas y no anónimas.', + 92: 'Saber más', + 93: 'Hacer público', + 94: 'Hacer privado', + 95: 'Sin direcciones públicas', + 96: 'Agregar dominio', + 97: 'Eliminando', + 98: 'Haciendo público', + 99: 'Haciendo privado', + 100: 'Cambios no guardados', + 101: 'Tienes cambios no guardados. ¿Estás seguro de que deseas salir?', + 102: 'Salir', + 103: '¿Estás seguro?', + 104: 'Seleccionar dominio', + 105: 'Local', + 106: 'Las direcciones locales solo pueden ser accedidas por dispositivos conectados a la misma red local que tu servidor, ya sea directamente o mediante una VPN.', + 107: 'Más información', + 108: 'Público', + 109: 'Privado', + 110: 'Agrega una dirección onion para exponer esta interfaz de forma anónima en la darknet. Las direcciones onion solo se pueden acceder a través de la red Tor.', + 111: 'Sin direcciones onion', + 112: 'Nueva dirección Onion', + 113: 'Clave privada (opcional)', + 114: 'Opcionalmente proporciona una clave privada ed25519 codificada en base64 para generar la dirección Tor V3 (.onion). Si no se proporciona, se generará una clave aleatoria.', + 115: 'Procesando 10,000 registros', + 116: 'Cargando registros anteriores', + 117: 'Esperando conectividad de red', + 118: 'Reconectando', + 119: 'Cargando registros', + 120: 'Desplazar hacia abajo', + 121: 'Reconectado', + 122: 'Desconectado', + 123: 'Más', + 124: 'Se realizaron las siguientes modificaciones', + 125: 'agregado', + 126: 'eliminado', + 127: 'cambiado de', + 128: 'a', + 129: 'entrada', + 130: 'lista', + 131: 'nueva entrada', + 132: 'nueva lista', + 133: 'Enviar', + 134: 'Cerrar', + 135: 'Registros del SO', + 136: 'Registros del kernel', + 137: 'Registros de Tor', + 138: 'Registros sin filtrar del sistema operativo', + 139: 'Diagnóstico de controladores y otros procesos del kernel', + 140: 'Registros de diagnóstico del servicio Tor en StartOS', + 141: 'Retroceder versión', + 142: 'Reinstalar', + 143: 'Ver instalados', + 144: 'Cambiar', + 145: 'Instalar', + 146: 'Iniciando instalación', + 147: 'Cambiar registro', + 148: 'Los servicios de este registro están empaquetados y mantenidos por el equipo de Start9. Si experimentas un problema o tienes preguntas relacionadas con un servicio de este registro, nuestro equipo de soporte estará encantado de ayudarte.', + 149: 'Los servicios de este registro están empaquetados y mantenidos por miembros de la comunidad Start9. Instálalos bajo tu propio riesgo. Si experimentas un problema o tienes una pregunta sobre un servicio en este mercado, comunícate con el desarrollador del paquete.', + 150: 'Los servicios de este registro están en fase de prueba beta y pueden contener errores. Instálalos bajo tu propio riesgo.', + 151: 'Los servicios de este registro están en fase de prueba alfa. Se espera que contengan errores y podrían dañar tu sistema. Instálalos bajo tu propio riesgo.', + 152: 'Este es un registro personalizado. Start9 no puede verificar la integridad ni el funcionamiento de los servicios de este registro, y podrían dañar tu sistema. Instálalos bajo tu propio riesgo.', + 153: 'Registros predeterminados', + 154: 'Registros personalizados', + 155: 'Agregar registro personalizado', + 156: 'Guardar para más tarde', + 157: 'Guardar y conectar', + 158: 'Eliminando', + 159: 'Cambiando registro', + 160: 'Cargando', + 161: 'Registro ya agregado', + 162: 'Validando registro', + 163: '¿Estás seguro de que deseas eliminar este registro?', + 164: 'Agregar registro personalizado', + 165: 'Una URL completamente calificada del registro personalizado', + 166: 'Debe ser una URL válida', + 167: 'instalado desde', + 168: 'instalado manualmente', + 169: 'Este servicio fue originalmente', + 170: 'pero actualmente estás conectado a', + 171: 'Para instalar desde', + 172: 'de todos modos, haz clic en "Continuar".', + 173: 'Como resultado de esta actualización, los siguientes servicios dejarán de funcionar correctamente y pueden fallar', + 174: 'Alerta', + 175: 'Porcentaje utilizado', + 176: 'Espacio de usuario', + 177: 'Espacio del kernel', + 178: 'Inactivo', + 179: 'Espera de E/S', + 180: 'ACME', + 181: 'Total', + 182: 'Usado', + 183: 'Disponible', + 184: 'zram usado', + 185: 'zram total', + 186: 'zram disponible', + 187: 'Hora del sistema', + 188: 'Tiempo en funcionamiento', + 189: 'Temperatura', + 190: 'Memoria', + 191: 'Almacenamiento', + 192: 'Capacidad', + 193: 'Fallo en la sincronización del reloj', + 194: 'la documentación', + 195: 'Días', + 196: 'Horas', + 197: 'Minutos', + 198: 'Segundos', + 199: 'Ver completo', + 200: 'Ver informe', + 201: 'Acción en lote', + 202: 'Marcar como visto', + 203: 'Marcar como no visto', + 204: 'Fecha', + 205: 'Título', + 206: 'Servicio', + 207: 'Mensaje', + 208: 'Sin notificaciones', + 209: 'Requerido', + 210: 'Opcional', + 211: 'No se proporcionó motivo', + 212: 'Tareas', + 213: 'Tipo', + 214: 'Descripción', + 215: 'Todas las tareas completas', + 216: 'Iniciar', + 217: 'Detener', + 218: 'Dependencias', + 219: 'Satisfechas', + 220: 'Sin dependencias', + 221: 'No instalado', + 222: 'Versión incorrecta', + 223: 'No en ejecución', + 224: 'Acción requerida', + 225: 'La verificación de salud requerida no está pasando', + 226: 'Una dependencia tiene un problema de dependencia', + 227: 'Error desconocido', + 228: 'Error', + 229: '"Reconstruir contenedor" es una acción inofensiva que solo toma unos segundos. Probablemente resolverá este problema.', + 230: '"Desinstalar servicio" es una acción peligrosa que eliminará el servicio de StartOS y borrará todos sus datos.', + 231: 'Reconstruir contenedor', + 232: 'Desinstalar servicio', + 233: 'Ver mensaje completo', + 234: 'Error del servicio', + 235: 'Esperando resultado', + 236: 'Iniciando', + 237: 'Éxito', + 238: 'Verificaciones de salud', + 239: 'Sin verificaciones de salud', + 240: 'Nombre', + 241: 'Estado', + 242: 'Abrir', + 243: 'Interfaces', + 244: 'Alojamiento', + 245: 'Instalando', + 246: 'Ver abajo', + 247: 'Controles', + 248: 'No hay servicios instalados', + 249: 'En ejecución', + 250: 'Detenido', + 251: 'Tarea requerida', + 252: 'Actualizando', + 253: 'Deteniendo', + 254: 'Confiar en tu CA raíz', + 255: 'Haciendo copia de seguridad', + 256: 'Reiniciando', + 257: 'Volver', + 258: 'Restaurando', + 259: 'Desconocido', + 260: 'Mostrar/Ocultar', + 261: 'Mostrar', + 262: 'Escanea este QR', + 263: 'Restablecer valores predeterminados', + 264: 'Como resultado de este cambio, los siguientes servicios dejarán de funcionar correctamente y pueden fallar', + 265: 'Error al iniciar el servicio', + 266: 'Problema', + 267: 'Fallo', + 268: 'Saludable', + 269: 'finalizando', + 270: '% desconocido', + 271: 'No proporcionado', + 272: 'Enlaces', + 273: 'Hash de Git', + 274: 'Licencia', + 275: 'Instalado desde', + 276: 'Repositorio del servicio', + 277: 'Repositorio del paquete', + 278: 'Sitio de marketing', + 279: 'Sitio de soporte', + 280: 'Enlace de donación', + 281: 'Acciones estándar', + 282: 'Reconstruir servicio', + 283: 'Reconstruye el contenedor del servicio. Solo es necesario si hay un error en StartOS', + 284: 'Desinstalar', + 285: 'Desinstala este servicio de StartOS y elimina todos los datos permanentemente.', + 286: 'Panel de control', + 287: 'panel de control', + 288: 'acciones', + 289: 'instrucciones', + 290: 'registros', + 291: 'acerca de', + 292: 'Iniciando carga', + 293: 'Intentar de nuevo', + 294: 'Subir archivo de paquete .s9pk', + 295: 'Advertencia: la carga del paquete será lenta a través de Tor. Cambia a conexión local para una mejor experiencia.', + 296: 'Subir', + 297: 'Se detectó un paquete s9pk de versión 1. Este formato está obsoleto. Puedes instalarlo manualmente con start-cli si es necesario.', + 298: 'Archivo de paquete inválido', + 299: 'Agrega proveedores ACME para generar certificados SSL (https) para el acceso desde clearnet.', + 300: 'Ver instrucciones', + 301: 'Proveedores guardados', + 302: 'Agregar proveedor', + 303: 'Contacto', + 304: 'Editar', + 305: 'Agregar proveedor ACME', + 306: 'Editar proveedor ACME', + 307: 'Correos de contacto', + 308: 'Necesarios para obtener un certificado de una Autoridad Certificadora', + 309: 'Alternar todo', + 310: 'Hecho', + 311: 'Se necesita la contraseña maestra', + 312: 'Ingresa tu contraseña maestra para cifrar esta copia de seguridad.', + 313: 'Contraseña maestra', + 314: 'Ingresa la contraseña maestra', + 315: 'Se necesita la contraseña original', + 316: 'Esta copia de seguridad fue creada con una contraseña diferente. Ingresa la contraseña original que se usó para cifrar esta copia.', + 317: 'Contraseña original', + 318: 'Ingresa la contraseña original', + 319: 'Iniciando copia de seguridad', + 320: 'Haz una copia de seguridad de StartOS y los datos de los servicios conectándote a un dispositivo en tu red local o a una unidad física conectada a tu servidor.', + 321: 'Restaura StartOS y los datos de los servicios desde un dispositivo en tu red local o una unidad física conectada a tu servidor que contenga una copia de seguridad existente.', + 322: 'Última copia de seguridad', + 323: 'Una carpeta en otro ordenador conectado a la misma red que tu servidor Start9.', + 324: 'Una unidad física conectada directamente a tu servidor Start9.', + 325: 'Seleccionar servicios para respaldar', + 326: 'Seleccionar copia de seguridad del servidor', + 327: 'Carpetas de red', + 328: 'Abrir nuevo', + 329: 'Nombre del host', + 330: 'Ruta', + 331: 'URL', + 332: 'Interfaz de red', + 333: 'Protocolo', + 334: 'Modelo', + 335: 'Agente de usuario', + 336: 'Plataforma', + 337: 'Última actividad', + 338: 'Creado en', + 339: 'Algoritmo', + 340: 'Huella digital', + 341: 'Hash del paquete', + 342: 'Publicado', + 343: 'Nueva carpeta de red', + 344: 'Actualizar carpeta de red', + 345: 'Probando la conectividad a la carpeta compartida', + 346: 'Asegúrate de que (1) el equipo de destino esté conectado a la misma LAN que tu servidor Start9, (2) la carpeta de destino esté compartida, y (3) el nombre del host, la ruta y las credenciales sean correctas.', + 347: 'No se pudo conectar', + 348: 'La carpeta de red no contiene una copia de seguridad válida', + 349: 'Conectar', + 350: 'Nombre de usuario', + 351: 'Contraseña', + 352: 'El nombre del host de tu dispositivo de destino en la red local.', + 353: 'En Windows, esta es la ruta completa a la carpeta compartida (por ejemplo, /Desktop/mi-carpeta). En Linux y Mac, es el nombre literal de la carpeta compartida (por ejemplo, mi-carpeta-compartida).', + 354: 'En Linux, este es el nombre de usuario de samba que creaste al compartir la carpeta. En Mac y Windows, es el nombre de usuario del usuario que comparte la carpeta.', + 355: 'En Linux, esta es la contraseña de samba que creaste al compartir la carpeta. En Mac y Windows, es la contraseña del usuario que comparte la carpeta.', + 356: 'Unidades físicas', + 357: 'No se detectaron unidades', + 358: 'Actualizar', + 359: 'La partición de la unidad no contiene una copia de seguridad válida', + 360: 'Progreso de la copia de seguridad', + 361: 'Completo', + 362: 'Haciendo copia de seguridad', + 363: 'Esperando', + 364: 'Copia de seguridad realizada', + 365: 'Restauración seleccionada', + 366: 'Inicializando', + 367: 'No disponible. La copia de seguridad se hizo en una versión más reciente de StartOS.', + 368: 'No disponible. El servicio ya está instalado.', + 369: 'Listo para restaurar', + 370: 'Nombre de host local', + 371: 'Creado', + 372: 'Se requiere contraseña', + 373: 'Ingresa la contraseña maestra que se usó para cifrar esta copia de seguridad. En la siguiente pantalla, podrás seleccionar los servicios individuales que deseas restaurar.', + 374: 'Descifrando unidad', + 375: 'Seleccionar servicios para restaurar', + 376: 'Disponible para copia de seguridad', + 377: 'Copias de seguridad de StartOS detectadas', + 378: 'No se detectaron copias de seguridad de StartOS', + 379: 'Versión de StartOS', + 380: 'Conectar un servidor SMTP externo permite que StartOS y tus servicios instalados te envíen correos electrónicos.', + 381: 'Credenciales SMTP', + 382: 'Enviar correo de prueba', + 383: 'Enviar', + 384: 'Enviando correo', + 385: 'Se ha enviado un correo de prueba a', + 386: 'Revisa tu carpeta de spam y márcalo como no spam.', + 387: 'La interfaz web de tu servidor StartOS, accesible desde cualquier navegador.', + 388: 'Cambia tu contraseña maestra de StartOS.', + 389: '¡Aún necesitarás tu contraseña actual para descifrar copias de seguridad existentes!', + 390: 'Las nuevas contraseñas no coinciden', + 391: 'La nueva contraseña debe tener al menos 12 caracteres', + 392: 'La nueva contraseña debe tener menos de 65 caracteres', + 393: 'La contraseña actual no es válida', + 394: 'Contraseña cambiada', + 395: 'Contraseña actual', + 396: 'Nueva contraseña', + 397: 'Reingresa nueva contraseña', + 398: 'Una sesión es un dispositivo que actualmente ha iniciado sesión en StartOS. Para mayor seguridad, cierra las sesiones que no reconozcas o que ya no uses.', + 399: 'Sesión actual', + 400: 'Otras sesiones', + 401: 'Terminar seleccionados', + 402: 'Terminando sesiones', + 403: 'Sin sesiones', + 404: 'Se requiere contraseña', + 405: 'Conectado', + 406: 'Olvidar', + 407: 'Credenciales WiFi', + 408: 'Obsoleto', + 409: 'El soporte para WiFi será eliminado en StartOS v0.4.1. Si no tienes acceso a Ethernet, puedes usar un extensor WiFi para conectarte a la red local y luego conectar tu servidor al extensor por Ethernet. Por favor, contacta al soporte de Start9 si tienes dudas o inquietudes.', + 410: 'Redes conocidas', + 411: 'Otras redes', + 412: 'WiFi está deshabilitado', + 413: 'No se detectó interfaz inalámbrica', + 414: 'Habilitando WiFi', + 415: 'Deshabilitando WiFi', + 416: 'Conectando. Esto podría tardar un poco', + 417: 'Reintentar', + 418: 'Mostrar más', + 419: 'Notas de la versión', + 420: 'Ver listado', + 421: 'Servicios que dependen de', + 422: 'ya no funcionarán correctamente y podrían fallar.', + 423: 'Solicitud fallida', + 424: '¡Todos los servicios están actualizados!', + 425: 'Ejecutar', + 426: 'La acción solo se puede ejecutar cuando el servicio está', + 427: 'Prohibido', + 428: 'puede experimentar problemas temporales', + 429: 'tiene dependencias no satisfechas. No funcionará como se espera.', + 430: 'Reconstruyendo contenedor', + 431: 'Iniciando desinstalación', + 432: 'eliminará permanentemente sus datos.', + 433: 'Desinstalando', + 434: 'Intentando alcanzar el servidor', + 435: 'Conexión restablecida', + 436: 'Estado desconocido', + 437: 'Servidor conectado', + 438: 'Sin Internet', + 439: 'Conectando', + 440: 'Apagando', + 441: 'Versiones', + 442: 'Nuevas notificaciones', + 443: 'Ver', + 444: 'Recargando PWA', + 445: 'Completado', + 446: 'Datos del sistema', + 447: 'No intentado', + 448: 'Fallido', + 449: 'Exitoso', + 450: 'Reinicia tu servidor para que estas actualizaciones surtan efecto. Puede tardar varios minutos en volver a estar en línea.', + 451: 'Descarga de StartOS completa', + 452: 'Unidad de almacenamiento desconocida detectada', + 453: 'Para usar una unidad de almacenamiento diferente, reemplaza la actual y haz clic en REINICIAR SERVIDOR abajo. Para usar la unidad actual, haz clic en USAR UNIDAD ACTUAL abajo y sigue las instrucciones. No se borrarán datos durante este proceso.', + 454: 'Unidad de almacenamiento no encontrada', + 455: 'Inserta tu unidad de almacenamiento de StartOS y haz clic en REINICIAR SERVIDOR abajo.', + 456: 'Unidad de almacenamiento dañada. Esto podría deberse a corrupción de datos o daño físico.', + 457: 'Puede o no ser posible reutilizar esta unidad reformateándola y recuperando desde una copia de seguridad. Para entrar en modo de recuperación, haz clic en ENTRAR EN MODO DE RECUPERACIÓN abajo y sigue las instrucciones. No se borrarán datos durante este paso.', + 458: 'Error del sistema de archivos', + 459: 'Reparar el disco podría ayudar a resolver este problema. Por favor, NO desconectes la unidad ni el servidor durante este tiempo o la situación empeorará.', + 460: 'Error de gestión de disco', + 461: 'Por favor, contacta con soporte', + 462: 'Modo de diagnóstico', + 463: 'error de inicio', + 464: 'Ver registros', + 465: 'Posibles soluciones', + 466: 'Configurar unidad actual', + 467: 'Entrar en modo de recuperación', + 468: 'El servidor se está reiniciando', + 469: 'Espera a que el servidor se reinicie y luego actualiza esta página.', + 470: 'Reiniciar servidor', + 471: 'Reparar unidad', + 472: 'Configurando tu servidor', + 473: 'Progreso', + 474: 'Iniciar sesión en StartOS', + 475: 'Iniciar sesión', + 476: 'Iniciando sesión', + 477: 'La contraseña debe tener menos de 65 caracteres', + 478: 'Contraseña inválida', + 479: 'Descarga y confía en tu Autoridad Certificadora raíz para establecer una conexión segura (HTTPS). Tendrás que repetir esto en cada dispositivo que uses para conectarte a tu servidor.', + 480: 'Guarda esta página para poder acceder más tarde. También puedes encontrar esta dirección en el archivo descargado al final de la configuración inicial.', + 481: 'Has confiado exitosamente en tu CA raíz y ahora puedes iniciar sesión de forma segura.', + 482: 'Tu servidor usa su CA raíz para generar certificados SSL/TLS para sí mismo y los servicios instalados. Estos certificados se utilizan para cifrar el tráfico de red con tus dispositivos cliente.', + 483: 'Sigue las instrucciones para tu sistema operativo. Al confiar en tu CA raíz, tu dispositivo puede verificar la autenticidad de las comunicaciones cifradas con tu servidor.', + 484: 'Actualiza la página. Si actualizar no funciona, puede que necesites cerrar y volver a abrir tu navegador, y luego volver a esta página.', + 485: 'Interfaz de StartOS', + 486: 'WiFi', +} as any satisfies i18n diff --git a/web/projects/shared/src/i18n/i18n.pipe.ts b/web/projects/shared/src/i18n/i18n.pipe.ts new file mode 100644 index 000000000..9313e038b --- /dev/null +++ b/web/projects/shared/src/i18n/i18n.pipe.ts @@ -0,0 +1,19 @@ +import { inject, Injectable, Pipe, PipeTransform } from '@angular/core' +import { ENGLISH } from './dictionaries/english' +import { I18N, i18nKey } from './i18n.providers' + +@Pipe({ + standalone: true, + name: 'i18n', + pure: false, +}) +@Injectable({ providedIn: 'root' }) +export class i18nPipe implements PipeTransform { + private readonly i18n = inject(I18N) + + transform(englishKey: i18nKey | null | undefined): string | undefined { + return englishKey + ? this.i18n()?.[ENGLISH[englishKey]] || englishKey + : undefined + } +} diff --git a/web/projects/ui/src/app/i18n/i18n.providers.ts b/web/projects/shared/src/i18n/i18n.providers.ts similarity index 59% rename from web/projects/ui/src/app/i18n/i18n.providers.ts rename to web/projects/shared/src/i18n/i18n.providers.ts index 8239f6edd..83ee2a741 100644 --- a/web/projects/ui/src/app/i18n/i18n.providers.ts +++ b/web/projects/shared/src/i18n/i18n.providers.ts @@ -5,20 +5,28 @@ import { tuiLanguageSwitcher, TuiLanguageSwitcherService, } from '@taiga-ui/i18n' -import ENGLISH from './dictionaries/english' +import { ENGLISH } from './dictionaries/english' import { i18nService } from './i18n.service' -export type i18n = typeof ENGLISH +export type i18nKey = keyof typeof ENGLISH +export type i18n = Record<(typeof ENGLISH)[i18nKey], string> -export const I18N = tuiCreateToken(signal(ENGLISH)) +export const I18N = tuiCreateToken(signal(null)) export const I18N_LOADER = tuiCreateToken<(lang: TuiLanguageName) => Promise>() +export const I18N_STORAGE = tuiCreateToken< + (lang: TuiLanguageName) => Promise +>(() => Promise.resolve()) export const I18N_PROVIDERS = [ tuiLanguageSwitcher(async (language: TuiLanguageName): Promise => { switch (language) { case 'spanish': return import('@taiga-ui/i18n/languages/spanish') + case 'polish': + return import('@taiga-ui/i18n/languages/polish') + case 'german': + return import('@taiga-ui/i18n/languages/german') default: return import('@taiga-ui/i18n/languages/english') } @@ -29,8 +37,12 @@ export const I18N_PROVIDERS = [ switch (language) { case 'spanish': return import('./dictionaries/spanish').then(v => v.default) + case 'polish': + return import('./dictionaries/polish').then(v => v.default) + case 'german': + return import('./dictionaries/german').then(v => v.default) default: - return import('./dictionaries/english').then(v => v.default) + return null } }, }, diff --git a/web/projects/ui/src/app/i18n/i18n.service.ts b/web/projects/shared/src/i18n/i18n.service.ts similarity index 71% rename from web/projects/ui/src/app/i18n/i18n.service.ts rename to web/projects/shared/src/i18n/i18n.service.ts index 85f7d3a83..4b791f71d 100644 --- a/web/projects/ui/src/app/i18n/i18n.service.ts +++ b/web/projects/shared/src/i18n/i18n.service.ts @@ -1,7 +1,6 @@ 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' +import { I18N, I18N_LOADER, I18N_STORAGE } from './i18n.providers' @Injectable({ providedIn: 'root', @@ -9,7 +8,7 @@ import { ApiService } from '../services/api/embassy-api.service' export class i18nService extends TuiLanguageSwitcherService { private readonly i18n = inject(I18N) private readonly i18nLoader = inject(I18N_LOADER) - private readonly api = inject(ApiService) + private readonly store = inject(I18N_STORAGE) readonly loading = signal(false) @@ -20,7 +19,7 @@ export class i18nService extends TuiLanguageSwitcherService { super.setLanguage(language) this.loading.set(true) - this.api.setDbValue(['language'], language).then(() => + this.store(language).then(() => this.i18nLoader(language).then(value => { this.i18n.set(value) this.loading.set(false) @@ -28,3 +27,6 @@ export class i18nService extends TuiLanguageSwitcherService { ) } } + +export const languages = ['english', 'spanish', 'polish', 'german'] as const +export type Languages = (typeof languages)[number] diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index dfc2e7e3d..1bbbaee14 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -5,20 +5,21 @@ export * from './classes/http-error' export * from './classes/rpc-error' -export * from './components/initializing/logs-window.component' -export * from './components/initializing/initializing.component' -export * from './components/loading/loading.component' -export * from './components/loading/loading.component' -export * from './components/loading/loading.service' -export * from './components/ticker/ticker.component' -export * from './components/ticker/ticker.module' +export * from './components/logs-window.component' +export * from './components/initializing.component' +export * from './components/ticker.component' export * from './components/drive.component' export * from './components/markdown.component' +export * from './components/prompt.component' export * from './components/server.component' export * from './directives/drag-scroller.directive' export * from './directives/safe-links.directive' +export * from './i18n/i18n.pipe' +export * from './i18n/i18n.providers' +export * from './i18n/i18n.service' + export * from './pipes/exver/exver.module' export * from './pipes/exver/exver.pipe' export * from './pipes/shared/shared.module' @@ -30,10 +31,12 @@ export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './pipes/markdown.pipe' export * from './services/copy.service' +export * from './services/dialog.service' export * from './services/download-html.service' export * from './services/exver.service' export * from './services/error.service' export * from './services/http.service' +export * from './services/loading.service' export * from './services/setup-logs.service' export * from './types/api' diff --git a/web/projects/shared/src/services/dialog.service.ts b/web/projects/shared/src/services/dialog.service.ts new file mode 100644 index 000000000..9a542f87f --- /dev/null +++ b/web/projects/shared/src/services/dialog.service.ts @@ -0,0 +1,93 @@ +import { inject, Injectable, TemplateRef } from '@angular/core' +import { + TuiResponsiveDialogOptions, + TuiResponsiveDialogService, +} from '@taiga-ui/addon-mobile' +import { TuiAlertOptions } from '@taiga-ui/core' +import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' +import { PROMPT, PromptOptions } from '../components/prompt.component' +import { i18nPipe } from '../i18n/i18n.pipe' +import { i18nKey } from '../i18n/i18n.providers' + +@Injectable({ + providedIn: 'root', +}) +export class DialogService { + private readonly dialogs = inject(TuiResponsiveDialogService) + private readonly i18n = inject(i18nPipe) + + openPrompt( + options: Partial> & { + label: i18nKey + data: PromptOptions + }, + ) { + const { message, label, placeholder, buttonText } = options.data + + return this.dialogs.open(PROMPT, { + label: this.i18n.transform(options.label), + data: { + ...options.data, + message: this.i18n.transform(message), + label: this.i18n.transform(label), + placeholder: this.i18n.transform(placeholder), + buttonText: this.i18n.transform(buttonText), + }, + }) + } + + openConfirm( + options: Partial> & { + label: i18nKey + data?: TuiConfirmData & { + content?: PolymorpheusComponent | i18nKey + yes?: i18nKey + no?: i18nKey + } + }, + ) { + options.data = options.data || {} + const { content, yes, no } = options.data + + return this.dialogs.open(TUI_CONFIRM, { + label: this.i18n.transform(options.label), + data: { + ...options.data, + content: isI18n(content) ? this.i18n.transform(content) : content, + yes: this.i18n.transform(yes), + no: this.i18n.transform(no), + }, + }) + } + + openAlert( + message: i18nKey | undefined, + options: Partial> & { + label?: i18nKey + } = {}, + ) { + return this.dialogs.open(this.i18n.transform(message), { + ...options, + label: this.i18n.transform(options.label), + }) + } + + openComponent( + component: PolymorpheusComponent | TemplateRef, + options: Partial> & { + label?: i18nKey + } = {}, + ) { + return this.dialogs.open(component, { + ...options, + label: this.i18n.transform(options.label), + }) + } +} + +function isI18n( + content: PolymorpheusComponent | i18nKey | undefined, +): content is i18nKey { + return typeof content === 'string' +} diff --git a/web/projects/shared/src/services/loading.service.ts b/web/projects/shared/src/services/loading.service.ts new file mode 100644 index 000000000..605fc8134 --- /dev/null +++ b/web/projects/shared/src/services/loading.service.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component, Injectable } from '@angular/core' +import { TuiPopoverService } from '@taiga-ui/cdk' +import { TUI_DIALOGS, TuiLoader } from '@taiga-ui/core' +import { injectContext } from '@taiga-ui/polymorpheus' +import { i18nPipe } from '../i18n/i18n.pipe' +import { i18nKey } from '../i18n/i18n.providers' + +@Component({ + standalone: true, + template: '', + styles: ` + :host { + display: flex; + align-items: center; + max-width: 80%; + margin: auto; + padding: 1.5rem; + background: var(--tui-background-elevation-1); + border-radius: var(--tui-radius-m); + box-shadow: var(--tui-shadow-popup); + + --tui-background-accent-1: var(--tui-status-warning); + } + + tui-loader { + flex-shrink: 0; + min-width: 2rem; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TuiLoader, i18nPipe], +}) +class LoadingComponent { + readonly content = injectContext<{ content: i18nKey }>().content +} + +@Injectable({ + providedIn: `root`, + useFactory: () => new LoadingService(TUI_DIALOGS, LoadingComponent), +}) +export class LoadingService extends TuiPopoverService { + override open(textContent: i18nKey) { + return super.open(textContent) + } +} diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 24d7c04c9..5daf01308 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -1,9 +1,9 @@ import { Component, inject, OnInit } from '@angular/core' import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { Title } from '@angular/platform-browser' +import { i18nService } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { combineLatest, map, merge, startWith } from 'rxjs' -import { i18nService } from 'src/app/i18n/i18n.service' import { ConnectionService } from './services/connection.service' import { PatchDataService } from './services/patch-data.service' import { DataModel } from './services/patch-db/data-model' diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index e73746cb4..fa5baa8b5 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -5,7 +5,13 @@ import { AbstractCategoryService, FilterPackagesPipe, } from '@start9labs/marketplace' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' +import { + I18N_PROVIDERS, + I18N_STORAGE, + i18nService, + RELATIVE_URL, + WorkspaceConfig, +} from '@start9labs/shared' import { TUI_DATE_FORMAT, TUI_DIALOGS_CLOSE, @@ -21,8 +27,6 @@ import { import { tuiTextfieldOptionsProvider } from '@taiga-ui/legacy' import { PatchDB } from 'patch-db-client' import { filter, of, pairwise } from 'rxjs' -import { I18N_PROVIDERS } from 'src/app/i18n/i18n.providers' -import { i18nService } from 'src/app/i18n/i18n.service' import { PATCH_CACHE, PatchDbSource, @@ -100,6 +104,14 @@ export const APP_PROVIDERS: Provider[] = [ ), ), }, + { + provide: I18N_STORAGE, + useFactory: () => { + const api = inject(ApiService) + + return (language: string) => api.setDbValue(['language'], language) + }, + }, ] export function appInitializer(): () => void { diff --git a/web/projects/ui/src/app/components/backup-report.component.ts b/web/projects/ui/src/app/components/backup-report.component.ts new file mode 100644 index 000000000..b10b0bf3a --- /dev/null +++ b/web/projects/ui/src/app/components/backup-report.component.ts @@ -0,0 +1,95 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + computed, + inject, +} from '@angular/core' +import { i18nKey, i18nPipe } from '@start9labs/shared' +import { TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core' +import { TuiCell } from '@taiga-ui/layout' +import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' +import { BackupReport } from 'src/app/services/api/api.types' + +@Component({ + template: ` +

+ {{ 'Completed' | i18n }}: {{ data.createdAt | date: 'medium' }} +

+
+
+ {{ 'System data' | i18n }} +
+ {{ system().result | i18n }} +
+
+ +
+ @for (pkg of data.content.packages | keyvalue; track $index) { +
+
+ {{ pkg.key }} +
+ {{ + pkg.value.error + ? ('Failed' | i18n) + ': ' + pkg.value.error + : ('Succeeded' | i18n) + }} +
+
+ +
+ } + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule, TuiIcon, TuiCell, TuiTitle, i18nPipe], +}) +export class BackupsReportModal { + private readonly i18n = inject(i18nPipe) + + readonly data = + injectContext< + TuiDialogContext + >().data + + readonly system = computed( + (): { result: i18nKey; icon: string; color: string } => { + if (!this.data.content.server.attempted) { + return { + result: 'Not attempted', + icon: '@tui.minus', + color: 'var(--tui-text-secondary)', + } + } + + if (this.data.content.server.error) { + return { + result: + `${this.i18n.transform('Failed')}: ${this.data.content.server.error}` as i18nKey, + icon: '@tui.circle-minus', + color: 'var(--tui-text-negative)', + } + } + + return { + result: 'Succeeded', + icon: '@tui.check', + color: 'var(--tui-text-positive)', + } + }, + ) + + getColor(error: unknown) { + return error ? 'var(--tui-text-negative)' : 'var(--tui-text-positive)' + } + + getIcon(error: unknown) { + return error ? '@tui.circle-minus' : '@tui.check' + } +} + +export const REPORT = new PolymorpheusComponent(BackupsReportModal) diff --git a/web/projects/ui/src/app/components/notifications-toast.component.ts b/web/projects/ui/src/app/components/notifications-toast.component.ts index 8532ba196..c15be9e7e 100644 --- a/web/projects/ui/src/app/components/notifications-toast.component.ts +++ b/web/projects/ui/src/app/components/notifications-toast.component.ts @@ -1,6 +1,7 @@ import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' +import { i18nPipe } from '@start9labs/shared' import { TuiAlert } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { endWith, map, merge, Observable, pairwise, Subject } from 'rxjs' @@ -15,12 +16,14 @@ import { DataModel } from 'src/app/services/patch-db/data-model' [tuiAlertOptions]="{ label: 'StartOS' }" (tuiAlertChange)="onDismiss()" > - New notifications - View + {{ 'New notifications' | i18n }} + + {{ 'View' | i18n }} + `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiAlert, RouterLink, AsyncPipe], + imports: [TuiAlert, RouterLink, AsyncPipe, i18nPipe], }) export class NotificationsToastComponent { private readonly dismiss$ = new Subject() diff --git a/web/projects/ui/src/app/components/refresh-alert.component.ts b/web/projects/ui/src/app/components/refresh-alert.component.ts index 1624214f7..1d97ba15e 100644 --- a/web/projects/ui/src/app/components/refresh-alert.component.ts +++ b/web/projects/ui/src/app/components/refresh-alert.component.ts @@ -70,7 +70,7 @@ export class RefreshAlertComponent { } async pwaReload() { - const loader = this.loader.open('Reloading PWA...').subscribe() + const loader = this.loader.open('Reloading PWA').subscribe() try { // attempt to update to the latest client version available diff --git a/web/projects/ui/src/app/components/report.component.ts b/web/projects/ui/src/app/components/report.component.ts deleted file mode 100644 index e28c37aa2..000000000 --- a/web/projects/ui/src/app/components/report.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { TuiDialogContext, TuiIcon, TuiTitle } from '@taiga-ui/core' -import { TuiCell } from '@taiga-ui/layout' -import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' -import { BackupReport } from 'src/app/services/api/api.types' - -@Component({ - template: ` -

Completed: {{ data.createdAt | date: 'medium' }}

-
-
- System data -
{{ system.result }}
-
- -
- @for (pkg of data.content.packages | keyvalue; track $index) { -
-
- {{ pkg.key }} -
- {{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }} -
-
- -
- } - `, - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [CommonModule, TuiIcon, TuiCell, TuiTitle], -}) -export class BackupsReportModal { - readonly data = - injectContext< - TuiDialogContext - >().data - - readonly system = this.getSystem() - - getColor(error: unknown) { - return error ? 'var(--tui-text-negative)' : 'var(--tui-text-positive)' - } - - getIcon(error: unknown) { - return error ? '@tui.circle-minus' : '@tui.check' - } - - private getSystem() { - if (!this.data.content.server.attempted) { - return { - result: 'Not Attempted', - icon: '@tui.minus', - color: 'var(--tui-text-secondary)', - } - } - - if (this.data.content.server.error) { - return { - result: `Failed: ${this.data.content.server.error}`, - icon: '@tui.circle-minus', - color: 'var(--tui-text-negative)', - } - } - - return { - result: 'Succeeded', - icon: '@tui.check', - color: 'var(--tui-text-positive)', - } - } -} - -export const REPORT = new PolymorpheusComponent(BackupsReportModal) diff --git a/web/projects/ui/src/app/components/update-toast.component.ts b/web/projects/ui/src/app/components/update-toast.component.ts index f2cdfb41d..a58a47cfb 100644 --- a/web/projects/ui/src/app/components/update-toast.component.ts +++ b/web/projects/ui/src/app/components/update-toast.component.ts @@ -1,6 +1,6 @@ import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared' import { TuiAlert, TuiButton } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { @@ -21,14 +21,16 @@ import { DataModel } from 'src/app/services/patch-db/data-model' - Restart your server for these updates to take effect. It can take several - minutes to come back online. + {{ + 'Restart your server for these updates to take effect. It can take several minutes to come back online.' + | i18n + }}
`, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiButton, TuiAlert, AsyncPipe], + imports: [TuiButton, TuiAlert, AsyncPipe, i18nPipe], }) export class UpdateToastComponent { private readonly api = inject(ApiService) @@ -65,7 +67,7 @@ export class UpdateToastComponent { async restart(): Promise { this.onDismiss() - const loader = this.loader.open('Restarting...').subscribe() + const loader = this.loader.open('Restarting').subscribe() try { await this.api.restartServer({}) diff --git a/web/projects/ui/src/app/i18n/dictionaries/english.ts b/web/projects/ui/src/app/i18n/dictionaries/english.ts deleted file mode 100644 index 04ebe9fac..000000000 --- a/web/projects/ui/src/app/i18n/dictionaries/english.ts +++ /dev/null @@ -1,49 +0,0 @@ -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: 'StartOS UI', - acme: 'ACME', - wifi: 'WiFi', - sessions: 'Active Sessions', - password: 'Change Password', - }, - general: { - title: 'General Settings', - subtitle: 'Manage your overall setup and preferences', - tab: 'Browser Tab Title', - language: 'Language', - repair: { - title: 'Disk Repair', - subtitle: 'Attempt automatic repair', - button: 'Repair', - }, - ca: { - title: 'Root Certificate Authority', - subtitle: `Download your server's Root CA`, - button: 'Download', - }, - tor: { - title: 'Reset Tor', - subtitle: 'Restart the Tor daemon on your server', - }, - update: { - title: 'Software Update', - button: { - restart: 'Restart to apply', - check: 'Check for updates', - }, - }, - }, - }, -} diff --git a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts b/web/projects/ui/src/app/i18n/dictionaries/spanish.ts deleted file mode 100644 index a92abf211..000000000 --- a/web/projects/ui/src/app/i18n/dictionaries/spanish.ts +++ /dev/null @@ -1,51 +0,0 @@ -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', - password: 'Cambiar Contraseña', - }, - general: { - title: 'Configuración General', - subtitle: 'Gestiona tu configuración general y preferencias', - tab: 'Título de la Pestaña del Navegador', - language: 'Idioma', - repair: { - title: 'Reparación de Disco', - subtitle: 'Intentar reparación automática', - button: 'Reparar', - }, - ca: { - title: 'Autoridad de Certificación Raíz', - subtitle: 'Descarga la autoridad certificadora raíz de tu servidor', - button: 'Descarga', - }, - tor: { - title: 'Reiniciar Tor', - subtitle: 'Reiniciar el daemon de Tor en tu servidor', - }, - update: { - title: 'Actualización de Software', - button: { - restart: 'Reiniciar para aplicar', - check: 'Buscar actualizaciones', - }, - }, - }, - }, -} satisfies i18n diff --git a/web/projects/ui/src/app/i18n/i18n.pipe.ts b/web/projects/ui/src/app/i18n/i18n.pipe.ts deleted file mode 100644 index bc8c7f03f..000000000 --- a/web/projects/ui/src/app/i18n/i18n.pipe.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { inject, Pipe, PipeTransform } from '@angular/core' -import { I18N, i18n } from './i18n.providers' - -type DeepKeyOf = { - [K in keyof T & string]: T[K] extends {} - ? T[K] extends string - ? K - : `${K}.${DeepKeyOf}` - : never -}[keyof T & string] - -@Pipe({ - standalone: true, - name: 'i18n', - pure: false, -}) -export class i18nPipe implements PipeTransform { - private readonly i18n = inject(I18N) - - transform(path: DeepKeyOf): string { - return path.split('.').reduce((acc, part) => acc[part], this.i18n() as any) - } -} diff --git a/web/projects/ui/src/app/routes/diagnostic/home/home.module.ts b/web/projects/ui/src/app/routes/diagnostic/home/home.module.ts index 10c8d2a8e..b4419cc2f 100644 --- a/web/projects/ui/src/app/routes/diagnostic/home/home.module.ts +++ b/web/projects/ui/src/app/routes/diagnostic/home/home.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { RouterModule, Routes } from '@angular/router' import { HomePage } from './home.page' +import { i18nPipe } from '@start9labs/shared' const ROUTES: Routes = [ { @@ -12,7 +13,7 @@ const ROUTES: Routes = [ ] @NgModule({ - imports: [CommonModule, TuiButton, RouterModule.forChild(ROUTES)], + imports: [CommonModule, TuiButton, RouterModule.forChild(ROUTES), i18nPipe], declarations: [HomePage], }) export class HomePageModule {} diff --git a/web/projects/ui/src/app/routes/diagnostic/home/home.page.html b/web/projects/ui/src/app/routes/diagnostic/home/home.page.html index c4fc493c0..5adc52c62 100644 --- a/web/projects/ui/src/app/routes/diagnostic/home/home.page.html +++ b/web/projects/ui/src/app/routes/diagnostic/home/home.page.html @@ -1,20 +1,20 @@ -

StartOS - Diagnostic Mode

+

StartOS - {{ 'Diagnostic Mode' | i18n }}

-

StartOS launch error:

+

StartOS {{ 'launch error' | i18n }}:

{{ error.problem }}

{{ error.details }}

- View Logs + {{ 'View logs' | i18n }} -

Possible solutions:

+

{{ 'Possible solutions' | i18n }}:

{{ error.solution }}

- +
-

Server is restarting

+

{{ 'Server is restarting' | i18n }}

- Wait for the server to restart, then refresh this page. + {{ 'Wait for the server to restart, then refresh this page.' | i18n }}

- +
diff --git a/web/projects/ui/src/app/routes/diagnostic/home/home.page.ts b/web/projects/ui/src/app/routes/diagnostic/home/home.page.ts index 0eae7e98b..13aae5680 100644 --- a/web/projects/ui/src/app/routes/diagnostic/home/home.page.ts +++ b/web/projects/ui/src/app/routes/diagnostic/home/home.page.ts @@ -1,12 +1,11 @@ -import { TUI_CONFIRM } from '@taiga-ui/kit' import { Component, Inject } from '@angular/core' import { WA_WINDOW } from '@ng-web-apis/common' -import { LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' +import { DialogService, i18nKey, LoadingService } from '@start9labs/shared' import { filter } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' +// @TODO Alex how to use i18nPipe in this component since not standalone? @Component({ selector: 'diagnostic-home', templateUrl: 'home.page.html', @@ -16,15 +15,15 @@ export class HomePage { restarted = false error?: { code: number - problem: string - solution: string + problem: i18nKey + solution: i18nKey details?: string } constructor( private readonly loader: LoadingService, private readonly api: ApiService, - private readonly dialogs: TuiDialogService, + private readonly dialog: DialogService, @Inject(WA_WINDOW) private readonly window: Window, readonly config: ConfigService, ) {} @@ -64,7 +63,7 @@ export class HomePage { } else if (error.code === 2) { this.error = { code: 2, - problem: 'Filesystem I/O error.', + problem: 'Filesystem error', solution: 'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.', details: error.data?.details, @@ -73,7 +72,7 @@ export class HomePage { } else if (error.code === 48) { this.error = { code: 48, - problem: 'Disk management error.', + problem: 'Disk management error', solution: 'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.', details: error.data?.details, @@ -81,8 +80,8 @@ export class HomePage { } else { this.error = { code: error.code, - problem: error.message, - solution: 'Please contact support.', + problem: error.message as i18nKey, + solution: 'Please contact support', details: error.data?.details, } } @@ -92,7 +91,7 @@ export class HomePage { } async restart(): Promise { - const loader = this.loader.open('Loading...').subscribe() + const loader = this.loader.open('Loading').subscribe() try { await this.api.diagnosticRestart() @@ -105,7 +104,7 @@ export class HomePage { } async forgetDrive(): Promise { - const loader = this.loader.open('Loading...').subscribe() + const loader = this.loader.open('Loading').subscribe() try { await this.api.diagnosticForgetDrive() @@ -119,15 +118,15 @@ export class HomePage { } async presentAlertRepairDisk() { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', data: { no: 'Cancel', yes: 'Repair', content: - '

This action should only be executed if directed by a Start9 support specialist.

If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.

', + 'This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.', }, }) .pipe(filter(Boolean)) @@ -145,7 +144,7 @@ export class HomePage { } private async repairDisk(): Promise { - const loader = this.loader.open('Loading...').subscribe() + const loader = this.loader.open('Loading').subscribe() try { await this.api.diagnosticRepairDisk() diff --git a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html index ccf6cd5b7..03b6685f0 100644 --- a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html +++ b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.html @@ -5,25 +5,18 @@ class="card" > -

Trust Your Root CA

+

{{ 'Trust your Root CA' | i18n }}

- Download and trust your server's Root Certificate Authority to establish a - secure (HTTPS) connection. You will need to repeat this on every device you - use to connect to your server. + {{ 'Download and trust your Root Certificate Authority to establish a secure (HTTPS) connection. You will need to repeat this on every device you use to connect to your server.' | i18n }}

  1. - Bookmark this page - - Save this page so you can access it later. You can also find the address - in the - StartOS-info.html - file downloaded at the end of initial setup. + {{ 'Bookmark this page' | i18n }} + - {{ 'Save this page so you can access it later. You can also find this address in the file downloaded at the end of initial setup.' | i18n }}
  2. - Download your server's Root CA - - Your server uses its Root CA to generate SSL/TLS certificates for itself - and installed services. These certificates are then used to encrypt - network traffic with your client devices. + {{ 'Download your Root CA' | i18n }} + - {{ 'Your server uses its Root CA to generate SSL/TLS certificates for itself and installed services. These certificates are then used to encrypt network traffic with your client devices.' | i18n }}
    - Download + {{ 'Download' | i18n }}
  3. - Trust your server's Root CA - - Follow instructions for your OS. By trusting your server's Root CA, your - device can verify the authenticity of encrypted communications with your - server. + {{ 'Trust your Root CA' | i18n }} + - {{ 'Follow instructions for your OS. By trusting your Root CA, your device can verify the authenticity of encrypted communications with your server.' | i18n }}
    - View Instructions + {{ 'View instructions' | i18n }}
  4. - Test - - Refresh the page. If refreshing the page does not work, you may need to - quit and re-open your browser, then revisit this page. + {{ 'Test' | i18n }} + - {{ 'Refresh the page. If refreshing the page does not work, you may need to quit and re-open your browser, then revisit this page.' | i18n }}
@@ -76,21 +66,20 @@ (click)="launchHttps()" [disabled]="caTrusted" > - Skip + {{ 'Skip' | i18n }} -
(not recommended)
+
({{ 'not recommended' | i18n }})
-

Root CA Trusted!

+

{{ 'Root CA Trusted!' | i18n }}

- You have successfully trusted your server's Root CA and may now log in - securely. + {{ 'You have successfully trusted your Root CA and may now log in securely.' | i18n }}

diff --git a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.ts b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.ts index 179215f2e..1a2b34bcd 100644 --- a/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.ts +++ b/web/projects/ui/src/app/routes/login/ca-wizard/ca-wizard.component.ts @@ -1,6 +1,6 @@ import { CommonModule, DOCUMENT } from '@angular/common' import { Component, inject } from '@angular/core' -import { RELATIVE_URL } from '@start9labs/shared' +import { i18nPipe, RELATIVE_URL } from '@start9labs/shared' import { TuiButton, TuiIcon, TuiSurface } from '@taiga-ui/core' import { TuiCardLarge } from '@taiga-ui/layout' import { ApiService } from 'src/app/services/api/embassy-api.service' @@ -11,7 +11,14 @@ import { ConfigService } from 'src/app/services/config.service' selector: 'ca-wizard', templateUrl: './ca-wizard.component.html', styleUrls: ['./ca-wizard.component.scss'], - imports: [CommonModule, TuiIcon, TuiButton, TuiCardLarge, TuiSurface], + imports: [ + CommonModule, + TuiIcon, + TuiButton, + TuiCardLarge, + TuiSurface, + i18nPipe, + ], }) export class CAWizardComponent { private readonly api = inject(ApiService) diff --git a/web/projects/ui/src/app/routes/login/login.module.ts b/web/projects/ui/src/app/routes/login/login.module.ts index deca017ff..862402cc8 100644 --- a/web/projects/ui/src/app/routes/login/login.module.ts +++ b/web/projects/ui/src/app/routes/login/login.module.ts @@ -10,6 +10,7 @@ import { } from '@taiga-ui/legacy' import { CAWizardComponent } from './ca-wizard/ca-wizard.component' import { LoginPage } from './login.page' +import { i18nPipe } from '@start9labs/shared' const routes: Routes = [ { @@ -29,6 +30,7 @@ const routes: Routes = [ TuiTextfieldControllerModule, TuiError, RouterModule.forChild(routes), + i18nPipe, ], declarations: [LoginPage], }) diff --git a/web/projects/ui/src/app/routes/login/login.page.html b/web/projects/ui/src/app/routes/login/login.page.html index d373d169b..d9e21f238 100644 --- a/web/projects/ui/src/app/routes/login/login.page.html +++ b/web/projects/ui/src/app/routes/login/login.page.html @@ -5,18 +5,18 @@
-

Login to StartOS

+

{{'Login to StartOS' | i18n}}

- Password + {{'Password' | i18n}} - +
diff --git a/web/projects/ui/src/app/routes/login/login.page.ts b/web/projects/ui/src/app/routes/login/login.page.ts index 5456b1104..bfabf4a9e 100644 --- a/web/projects/ui/src/app/routes/login/login.page.ts +++ b/web/projects/ui/src/app/routes/login/login.page.ts @@ -4,7 +4,7 @@ import { Component, Inject, DestroyRef, inject } from '@angular/core' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' import { ConfigService } from 'src/app/services/config.service' -import { LoadingService } from '@start9labs/shared' +import { i18nKey, LoadingService } from '@start9labs/shared' import { DOCUMENT } from '@angular/common' @Component({ @@ -15,7 +15,7 @@ import { DOCUMENT } from '@angular/common' }) export class LoginPage { password = '' - error = '' + error: i18nKey | null = null constructor( private readonly router: Router, @@ -27,10 +27,10 @@ export class LoginPage { ) {} async submit() { - this.error = '' + this.error = null const loader = this.loader - .open('Logging in...') + .open('Logging in') .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe() @@ -50,7 +50,7 @@ export class LoginPage { this.router.navigate([''], { replaceUrl: true }) } catch (e: any) { // code 7 is for incorrect password - this.error = e.code === 7 ? 'Invalid Password' : e.message + this.error = e.code === 7 ? 'Invalid password' : (e.message as i18nKey) } finally { loader.unsubscribe() } diff --git a/web/projects/ui/src/app/routes/portal/components/form.component.ts b/web/projects/ui/src/app/routes/portal/components/form.component.ts index 07c27b769..31cc464c5 100644 --- a/web/projects/ui/src/app/routes/portal/components/form.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form.component.ts @@ -16,7 +16,7 @@ import { import { TuiButton, TuiDialogContext } from '@taiga-ui/core' import { TuiConfirmService } from '@taiga-ui/kit' import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus' -import { compare, Operation } from 'fast-json-patch' +import { Operation } from 'fast-json-patch' import { FormModule } from 'src/app/routes/portal/components/form/form.module' import { InvalidService } from 'src/app/routes/portal/components/form/invalid.service' import { FormService } from 'src/app/services/form.service' diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html index eea9e2a5b..3dff54e88 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html +++ b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.html @@ -11,7 +11,7 @@ [disabled]="!canAdd" (click)="add()" > - + Add + + {{ 'Add' | i18n }} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts index 005f25642..0b87734e1 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form-array/form-array.component.ts @@ -9,17 +9,16 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { AbstractControl, FormArrayName } from '@angular/forms' import { TUI_ANIMATIONS_SPEED, - TuiDialogService, tuiFadeIn, tuiHeightCollapse, tuiParentStop, tuiToAnimationOptions, } from '@taiga-ui/core' -import { TUI_CONFIRM } from '@taiga-ui/kit' import { filter } from 'rxjs' import { IST } from '@start9labs/start-sdk' import { FormService } from 'src/app/services/form.service' import { ERRORS } from '../form-group/form-group.component' +import { DialogService, i18nKey } from '@start9labs/shared' @Component({ selector: 'form-array', @@ -39,8 +38,8 @@ export class FormArrayComponent { private warned = false private readonly formService = inject(FormService) - private readonly dialogs = inject(TuiDialogService) private readonly destroyRef = inject(DestroyRef) + private readonly dialog = inject(DialogService) get canAdd(): boolean { return ( @@ -52,11 +51,15 @@ export class FormArrayComponent { add() { if (!this.warned && this.spec.warning) { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', - data: { content: this.spec.warning, yes: 'Ok', no: 'Cancel' }, + data: { + content: this.spec.warning as i18nKey, + yes: 'Ok', + no: 'Cancel', + }, }) .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef)) .subscribe(() => { @@ -70,8 +73,8 @@ export class FormArrayComponent { } removeAt(index: number) { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Confirm', size: 's', data: { diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html index ad839900a..d063775ce 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html +++ b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.html @@ -16,7 +16,7 @@ let-completeWith="completeWith" > {{ spec.warning }} -

This value cannot be changed once set!

+

{{ 'This value cannot be changed once set' | i18n }}!

diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts index 378e279c0..0c9a77dcb 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts @@ -9,10 +9,11 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { AbstractTuiNullableControl } from '@taiga-ui/legacy' import { filter } from 'rxjs' -import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core' +import { TuiDialogContext } from '@taiga-ui/core' import { IST } from '@start9labs/start-sdk' import { ERRORS } from '../form-group/form-group.component' import { FORM_CONTROL_PROVIDERS } from './form-control.providers' +import { DialogService, i18nKey } from '@start9labs/shared' @Component({ selector: 'form-control', @@ -34,7 +35,7 @@ export class FormControlComponent< warned = false focused = false readonly order = ERRORS - private readonly alerts = inject(TuiAlertService) + private readonly dialog = inject(DialogService) get immutable(): boolean { return 'immutable' in this.spec && this.spec.immutable @@ -49,8 +50,8 @@ export class FormControlComponent< const previous = this.value if (!this.warned && this.warning) { - this.alerts - .open(this.warning, { + this.dialog + .openAlert(this.warning as unknown as i18nKey, { label: 'Warning', appearance: 'warning', closeable: false, diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html index 8b064f62b..9ea93323f 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html +++ b/web/projects/ui/src/app/routes/portal/components/form/form-file/form-file.component.html @@ -25,9 +25,9 @@ (edited)="value = null" /> } @else { - Click or drop file here + {{ 'Click or drop file here' | i18n }} } -
Drop file here
+
{{ 'Drop file here' | i18n }}
diff --git a/web/projects/ui/src/app/routes/portal/components/form/form.module.ts b/web/projects/ui/src/app/routes/portal/components/form/form.module.ts index 2393b2a54..5839a3e06 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form.module.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form.module.ts @@ -50,6 +50,7 @@ import { FormUnionComponent } from './form-union/form-union.component' import { HintPipe } from './hint.pipe' import { MustachePipe } from './mustache.pipe' import { FilterHiddenPipe } from './filter-hidden.pipe' +import { i18nPipe } from '@start9labs/shared' @NgModule({ imports: [ @@ -82,6 +83,7 @@ import { FilterHiddenPipe } from './filter-hidden.pipe' TuiAppearance, TuiIcon, TuiNumberFormat, + i18nPipe, ], declarations: [ FormGroupComponent, diff --git a/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts b/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts index f03d9577f..862d52168 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/hint.pipe.ts @@ -1,10 +1,13 @@ -import { Pipe, PipeTransform } from '@angular/core' +import { inject, Pipe, PipeTransform } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { IST } from '@start9labs/start-sdk' @Pipe({ name: 'hint', }) export class HintPipe implements PipeTransform { + private readonly i18n = inject(i18nPipe) + transform(spec: Exclude): string { const hint = [] @@ -13,7 +16,7 @@ export class HintPipe implements PipeTransform { } if ('disabled' in spec && typeof spec.disabled === 'string') { - hint.push(`Disabled: ${spec.disabled}`) + hint.push(`${this.i18n.transform('Disabled')}: ${spec.disabled}`) } return hint.join('\n\n') diff --git a/web/projects/ui/src/app/routes/portal/components/header/about.component.ts b/web/projects/ui/src/app/routes/portal/components/header/about.component.ts index 2643fd54a..fc62c010f 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/about.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/about.component.ts @@ -2,7 +2,7 @@ import { TuiCell } from '@taiga-ui/layout' import { TuiTitle, TuiButton } from '@taiga-ui/core' import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { CopyService } from '@start9labs/shared' +import { CopyService, i18nPipe } from '@start9labs/shared' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -13,7 +13,7 @@ import { ConfigService } from 'src/app/services/config.service'
- Version + {{ 'Version' | i18n }}
{{ server.version }}
@@ -28,7 +28,7 @@ import { ConfigService } from 'src/app/services/config.service' iconStart="@tui.copy" (click)="copyService.copy(gitHash)" > - Copy + {{ 'Copy' | i18n }}
@@ -42,7 +42,7 @@ import { ConfigService } from 'src/app/services/config.service' iconStart="@tui.copy" (click)="copyService.copy(server.caFingerprint)" > - Copy + {{ 'Copy' | i18n }}
@@ -50,7 +50,7 @@ import { ConfigService } from 'src/app/services/config.service' styles: ['[tuiCell] { padding-inline: 0 }'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TuiTitle, TuiButton, TuiCell], + imports: [CommonModule, TuiTitle, TuiButton, TuiCell, i18nPipe], }) export class AboutComponent { readonly server$ = inject>(PatchDB).watch$('serverInfo') diff --git a/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts b/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts index 47cff2b07..bb456d971 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/menu.component.ts @@ -1,15 +1,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink } from '@angular/router' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { - TuiButton, - TuiDataList, - TuiDialogOptions, - TuiDropdown, - TuiIcon, -} from '@taiga-ui/core' -import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' + DialogService, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' +import { TuiButton, TuiDataList, TuiDropdown, TuiIcon } from '@taiga-ui/core' import { filter } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' @@ -33,13 +30,13 @@ import { ABOUT } from './about.component' @if (status().status !== 'success') {
- {{ status().message }} + {{ status().message | i18n }}
} @@ -51,7 +48,7 @@ import { ABOUT } from './about.component' [iconStart]="link.icon" [href]="link.href" > - {{ link.name }} + {{ link.name | i18n }} } @@ -62,26 +59,26 @@ import { ABOUT } from './about.component' routerLink="/portal/system" (click)="open = false" > - System Settings + {{ 'System Settings' | i18n }} @@ -115,14 +112,14 @@ import { ABOUT } from './about.component' host: { '[class._open]': 'open' }, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiDropdown, TuiDataList, TuiButton, TuiIcon, RouterLink], + imports: [TuiDropdown, TuiDataList, TuiButton, TuiIcon, RouterLink, i18nPipe], }) export class HeaderMenuComponent { private readonly api = inject(ApiService) private readonly auth = inject(AuthService) - private readonly dialogs = inject(TuiResponsiveDialogService) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) + private readonly dialog = inject(DialogService) open = false @@ -130,19 +127,41 @@ export class HeaderMenuComponent { readonly status = inject(STATUS) about() { - this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe() + this.dialog.openComponent(ABOUT, { label: 'About this server' }).subscribe() } - async promptPower(action: 'Restart' | 'Shutdown') { - this.dialogs - .open(TUI_CONFIRM, getOptions(action)) + async promptPower(action: 'restart' | 'shutdown') { + this.dialog + .openConfirm( + action === 'restart' + ? { + label: 'Restart', + size: 's', + data: { + content: + 'Are you sure you want to restart your server? It can take several minutes to come back online.', + yes: 'Restart', + no: 'Cancel', + }, + } + : { + label: 'Warning', + size: 's', + data: { + content: + 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in.', + yes: 'Shutdown', + no: 'Cancel', + }, + }, + ) .pipe(filter(Boolean)) .subscribe(async () => { - const loader = this.loader.open(`Beginning ${action}...`).subscribe() + const loader = this.loader.open(`Beginning ${action}`).subscribe() try { await this.api[ - action === 'Restart' ? 'restartServer' : 'shutdownServer' + action === 'restart' ? 'restartServer' : 'shutdownServer' ]({}) } catch (e: any) { this.errorService.handleError(e) @@ -157,29 +176,3 @@ export class HeaderMenuComponent { this.auth.setUnverified() } } - -function getOptions( - operation: 'Restart' | 'Shutdown', -): Partial> { - return operation === 'Restart' - ? { - label: 'Restart', - size: 's', - data: { - content: - 'Are you sure you want to restart your server? It can take several minutes to come back online.', - yes: 'Restart', - no: 'Cancel', - }, - } - : { - label: 'Warning', - size: 's', - data: { - content: - 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in', - yes: 'Shutdown', - no: 'Cancel', - }, - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts index bf541e1f3..e50543cb7 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { RouterLink, RouterLinkActive } from '@angular/router' +import { i18nPipe } from '@start9labs/shared' import { TUI_ANIMATIONS_SPEED, - TuiButton, tuiFadeIn, TuiHint, TuiIcon, @@ -40,7 +40,7 @@ import { getMenu } from 'src/app/utils/system-utilities' } - {{ item.name }} + {{ item.name | i18n }} } `, @@ -178,11 +178,11 @@ import { getMenu } from 'src/app/utils/system-utilities' imports: [ TuiBadgeNotification, TuiBadgedContent, - TuiButton, RouterLink, TuiIcon, RouterLinkActive, TuiHint, + i18nPipe, ], }) export class HeaderNavigationComponent { diff --git a/web/projects/ui/src/app/routes/portal/components/header/status.component.ts b/web/projects/ui/src/app/routes/portal/components/header/status.component.ts index f28e53f49..6a22c14f7 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/status.component.ts @@ -1,5 +1,5 @@ -import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { TuiIcon } from '@taiga-ui/core' import { STATUS } from 'src/app/services/status.service' @@ -16,7 +16,7 @@ import { STATUS } from 'src/app/services/status.service' [attr.data-status]="status().status" /> - {{ status().message }} + {{ status().message | i18n }} `, styles: [ ` @@ -48,7 +48,7 @@ import { STATUS } from 'src/app/services/status.service' ], host: { '[class._connected]': 'status().status === "success"' }, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiIcon, AsyncPipe], + imports: [TuiIcon, i18nPipe], }) export class HeaderStatusComponent { readonly status = inject(STATUS) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts index 692435a63..f5b713740 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/actions.component.ts @@ -4,8 +4,12 @@ import { inject, input, } from '@angular/core' -import { CopyService } from '@start9labs/shared' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' +import { + CopyService, + DialogService, + i18nKey, + i18nPipe, +} from '@start9labs/shared' import { TuiButton, tuiButtonOptionsProvider, @@ -30,18 +34,18 @@ import { InterfaceComponent } from './interface.component' rel="noreferrer" [href]="actions()" > - Launch UI + {{ 'Launch UI' | i18n }} }
@@ -51,7 +55,7 @@ import { InterfaceComponent } from './interface.component' tuiDropdownOpen [tuiDropdown]="dropdown" > - Actions + {{ 'Actions' | i18n }} @@ -63,17 +67,17 @@ import { InterfaceComponent } from './interface.component' rel="noreferrer" [href]="actions()" > - Launch UI + {{ 'Launch UI' | i18n }} } @@ -105,22 +109,22 @@ import { InterfaceComponent } from './interface.component' } } `, - imports: [TuiButton, TuiDropdown, TuiDataList], + imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe], providers: [tuiButtonOptionsProvider({ appearance: 'icon' })], changeDetection: ChangeDetectionStrategy.OnPush, }) export class InterfaceActionsComponent { - readonly dialogs = inject(TuiResponsiveDialogService) + readonly dialog = inject(DialogService) readonly copyService = inject(CopyService) readonly interface = inject(InterfaceComponent) readonly actions = input.required() showQR() { - this.dialogs - .open(new PolymorpheusComponent(QRModal), { + this.dialog + .openComponent(new PolymorpheusComponent(QRModal), { size: 'auto', - label: 'Interface URL', + label: this.actions() as i18nKey, data: this.actions(), }) .subscribe() diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts index 1c7eed0b3..194955231 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts @@ -6,18 +6,21 @@ import { input, } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { + DialogService, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { ISB, utils } from '@start9labs/start-sdk' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiAppearance, TuiButton, TuiDataList, - TuiDialogOptions, TuiIcon, TuiLink, } from '@taiga-ui/core' -import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit' +import { TuiTooltip } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { defaultIfEmpty, firstValueFrom, map } from 'rxjs' import { @@ -50,15 +53,17 @@ type ClearnetForm = { Clearnet - Add a clearnet address to expose this interface on the Internet. - Clearnet addresses are fully public and not anonymous. + {{ + 'Add a clearnet address to expose this interface on the Internet. Clearnet addresses are fully public and not anonymous.' + | i18n + }} - Learn More + {{ 'Learn more' | i18n }} @if (clearnet().length) { - + } @if (clearnet().length) { - +
@for (address of clearnet(); track $index) { @@ -87,7 +94,7 @@ type ClearnetForm = { [style.margin-inline-end.rem]="0.5" (click)="remove(address)" > - Delete + {{ 'Delete' | i18n }} @@ -103,9 +110,9 @@ type ClearnetForm = {
{{ address.acme | acme }}
} @else { - No public addresses + {{ 'No public addresses' | i18n }} } @@ -123,11 +130,12 @@ type ClearnetForm = { MaskPipe, AcmePipe, InterfaceActionsComponent, + i18nPipe, ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class InterfaceClearnetComponent { - private readonly dialogs = inject(TuiResponsiveDialogService) + private readonly dialog = inject(DialogService) private readonly formDialog = inject(FormDialogService) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) @@ -146,8 +154,8 @@ export class InterfaceClearnetComponent { async remove({ url }: AddressDetails) { const confirm = await firstValueFrom( - this.dialogs - .open(TUI_CONFIRM, { label: 'Are you sure?', size: 's' }) + this.dialog + .openConfirm({ label: 'Are you sure?', size: 's' }) .pipe(defaultIfEmpty(false)), ) @@ -205,8 +213,8 @@ export class InterfaceClearnetComponent { } async add() { - const options: Partial>> = { - label: 'Select Domain/Subdomain', + this.formDialog.open>(FormComponent, { + label: 'Select Domain', data: { spec: await configBuilderToSpec( ISB.InputSpec.of({ @@ -240,12 +248,11 @@ export class InterfaceClearnetComponent { }, ], }, - } - this.formDialog.open(FormComponent, options) + }) } private async save(domainInfo: ClearnetForm): Promise { - const loader = this.loader.open('Saving...').subscribe() + const loader = this.loader.open('Saving').subscribe() const { domain, acme } = domainInfo diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts index 0eb8e7d38..a9cf4b4ab 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, input, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { tuiButtonOptionsProvider } from '@taiga-ui/core' import { InterfaceClearnetComponent } from 'src/app/routes/portal/components/interfaces/clearnet.component' import { InterfaceLocalComponent } from 'src/app/routes/portal/components/interfaces/local.component' diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index c5fbf734d..935f5ce80 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -1,6 +1,4 @@ import { T, utils } from '@start9labs/start-sdk' -import { TuiDialogOptions } from '@taiga-ui/core' -import { TuiConfirmData } from '@taiga-ui/kit' import { ConfigService } from 'src/app/services/config.service' export abstract class AddressesService { @@ -9,16 +7,6 @@ export abstract class AddressesService { abstract remove(): Promise } -export const REMOVE: Partial> = { - label: 'Confirm', - size: 's', - data: { - content: 'Remove clearnet address?', - yes: 'Remove', - no: 'Cancel', - }, -} - export function getAddresses( serviceInterface: T.ServiceInterface, host: T.Host, diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts index 1d0fa2860..5d9e878c4 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts @@ -5,28 +5,31 @@ import { TableComponent } from 'src/app/routes/portal/components/table.component import { InterfaceActionsComponent } from './actions.component' import { AddressDetails } from './interface.utils' import { MaskPipe } from './mask.pipe' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, selector: 'section[local]', template: `
- Local + {{ 'Local' | i18n }} - Local addresses can only be accessed by devices connected to the same - LAN as your server, either directly or using a VPN. + {{ + 'Local addresses can only be accessed by devices connected to the same LAN as your server, either directly or using a VPN.' + | i18n + }} - Learn More + {{ 'Learn More' | i18n }}
- +
@for (address of local(); track $index) { @@ -44,6 +47,7 @@ import { MaskPipe } from './mask.pipe' TableComponent, InterfaceActionsComponent, MaskPipe, + i18nPipe, ], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts index f550e63c0..3933f854f 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/status.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { TuiBadge } from '@taiga-ui/kit' @Component({ @@ -12,11 +13,11 @@ import { TuiBadge } from '@taiga-ui/kit' [style.margin]="'0 0.25rem -0.25rem'" [appearance]="public() ? 'positive' : 'negative'" > - {{ public() ? 'Public' : 'Private' }} + {{ public() ? ('Public' | i18n) : ('Private' | i18n) }} `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiBadge], + imports: [TuiBadge, i18nPipe], }) export class InterfaceStatusComponent { readonly public = input(false) diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts index a54ab3b86..790942f3c 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts @@ -4,23 +4,21 @@ import { inject, input, } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { + DialogService, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { ISB, utils } from '@start9labs/start-sdk' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiAppearance, TuiButton, - TuiDialogOptions, TuiIcon, TuiLink, TuiOption, } from '@taiga-ui/core' -import { - TUI_CONFIRM, - TuiFade, - TuiFluidTypography, - TuiTooltip, -} from '@taiga-ui/kit' +import { TuiFade, TuiFluidTypography, TuiTooltip } from '@taiga-ui/kit' import { defaultIfEmpty, firstValueFrom } from 'rxjs' import { FormComponent, @@ -48,15 +46,17 @@ type OnionForm = { Tor - Add an onion address to anonymously expose this interface on the - darknet. Onion addresses can only be reached over the Tor network. + {{ + 'Add an onion address to anonymously expose this interface on the darknet. Onion addresses can only be reached over the Tor network.' + | i18n + }} - Learn More + {{ 'Learn More' | i18n }} @if (tor().length) { @@ -66,12 +66,12 @@ type OnionForm = { [style.margin-inline-start]="'auto'" (click)="add()" > - Add + {{ 'Add' | i18n }} } @if (tor().length) { -
{{ address.label }}
+
@for (address of tor(); track $index) { @@ -87,7 +87,7 @@ type OnionForm = { [style.margin-inline-end.rem]="0.5" (click)="remove(address)" > - Delete + {{ 'Delete' | i18n }} @@ -103,8 +103,10 @@ type OnionForm = {
{{ address.label }}
} @else { - No Tor addresses available - + {{ 'No onion addresses' | i18n }} + } `, @@ -128,23 +130,25 @@ type OnionForm = { InterfaceActionsComponent, TuiFade, TuiFluidTypography, + i18nPipe, ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class InterfaceTorComponent { - private readonly dialogs = inject(TuiResponsiveDialogService) + private readonly dialog = inject(DialogService) private readonly formDialog = inject(FormDialogService) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) private readonly interface = inject(InterfaceComponent) + private readonly i18n = inject(i18nPipe) readonly tor = input.required() async remove({ url }: AddressDetails) { const confirm = await firstValueFrom( - this.dialogs - .open(TUI_CONFIRM, { label: 'Are you sure?', size: 's' }) + this.dialog + .openConfirm({ label: 'Are you sure?', size: 's' }) .pipe(defaultIfEmpty(false)), ) @@ -175,15 +179,16 @@ export class InterfaceTorComponent { } async add() { - const options: Partial>> = { - label: 'New Tor Address', + this.formDialog.open>(FormComponent, { + label: 'New Onion Address', data: { spec: await configBuilderToSpec( ISB.InputSpec.of({ key: ISB.Value.text({ - name: 'Private Key (optional)', - description: + name: this.i18n.transform('Private Key (optional)')!, + description: this.i18n.transform( 'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) address. If not provided, a random key will be generated and used.', + ), required: false, default: null, patterns: [utils.Patterns.base64], @@ -192,17 +197,16 @@ export class InterfaceTorComponent { ), buttons: [ { - text: 'Save', + text: this.i18n.transform('Save')!, handler: async value => this.save(value), }, ], }, - } - this.formDialog.open(FormComponent, options) + }) } private async save(form: OnionForm): Promise { - const loader = this.loader.open('Saving...').subscribe() + const loader = this.loader.open('Saving').subscribe() try { let onion = form.key diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs-download.directive.ts b/web/projects/ui/src/app/routes/portal/components/logs/logs-download.directive.ts index b983579f1..38d568a93 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs-download.directive.ts +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs-download.directive.ts @@ -24,7 +24,7 @@ export class LogsDownloadDirective { @HostListener('click') async download() { - const loader = this.loader.open('Processing 10,000 logs...').subscribe() + const loader = this.loader.open('Processing 10,000 logs').subscribe() try { const { entries } = await this.logsDownload({ diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.html b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.html index d47428b54..df51101b1 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.html +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.html @@ -6,7 +6,7 @@ (logsFetch)="onPrevious($event)" > @if (loading) { - + }
@@ -26,14 +26,14 @@

{{ status$.value === 'reconnecting' - ? 'Reconnecting' - : 'Waiting for network connectivity' + ? ('Reconnecting' | i18n) + : ('Waiting for network connectivity' | i18n) }}

} } @else { - + }
- Scroll to bottom + {{'Scroll to bottom' | i18n}} diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.ts b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.ts index 00005d859..95702aa8e 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.component.ts @@ -5,7 +5,7 @@ import { WaIntersectionObserver, } from '@ng-web-apis/intersection-observer' import { WaMutationObserver } from '@ng-web-apis/mutation-observer' -import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared' +import { FetchLogsReq, FetchLogsRes, i18nPipe } from '@start9labs/shared' import { TuiLoader, TuiScrollbar, TuiButton } from '@taiga-ui/core' import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { RR } from 'src/app/services/api/api.types' @@ -30,6 +30,7 @@ import { BehaviorSubject } from 'rxjs' LogsDownloadDirective, LogsFetchDirective, LogsPipe, + i18nPipe, ], providers: [ { diff --git a/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts b/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts index 94f909b6e..4cf28886c 100644 --- a/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/components/logs/logs.pipe.ts @@ -1,5 +1,10 @@ import { inject, Pipe, PipeTransform } from '@angular/core' -import { convertAnsi, Log, toLocalIsoString } from '@start9labs/shared' +import { + convertAnsi, + i18nPipe, + Log, + toLocalIsoString, +} from '@start9labs/shared' import { bufferTime, catchError, @@ -30,6 +35,7 @@ export class LogsPipe implements PipeTransform { private readonly api = inject(ApiService) private readonly logs = inject(LogsComponent) private readonly connection = inject(ConnectionService) + private readonly i18n = inject(i18nPipe) transform( followLogs: ( @@ -40,7 +46,7 @@ export class LogsPipe implements PipeTransform { this.logs.status$.pipe( skipWhile(value => value === 'connected'), filter(value => value === 'connected'), - map(() => getMessage(true)), + map(() => this.getMessage(true)), ), defer(() => followLogs(this.options)).pipe( tap(r => this.logs.setCursor(r.startCursor)), @@ -62,7 +68,7 @@ export class LogsPipe implements PipeTransform { filter(Boolean), take(1), ignoreElements(), - startWith(getMessage(false)), + startWith(this.getMessage(false)), ), ), repeat(), @@ -70,15 +76,15 @@ export class LogsPipe implements PipeTransform { ) } + private getMessage(success: boolean): string { + return `

${this.i18n.transform( + success ? 'Reconnected' : 'Disconnected', + )} at ${toLocalIsoString(new Date())}

` + } + private get options() { return this.logs.status$.value === 'connected' ? { limit: 400 } : {} } } - -function getMessage(success: boolean): string { - return `

${ - success ? 'Reconnected' : 'Disconnected' - } at ${toLocalIsoString(new Date())}

` -} diff --git a/web/projects/ui/src/app/routes/portal/components/table.component.ts b/web/projects/ui/src/app/routes/portal/components/table.component.ts index d8b5af656..f72c60579 100644 --- a/web/projects/ui/src/app/routes/portal/components/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/table.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' +import { i18nKey, i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -7,7 +8,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' @for (header of appTable(); track $index) { - {{ header }} + {{ header | i18n }} } @@ -20,7 +21,8 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' `, host: { class: 'g-table' }, changeDetection: ChangeDetectionStrategy.OnPush, + imports: [i18nPipe], }) export class TableComponent { - readonly appTable = input.required() + readonly appTable = input.required>() } diff --git a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts index 0b51d649e..234322236 100644 --- a/web/projects/ui/src/app/routes/portal/components/tabs.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/tabs.component.ts @@ -7,6 +7,7 @@ import { } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { RouterLink, RouterLinkActive } from '@angular/router' +import { i18nPipe } from '@start9labs/shared' import { TuiResponsiveDialogService, TuiTabBar } from '@taiga-ui/addon-mobile' import { TuiIcon } from '@taiga-ui/core' import { TuiBadgeNotification } from '@taiga-ui/kit' @@ -27,7 +28,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace'] routerLinkActive (isActiveChange)="update()" > - Services + {{ 'Services' | i18n }} - Marketplace + {{ 'Marketplace' | i18n }} - System + {{ 'System' | i18n }} - {{ logs[key]?.title }} + {{ logs[key]?.title | i18n }} } @else { - Logs + {{ 'Logs' | i18n }} } @if (current(); as key) { @@ -47,16 +48,16 @@ interface Log { class="close" (click)="current.set(null)" > - Close + {{ 'Close' | i18n }} - {{ logs[key]?.title }} + {{ logs[key]?.title | i18n }} -

{{ logs[key]?.subtitle }}

+

{{ logs[key]?.subtitle | i18n }}

@for (log of logs | keyvalue; track $index) { @if (log.key === current()) { @@ -71,8 +72,8 @@ interface Log { > - {{ log.value.title }} - {{ log.value.subtitle }} + {{ log.value.title | i18n }} + {{ log.value.subtitle | i18n }} @@ -164,6 +165,7 @@ interface Log { TuiIcon, TuiAppearance, TuiButton, + i18nPipe, ], }) export default class SystemLogsComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts index 697a129a8..92a3db677 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/controls.component.ts @@ -15,6 +15,7 @@ import { LoadingService, sameUrl, ExverPipesModule, + i18nPipe, } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { firstValueFrom } from 'rxjs' @@ -45,7 +46,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' appearance="secondary-destructive" (click)="tryInstall()" > - Downgrade + {{ 'Downgrade' | i18n }} } @case (-1) { @@ -55,7 +56,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' appearance="primary" (click)="tryInstall()" > - Update + {{ 'Update' | i18n }} } @case (0) { @@ -65,7 +66,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' appearance="secondary-grayscale" (click)="tryInstall()" > - Reinstall + {{ 'Reinstall' | i18n }} } } @@ -76,7 +77,7 @@ import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' appearance="outline-grayscale" (click)="showService()" > - View Installed + {{ 'View Installed' | i18n }} } @else { } `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, ExverPipesModule, TuiButton, ToManifestPipe], + imports: [ + CommonModule, + ExverPipesModule, + TuiButton, + ToManifestPipe, + i18nPipe, + ], }) export class MarketplaceControlsComponent { private readonly alerts = inject(MarketplaceAlertsService) @@ -161,7 +168,7 @@ export class MarketplaceControlsComponent { } private async install(url: string) { - const loader = this.loader.open('Beginning Install...').subscribe() + const loader = this.loader.open('Beginning install').subscribe() const { id, version } = this.pkg try { diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/menu.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/menu.component.ts index 46603da40..1efb24bc2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/menu.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/menu.component.ts @@ -1,15 +1,11 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { CommonModule } from '@angular/common' import { MenuModule } from '@start9labs/marketplace' -import { - TuiDialogService, - TuiIcon, - TuiButton, - TuiAppearance, -} from '@taiga-ui/core' +import { TuiIcon, TuiButton, TuiAppearance } from '@taiga-ui/core' import { ConfigService } from 'src/app/services/config.service' import { MARKETPLACE_REGISTRY } from '../modals/registry.component' import { MarketplaceService } from 'src/app/services/marketplace.service' +import { DialogService, i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -24,11 +20,11 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' iconStart="@tui.repeat" (click)="changeRegistry()" > - Change Registry + {{ 'Change Registry' | i18n }} `, @@ -47,17 +43,24 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' `, ], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, MenuModule, TuiButton, TuiIcon, TuiAppearance], + imports: [ + CommonModule, + MenuModule, + TuiButton, + TuiIcon, + TuiAppearance, + i18nPipe, + ], }) export class MarketplaceMenuComponent { - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) readonly marketplace = inject(ConfigService).marketplace private readonly marketplaceService = inject(MarketplaceService) readonly registry$ = this.marketplaceService.getRegistry$() changeRegistry() { - this.dialogs - .open(MARKETPLACE_REGISTRY, { + this.dialog + .openComponent(MARKETPLACE_REGISTRY, { label: 'Change Registry', }) .subscribe() diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/notification.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/notification.component.ts index e0c0ff565..c906e1c93 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/components/notification.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/components/notification.component.ts @@ -1,4 +1,5 @@ import { Component, inject, Input } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { TuiNotification } from '@taiga-ui/core' import { ConfigService } from 'src/app/services/config.service' @@ -13,40 +14,34 @@ import { ConfigService } from 'src/app/services/config.service' > @switch (status) { @case ('success') { - Services from this registry are packaged and maintained by the Start9 - team. If you experience an issue or have questions related to a - service from this registry, one of our dedicated support staff will be - happy to assist you. + {{ + 'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.' + | i18n + }} } @case ('info') { - Services from this registry are packaged and maintained by members of - the Start9 community. - Install at your own risk - . If you experience an issue or have a question related to a service - in this marketplace, please reach out to the package developer for - assistance. + {{ + 'Services from this registry are packaged and maintained by members of the Start9 community. Install at your own risk. If you experience an issue or have a question related to a service in this marketplace, please reach out to the package developer for assistance.' + | i18n + }} } @case ('warning') { - Services from this registry are undergoing - beta - testing and may contain bugs. - Install at your own risk - . + {{ + 'Services from this registry are undergoing beta testing and may contain bugs. Install at your own risk.' + | i18n + }} } @case ('error') { - Services from this registry are undergoing - alpha - testing. They are expected to contain bugs and could damage your - system. - Install at your own risk - . + {{ + 'Services from this registry are undergoing alpha testing. They are expected to contain bugs and could damage your system. Install at your own risk.' + | i18n + }} } @default { - This is a Custom Registry. Start9 cannot verify the integrity or - functionality of services from this registry, and they could damage - your system. - Install at your own risk - . + {{ + 'This is a Custom Registry. Start9 cannot verify the integrity or functionality of services from this registry, and they could damage your system. Install at your own risk.' + | i18n + }} } } @@ -59,7 +54,7 @@ import { ConfigService } from 'src/app/services/config.service' } `, ], - imports: [TuiNotification], + imports: [TuiNotification, i18nPipe], }) export class MarketplaceNotificationComponent { private readonly marketplace = inject(ConfigService).marketplace diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts index f18535434..46516c634 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/marketplace.component.ts @@ -17,11 +17,12 @@ import { MarketplaceMenuComponent } from './components/menu.component' import { MarketplaceNotificationComponent } from './components/notification.component' import { MarketplaceSidebarsComponent } from './components/sidebars.component' import { MarketplaceTileComponent } from './components/tile.component' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, template: ` - Marketplace + {{ 'Marketplace' | i18n }}
@@ -48,7 +49,7 @@ import { MarketplaceTileComponent } from './components/tile.component'
} @else {

- Loading + {{ 'Loading' | i18n }}

} @@ -156,6 +157,7 @@ import { MarketplaceTileComponent } from './components/tile.component' TuiScrollbar, FilterPackagesPipeModule, TitleDirective, + i18nPipe, ], }) export default class MarketplaceComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts index 349db4bdd..ad3f96c7e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/preview.component.ts @@ -17,13 +17,13 @@ import { MarketplacePackageHeroComponent, MarketplacePkg, } from '@start9labs/marketplace' -import { Exver, SharedPipesModule } from '@start9labs/shared' import { - TuiButton, - TuiDialogContext, - TuiDialogService, - TuiLoader, -} from '@taiga-ui/core' + DialogService, + Exver, + i18nPipe, + SharedPipesModule, +} from '@start9labs/shared' +import { TuiButton, TuiDialogContext, TuiLoader } from '@taiga-ui/core' import { TuiRadioList } from '@taiga-ui/kit' import { BehaviorSubject, @@ -77,14 +77,14 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' appearance="secondary" (click)="completeWith(null)" > - Cancel + {{ 'Cancel' | i18n }} @@ -174,13 +174,14 @@ import { MarketplaceService } from 'src/app/services/marketplace.service' TuiRadioList, TuiLoader, FlavorsComponent, + i18nPipe, ], }) export class MarketplacePreviewComponent { @Input({ required: true }) pkgId!: string - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly exver = inject(Exver) private readonly router = inject(Router) private readonly marketplaceService = inject(MarketplaceService) @@ -233,8 +234,8 @@ export class MarketplacePreviewComponent { { version }: MarketplacePkg, template: TemplateRef, ) { - this.dialogs - .open(template, { + this.dialog + .openComponent(template, { label: 'Versions', size: 's', data: { diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/registry.component.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/registry.component.ts index 91eb44eb5..cd4ad6a05 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/registry.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/modals/registry.component.ts @@ -1,12 +1,15 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' +import { Router } from '@angular/router' import { MarketplaceRegistryComponent, StoreIconComponentModule, } from '@start9labs/marketplace' import { + DialogService, ErrorService, + i18nKey, + i18nPipe, LoadingService, sameUrl, toUrl, @@ -14,11 +17,10 @@ import { import { TuiButton, TuiDialogContext, - TuiDialogService, + TuiDialogOptions, TuiIcon, TuiTitle, } from '@taiga-ui/core' -import { TUI_CONFIRM } from '@taiga-ui/kit' import { TuiCell } from '@taiga-ui/layout' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' @@ -29,13 +31,14 @@ import { ConfigService } from 'src/app/services/config.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' -import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry' +import { TuiConfirmData } from '@taiga-ui/kit' +import { IST, utils } from '@start9labs/start-sdk' @Component({ standalone: true, template: ` @if (stores$ | async; as stores) { -

Default Registries

+

{{ 'Default Registries' | i18n }}

@for (registry of stores.standard; track $index) { } -

Custom Registries

+

{{ 'Custom Registries' | i18n }}

@for (registry of stores.alt; track $index) {
@@ -64,7 +67,7 @@ import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry' iconStart="@tui.trash-2" (click)="delete(registry.url, registry.name)" > - Delete + {{ 'Delete' | i18n }}
} @@ -88,6 +91,7 @@ import { getMarketplaceValueSpec, getPromptOptions } from '../utils/registry' TuiButton, MarketplaceRegistryComponent, StoreIconComponentModule, + i18nPipe, ], }) export class MarketplaceRegistryModal { @@ -95,16 +99,16 @@ export class MarketplaceRegistryModal { private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) private readonly formDialog = inject(FormDialogService) - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly marketplaceService = inject(MarketplaceService) private readonly context = injectContext() - private readonly route = inject(ActivatedRoute) private readonly router = inject(Router) private readonly hosts$ = inject>(PatchDB).watch$( 'ui', 'marketplace', 'knownHosts', ) + private readonly i18n = inject(i18nPipe) readonly marketplaceConfig = inject(ConfigService).marketplace readonly stores$ = combineLatest([ @@ -122,19 +126,19 @@ export class MarketplaceRegistryModal { ) add() { - const { name, spec } = getMarketplaceValueSpec() + const { name, spec } = this.getMarketplaceValueSpec() this.formDialog.open(FormComponent, { - label: name, + label: name as i18nKey, data: { spec, buttons: [ { - text: 'Save for Later', + text: this.i18n.transform('Save for later'), handler: async ({ url }: { url: string }) => this.save(url), }, { - text: 'Save and Connect', + text: this.i18n.transform('Save and connect'), handler: async ({ url }: { url: string }) => this.save(url, true), isSubmit: true, }, @@ -144,11 +148,19 @@ export class MarketplaceRegistryModal { } delete(url: string, name: string = '') { - this.dialogs - .open(TUI_CONFIRM, getPromptOptions(name)) + this.dialog + .openConfirm({ + label: 'Confirm', + size: 's', + data: { + content: 'Are you sure you want to delete this registry?', + yes: 'Delete', + no: 'Cancel', + }, + }) .pipe(filter(Boolean)) .subscribe(async () => { - const loader = this.loader.open('Deleting...').subscribe() + const loader = this.loader.open('Deleting').subscribe() const hosts = await firstValueFrom(this.hosts$) const filtered: { [url: string]: UIStore } = Object.keys(hosts) .filter(key => !sameUrl(key, url)) @@ -176,7 +188,7 @@ export class MarketplaceRegistryModal { ): Promise { loader.unsubscribe() loader.closed = false - loader.add(this.loader.open('Changing Registry...').subscribe()) + loader.add(this.loader.open('Changing registry').subscribe()) try { this.marketplaceService.setRegistryUrl(url) this.router.navigate([], { @@ -192,6 +204,36 @@ export class MarketplaceRegistryModal { } } + private getMarketplaceValueSpec(): IST.ValueSpecObject { + return { + type: 'object', + name: this.i18n.transform('Add Custom Registry')!, + description: null, + warning: null, + spec: { + url: { + type: 'text', + name: 'URL', + description: this.i18n.transform( + 'A fully-qualified URL of the custom registry', + )!, + inputmode: 'url', + required: true, + masked: false, + minLength: null, + maxLength: null, + patterns: [utils.Patterns.url], + placeholder: 'e.g. https://example.org', + default: null, + warning: null, + disabled: false, + immutable: false, + generate: null, + }, + }, + } + } + private async save(rawUrl: string, connect = false): Promise { const loader = this.loader.open('Loading').subscribe() const url = new URL(rawUrl).toString() @@ -215,12 +257,13 @@ export class MarketplaceRegistryModal { // Error on duplicates const hosts = await firstValueFrom(this.hosts$) const currentUrls = Object.keys(hosts).map(toUrl) - if (currentUrls.includes(url)) throw new Error('Marketplace already added') + if (currentUrls.includes(url)) + throw new Error(this.i18n.transform('Registry already added')) // Validate loader.unsubscribe() loader.closed = false - loader.add(this.loader.open('Validating marketplace...').subscribe()) + loader.add(this.loader.open('Validating registry').subscribe()) const { name } = await firstValueFrom( this.marketplaceService.fetchInfo$(url), @@ -229,7 +272,7 @@ export class MarketplaceRegistryModal { // Save loader.unsubscribe() loader.closed = false - loader.add(this.loader.open('Saving...').subscribe()) + loader.add(this.loader.open('Saving').subscribe()) await this.api.setDbValue(['marketplace', 'knownHosts', url], { name }) } diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts index 006e794a5..d40c589ff 100644 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts +++ b/web/projects/ui/src/app/routes/portal/routes/marketplace/services/alerts.service.ts @@ -1,34 +1,37 @@ -import { TUI_CONFIRM } from '@taiga-ui/kit' import { inject, Injectable } from '@angular/core' -import { MarketplacePkg, MarketplacePkgBase } from '@start9labs/marketplace' -import { TuiDialogService } from '@taiga-ui/core' +import { MarketplacePkgBase } from '@start9labs/marketplace' import { PatchDB } from 'patch-db-client' import { defaultIfEmpty, firstValueFrom } from 'rxjs' import { DataModel } from 'src/app/services/patch-db/data-model' +import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' @Injectable({ providedIn: 'root', }) export class MarketplaceAlertsService { - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly marketplace$ = inject>(PatchDB).watch$( 'ui', 'marketplace', ) + private readonly i18n = inject(i18nPipe) async alertMarketplace(url: string, originalUrl: string): Promise { const marketplaces = await firstValueFrom(this.marketplace$) const name = marketplaces.knownHosts[url]?.name || url const source = marketplaces.knownHosts[originalUrl]?.name || originalUrl - const message = source ? `installed from ${source}` : 'side loaded' + const message = source + ? `${this.i18n.transform('installed from')} ${source}` + : this.i18n.transform('sideloaded') return new Promise(async resolve => { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', data: { - content: `This service was originally ${message}, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`, + content: + `${this.i18n.transform('This service was originally')} ${message}, ${this.i18n.transform('but you are currently connected to')} ${name}. ${this.i18n.transform('To install from')} ${name} ${this.i18n.transform('anyway, click "Continue".')}` as i18nKey, yes: 'Continue', no: 'Cancel', }, @@ -39,14 +42,14 @@ export class MarketplaceAlertsService { } async alertBreakages(breakages: string[]): Promise { - let content: string = - 'As a result of this update, the following services will no longer work properly and may crash:
    ' + let content = + `${this.i18n.transform('As a result of this update, the following services will no longer work properly and may crash')}:
      '` as i18nKey const bullets = breakages.map(title => `
    • ${title}
    • `) - content = `${content}${bullets.join('')}
    ` + content = `${content}${bullets.join('')}
` as i18nKey return new Promise(async resolve => { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', data: { @@ -61,13 +64,13 @@ export class MarketplaceAlertsService { } async alertInstall({ alerts }: MarketplacePkgBase): Promise { - const content = alerts.install + const content = alerts.install as i18nKey return ( !!content && new Promise(resolve => { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Alert', size: 's', data: { diff --git a/web/projects/ui/src/app/routes/portal/routes/marketplace/utils/registry.ts b/web/projects/ui/src/app/routes/portal/routes/marketplace/utils/registry.ts deleted file mode 100644 index 774e5941d..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/marketplace/utils/registry.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IST } from '@start9labs/start-sdk' -import { TuiDialogOptions } from '@taiga-ui/core' -import { TuiConfirmData } from '@taiga-ui/kit' - -export function getMarketplaceValueSpec(): IST.ValueSpecObject { - return { - type: 'object', - name: 'Add Custom Registry', - description: null, - warning: null, - spec: { - url: { - type: 'text', - name: 'URL', - description: 'A fully-qualified URL of the custom registry', - inputmode: 'url', - required: true, - masked: false, - minLength: null, - maxLength: null, - patterns: [ - { - regex: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`, - description: 'Must be a valid URL', - }, - ], - placeholder: 'e.g. https://example.org', - default: null, - warning: null, - disabled: false, - immutable: false, - generate: null, - }, - }, - } -} - -export function getPromptOptions( - name: string, -): Partial> { - return { - label: 'Confirm', - size: 's', - data: { - content: `Are you sure you want to delete ${name}?`, - yes: 'Delete', - no: 'Cancel', - }, - } -} diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/cpu.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/cpu.component.ts index e121b5b62..495644815 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/cpu.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/cpu.component.ts @@ -6,13 +6,14 @@ import { } from '@angular/core' import { ServerMetrics } from 'src/app/services/api/api.types' import { DataComponent } from './data.component' +import { i18nKey } from '@start9labs/shared' -const LABELS = { - percentageUsed: 'Percentage Used', - userSpace: 'User Space', - kernelSpace: 'Kernel Space', +const LABELS: Record = { + percentageUsed: 'Percentage used', + userSpace: 'User space', + kernelSpace: 'Kernel space', idle: 'Idle', - wait: 'I/O Wait', + wait: 'I/O wait', } @Component({ diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/data.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/data.component.ts index bd0c888c9..3d32db532 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/data.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/data.component.ts @@ -8,6 +8,7 @@ import { TuiTitle } from '@taiga-ui/core' import { TuiCell } from '@taiga-ui/layout' import { ServerMetrics } from 'src/app/services/api/api.types' import { ValuePipe } from './value.pipe' +import { i18nKey, i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -15,7 +16,7 @@ import { ValuePipe } from './value.pipe' template: ` @for (key of keys(); track $index) {
- {{ labels()[key] }} + {{ labels()[key] | i18n }} {{ $any(value()?.[key])?.value | value }} @@ -41,10 +42,10 @@ import { ValuePipe } from './value.pipe' } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiCell, TuiTitle, ValuePipe], + imports: [TuiCell, TuiTitle, ValuePipe, i18nPipe], }) export class DataComponent { - readonly labels = input.required>() + readonly labels = input.required>() readonly value = input() readonly keys = computed(() => Object.keys(this.labels()) as Array) } diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/memory.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/memory.component.ts index 0a144bd67..a6c587421 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/memory.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/memory.component.ts @@ -8,15 +8,16 @@ import { TuiProgress } from '@taiga-ui/kit' import { ServerMetrics } from 'src/app/services/api/api.types' import { DataComponent } from './data.component' import { ValuePipe } from './value.pipe' +import { i18nKey } from '@start9labs/shared' -const LABELS = { - percentageUsed: 'Percentage Used', +const LABELS: Record = { + percentageUsed: 'Percentage used', total: 'Total', used: 'Used', available: 'Available', - zramUsed: 'zram Used', - zramTotal: 'zram Total', - zramAvailable: 'zram Available', + zramUsed: 'zram used', + zramTotal: 'zram total', + zramAvailable: 'zram available', } @Component({ diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.component.ts index 254979b44..6ac4a9f79 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/metrics.component.ts @@ -13,23 +13,24 @@ import { StorageComponent } from './storage.component' import { TemperatureComponent } from './temperature.component' import { TimeComponent } from './time.component' import { UptimeComponent } from './uptime.component' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, selector: 'app-metrics', template: ` - Metrics + {{ 'Metrics' | i18n }}
-
System Time
+
{{ 'System Time' | i18n }}
-
Uptime
+
{{ 'Uptime' | i18n }}
-
Temperature
+
{{ 'Temperature' | i18n }}
@@ -37,11 +38,11 @@ import { UptimeComponent } from './uptime.component'
-
Memory
+
{{ 'Memory' | i18n }}
-
Storage
+
{{ 'Storage' | i18n }}
@@ -93,6 +94,7 @@ import { UptimeComponent } from './uptime.component' MemoryComponent, UptimeComponent, TimeComponent, + i18nPipe, ], }) export default class SystemMetricsComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/storage.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/storage.component.ts index a4eb0d51a..388bf4f1d 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/storage.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/storage.component.ts @@ -7,9 +7,10 @@ import { import { TuiProgress } from '@taiga-ui/kit' import { ServerMetrics } from 'src/app/services/api/api.types' import { DataComponent } from './data.component' +import { i18nKey } from '@start9labs/shared' -const LABELS = { - percentageUsed: 'Percentage Used', +const LABELS: Record = { + percentageUsed: 'Percentage used', capacity: 'Capacity', used: 'Used', available: 'Available', diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts index a000d2bd2..fea30fc2e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/time.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { i18nPipe } from '@start9labs/shared' import { TuiHint, TuiIcon, @@ -37,11 +38,11 @@ import { TimeService } from 'src/app/services/time.service' } } @else { - Loading... + {{ 'Loading' | i18n }}... }
@@ -108,6 +109,7 @@ import { TimeService } from 'src/app/services/time.service' TuiCell, TuiIcon, TuiHint, + i18nPipe, ], }) export class TimeComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts b/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts index 7f2428064..d0383efc8 100644 --- a/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/metrics/uptime.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { i18nPipe } from '@start9labs/shared' import { TimeService } from 'src/app/services/time.service' @Component({ @@ -9,22 +10,22 @@ import { TimeService } from 'src/app/services/time.service' @if (uptime(); as time) {
{{ time.days }} - Days + {{ 'Days' | i18n }}
{{ time.hours }} - Hours + {{ 'Hours' | i18n }}
{{ time.minutes }} - Minutes + {{ 'Minutes' | i18n }}
{{ time.seconds }} - Seconds + {{ 'Seconds' | i18n }}
} @else { - Loading... + {{ 'Loading' | i18n }}... } `, styles: ` @@ -49,6 +50,7 @@ import { TimeService } from 'src/app/services/time.service' } `, changeDetection: ChangeDetectionStrategy.OnPush, + imports: [i18nPipe], }) export class UptimeComponent { readonly uptime = toSignal(inject(TimeService).uptime$) diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts index 41c1a4327..4673c0ecc 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/item.component.ts @@ -16,6 +16,7 @@ import { ServerNotification } from 'src/app/services/api/api.types' import { NotificationService } from 'src/app/services/notification.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { toRouterLink } from 'src/app/utils/to-router-link' +import { i18nPipe } from '@start9labs/shared' @Component({ selector: '[notificationItem]', @@ -47,12 +48,12 @@ import { toRouterLink } from 'src/app/utils/to-router-link' /> @if (overflow) { } @if (notificationItem.code === 1 || notificationItem.code === 2) { } @@ -109,7 +110,7 @@ import { toRouterLink } from 'src/app/utils/to-router-link' } } `, - imports: [CommonModule, RouterLink, TuiLineClamp, TuiLink, TuiIcon], + imports: [CommonModule, RouterLink, TuiLineClamp, TuiLink, TuiIcon, i18nPipe], }) export class NotificationItemComponent { private readonly patch = inject>(PatchDB) diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts index c9d878603..61853371c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts @@ -4,7 +4,7 @@ import { inject, signal, } from '@angular/core' -import { ErrorService } from '@start9labs/shared' +import { ErrorService, i18nPipe } from '@start9labs/shared' import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core' import { RR, ServerNotifications } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' @@ -14,7 +14,7 @@ import { NotificationsTableComponent } from './table.component' @Component({ template: ` - Notifications + {{ 'Notifications' | i18n }}

@@ -37,16 +37,16 @@ import { NotificationsTableComponent } from './table.component' tuiOption (click)="markSeen(notifications(), table.selected())" > - Mark seen + {{ 'Mark seen' | i18n }} @@ -62,6 +62,7 @@ import { NotificationsTableComponent } from './table.component' TuiDataList, NotificationsTableComponent, TitleDirective, + i18nPipe, ], }) export default class NotificationsComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts index 521b66be1..40dd4d14c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts @@ -12,6 +12,7 @@ import { ServerNotifications, } from 'src/app/services/api/api.types' import { NotificationItemComponent } from './item.component' +import { i18nPipe } from '@start9labs/shared' @Component({ selector: 'table[notifications]', @@ -29,10 +30,10 @@ import { NotificationItemComponent } from './item.component' (ngModelChange)="onAll($event)" /> - Date - Title - Service - Message + {{ 'Date' | i18n }} + {{ 'Title' | i18n }} + {{ 'Service' | i18n }} + {{ 'Message' | i18n }} @@ -53,13 +54,15 @@ import { NotificationItemComponent } from './item.component' } @empty { - You have no notifications + {{ 'No notifications' | i18n }} } } @else { @for (row of ['', '']; track $index) { -
Loading
+ +
{{ 'Loading' | i18n }}
+ } } @@ -75,7 +78,13 @@ import { NotificationItemComponent } from './item.component' `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [FormsModule, TuiCheckbox, NotificationItemComponent, TuiSkeleton], + imports: [ + FormsModule, + TuiCheckbox, + NotificationItemComponent, + TuiSkeleton, + i18nPipe, + ], }) export class NotificationsTableComponent implements OnChanges { @Input() notifications?: ServerNotifications diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/action-request.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/action-request.component.ts index 7adc990cc..d0c515a29 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/action-request.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/action-request.component.ts @@ -5,6 +5,7 @@ import { inject, input, } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiButton } from '@taiga-ui/core' import { TuiAvatar } from '@taiga-ui/kit' @@ -22,16 +23,20 @@ import { getManifest } from 'src/app/utils/get-package-data' @if (actionRequest().severity === 'critical') { - Required + + {{ 'Required' | i18n }} + } @else { - Optional + + {{ 'Optional' | i18n }} + } - {{ actionRequest().reason || 'No reason provided' }} + {{ actionRequest().reason || ('No reason provided' | i18n) }} } @@ -33,7 +34,7 @@ import { getManifest } from 'src/app/utils/get-package-data' iconStart="@tui.rotate-cw" (click)="controls.restart(manifest())" > - Restart + {{ 'Restart' | i18n }} } @@ -43,7 +44,7 @@ import { getManifest } from 'src/app/utils/get-package-data' iconStart="@tui.play" (click)="controls.start(manifest(), !!hasUnmet())" > - Start + {{ 'Start' | i18n }} } `, @@ -82,7 +83,7 @@ import { getManifest } from 'src/app/utils/get-package-data' ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiButton], + imports: [TuiButton, i18nPipe], }) export class ServiceControlsComponent { private readonly errors = inject(DepErrorService) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts index 14f61e6f1..2db158d42 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts @@ -1,6 +1,7 @@ import { KeyValuePipe } from '@angular/common' import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { RouterLink } from '@angular/router' +import { i18nKey, i18nPipe } from '@start9labs/shared' import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiAvatar } from '@taiga-ui/kit' import { TuiCell } from '@taiga-ui/layout' @@ -11,7 +12,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @Component({ selector: 'service-dependencies', template: ` -
Dependencies
+
{{ 'Dependencies' | i18n }}
@for (d of pkg.currentDependencies | keyvalue; track $index) { {{ d.value.title }} @if (getError(d.key); as error) { - {{ error }} + {{ error | i18n }} } @else { - Satisfied + {{ 'Satisfied' | i18n }} } {{ d.value.versionRange }} } @empty { - No dependencies + + {{ 'No dependencies' | i18n }} + } `, styles: ` @@ -51,6 +54,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' TuiTitle, TuiIcon, PlaceholderComponent, + i18nPipe, ], }) export class ServiceDependenciesComponent { @@ -63,11 +67,11 @@ export class ServiceDependenciesComponent { @Input({ required: true }) errors: PkgDependencyErrors = {} - getError(id: string): string { + getError(id: string): i18nKey | undefined { const depError = this.errors[id] if (!depError) { - return '' + return undefined } switch (depError.type) { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts index 072c69770..f2c8c2ad2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts @@ -4,13 +4,8 @@ import { inject, Input, } from '@angular/core' -import { copyToClipboard } from '@start9labs/shared' -import { - TuiAlertService, - TuiButton, - TuiDialogService, - TuiIcon, -} from '@taiga-ui/core' +import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' +import { TuiButton, TuiIcon } from '@taiga-ui/core' import { TuiLineClamp, TuiTooltip } from '@taiga-ui/kit' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { StandardActionsService } from 'src/app/services/standard-actions.service' @@ -20,34 +15,40 @@ import { getManifest } from 'src/app/utils/get-package-data' standalone: true, selector: 'service-error', template: ` -
Error
+
{{ 'Error' | i18n }}

- Actions + {{ 'Actions' | i18n }}

-
- Rebuild Container - is harmless action that and only takes a few seconds to complete. It - will likely resolve this issue. -
- Uninstall Service - is a dangerous action that will remove the service from StartOS and wipe - all its data. +

+ {{ + '"Rebuild container" is a harmless action that and only takes a few seconds to complete. It will likely resolve this issue.' + | i18n + }} +

+

+ {{ + '"Uninstall service" is a dangerous action that will remove the service from StartOS and wipe all its data.' + | i18n + }} +

- + @if (overflow) { }

@@ -79,11 +80,10 @@ import { getManifest } from 'src/app/utils/get-package-data' `, host: { class: 'g-card' }, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiButton, TuiIcon, TuiTooltip, TuiLineClamp], + imports: [TuiButton, TuiIcon, TuiTooltip, TuiLineClamp, i18nPipe], }) export class ServiceErrorComponent { - private readonly dialogs = inject(TuiDialogService) - private readonly alerts = inject(TuiAlertService) + private readonly dialog = inject(DialogService) private readonly service = inject(StandardActionsService) @Input({ required: true }) @@ -95,16 +95,6 @@ export class ServiceErrorComponent { return this.pkg.status.main === 'error' ? this.pkg.status : null } - async copy(text: string): Promise { - const success = await copyToClipboard(text) - - this.alerts - .open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.', { - appearance: success ? 'positive' : 'negative', - }) - .subscribe() - } - rebuild() { this.service.rebuild(getManifest(this.pkg).id) } @@ -114,8 +104,8 @@ export class ServiceErrorComponent { } show() { - this.dialogs - .open(this.error?.message, { label: 'Service error' }) + this.dialog + .openAlert(this.error?.message as i18nKey, { label: 'Service error' }) .subscribe() } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/health-check.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/health-check.component.ts index 0ce752e8d..bf95365df 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/health-check.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/health-check.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiIcon, TuiLoader } from '@taiga-ui/core' @@ -48,6 +54,8 @@ export class ServiceHealthCheckComponent { @Input({ required: true }) healthCheck!: T.NamedHealthCheckResult + private readonly i18n = inject(i18nPipe) + get loading(): boolean { const { result } = this.healthCheck @@ -82,14 +90,14 @@ export class ServiceHealthCheckComponent { get message(): string { if (!this.healthCheck.result) { - return 'Awaiting result...' + return this.i18n.transform('Awaiting result')! } switch (this.healthCheck.result) { case 'starting': - return 'Starting...' + return this.i18n.transform('Starting')! case 'success': - return `Success: ${this.healthCheck.message}` + return `${this.i18n.transform('Success')}: ${this.healthCheck.message}` case 'loading': case 'failure': return this.healthCheck.message diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/health-checks.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/health-checks.component.ts index e71805e03..7da77b968 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/health-checks.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/health-checks.component.ts @@ -3,17 +3,18 @@ import { T } from '@start9labs/start-sdk' import { TuiTable } from '@taiga-ui/addon-table' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { ServiceHealthCheckComponent } from './health-check.component' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, selector: 'service-health-checks', template: ` -
Health Checks
+
{{ 'Health Checks' | i18n }}
- - + + @@ -24,7 +25,7 @@ import { ServiceHealthCheckComponent } from './health-check.component'
NameStatus{{ 'Name' | i18n }}{{ 'Status' | i18n }}
@if (!checks().length) { - No health checks + {{ 'No health checks' | i18n }} } `, @@ -36,7 +37,12 @@ import { ServiceHealthCheckComponent } from './health-check.component' `, host: { class: 'g-card' }, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ServiceHealthCheckComponent, PlaceholderComponent, TuiTable], + imports: [ + ServiceHealthCheckComponent, + PlaceholderComponent, + TuiTable, + i18nPipe, + ], }) export class ServiceHealthChecksComponent { readonly checks = input.required() diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts index a84bac940..ced31d3fa 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/interface.component.ts @@ -5,6 +5,7 @@ import { Input, } from '@angular/core' import { RouterLink } from '@angular/router' +import { i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiBadge } from '@taiga-ui/kit' @@ -57,7 +58,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' [attr.href]="href" (click.stop)="(0)" > - Open + {{ 'Open' | i18n }} } @@ -107,7 +108,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiButton, TuiBadge, TuiLink, RouterLink], + imports: [TuiButton, TuiBadge, TuiLink, RouterLink, i18nPipe], }) export class ServiceInterfaceComponent { private readonly config = inject(ConfigService) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts index 81f5b4482..3518c5bd2 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts @@ -12,19 +12,20 @@ import { ConfigService } from 'src/app/services/config.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { getAddresses } from '../../../components/interfaces/interface.utils' import { ServiceInterfaceComponent } from './interface.component' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, selector: 'service-interfaces', template: ` -
Interfaces
+
{{ 'Interfaces' | i18n }}
- - - - + + + + @@ -48,7 +49,7 @@ import { ServiceInterfaceComponent } from './interface.component' `, host: { class: 'g-card' }, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ServiceInterfaceComponent, TuiTable, RouterLink], + imports: [ServiceInterfaceComponent, TuiTable, RouterLink, i18nPipe], }) export class ServiceInterfacesComponent { private readonly config = inject(ConfigService) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts index 8d5fac35d..f1530528d 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { i18nKey, i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiLoader } from '@taiga-ui/core' import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' @@ -11,20 +17,20 @@ import { @Component({ selector: 'service-status', template: ` -
Status
+
{{ 'Status' | i18n }}
@if (installingInfo) {

- Installing + {{ 'Installing' | i18n }} - {{ getText(installingInfo.progress.overall) }} + {{ getText(installingInfo.progress.overall) | i18n }}

} @else {

- {{ text }} - @if (text === 'Action Required') { - See below + {{ text | i18n }} + @if (text === 'Task Required') { + {{ 'See below' | i18n }} } @if (rendering?.showDots) { @@ -90,7 +96,7 @@ import { host: { class: 'g-card' }, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiLoader], + imports: [TuiLoader, i18nPipe], }) export class ServiceStatusComponent { @Input({ required: true }) @@ -102,8 +108,10 @@ export class ServiceStatusComponent { @Input() connected = false - get text() { - return this.connected ? this.rendering?.display : 'Unknown' + private readonly i18n = inject(i18nPipe) + + get text(): i18nKey { + return this.connected ? this.rendering?.display || 'Unknown' : 'Unknown' } get class(): string | null { @@ -127,7 +135,7 @@ export class ServiceStatusComponent { return this.status && PrimaryRendering[this.status] } - getText(progress: T.Progress): string { + getText(progress: T.Progress): i18nKey { return getProgressText(progress) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts index 467fda1fa..2aaaa9d64 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts @@ -15,6 +15,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { getManifest } from 'src/app/utils/get-package-data' import { UILaunchComponent } from './ui.component' +import { i18nPipe } from '@start9labs/shared' const RUNNING = ['running', 'starting', 'restarting'] @@ -28,7 +29,7 @@ const RUNNING = ['running', 'starting', 'restarting'] iconStart="@tui.square" (click)="controls.stop(manifest())" > - Stop + {{ 'Stop' | i18n }} } @else { } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiButton, UILaunchComponent, TuiLet, AsyncPipe], + imports: [TuiButton, UILaunchComponent, TuiLet, AsyncPipe, i18nPipe], providers: [tuiButtonOptionsProvider({ size: 's', appearance: 'none' })], styles: ` :host { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts index f134f8e66..01c92dbb0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts @@ -9,20 +9,29 @@ import { TitleDirective } from 'src/app/services/title.service' import { getManifest } from 'src/app/utils/get-package-data' import { ServiceComponent } from './service.component' import { ServicesService } from './services.service' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, template: ` - Services + {{ 'Services' | i18n }}

NameTypeDescriptionHosting{{ 'Name' | i18n }}{{ 'Type' | i18n }}{{ 'Description' | i18n }}{{ 'Hosting' | i18n }}
- - - - - + + + + + @@ -35,7 +44,11 @@ import { ServicesService } from './services.service' } @empty { } @@ -54,7 +67,13 @@ import { ServicesService } from './services.service' } `, host: { class: 'g-page' }, - imports: [ServiceComponent, ToManifestPipe, TuiTable, TitleDirective], + imports: [ + ServiceComponent, + ToManifestPipe, + TuiTable, + TitleDirective, + i18nPipe, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export default class DashboardComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts index de2a77797..a95406655 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { i18nKey, i18nPipe } from '@start9labs/shared' import { tuiPure } from '@taiga-ui/cdk' import { TuiIcon, TuiLoader } from '@taiga-ui/core' import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' @@ -18,7 +24,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' } } - {{ status }} + {{ status | i18n }}{{ dots }} `, styles: ` :host { @@ -41,7 +47,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiIcon, TuiLoader], + imports: [TuiIcon, TuiLoader, i18nPipe], }) export class StatusComponent { @Input() @@ -50,6 +56,8 @@ export class StatusComponent { @Input() hasDepErrors = false + private readonly i18n = inject(i18nPipe) + get healthy(): boolean { return !this.hasDepErrors && this.getStatus(this.pkg).health !== 'failure' } @@ -63,9 +71,9 @@ export class StatusComponent { return renderPkgStatus(pkg, {}) } - get status(): string { + get status(): i18nKey { if (this.pkg.stateInfo.installingInfo) { - return `Installing...${getProgressText(this.pkg.stateInfo.installingInfo.progress.overall)}` + return `${this.i18n.transform('Installing')}...${this.i18n.transform(getProgressText(this.pkg.stateInfo.installingInfo.progress.overall))}` as i18nKey } switch (this.getStatus(this.pkg).primary) { @@ -74,26 +82,41 @@ export class StatusComponent { case 'stopped': return 'Stopped' case 'actionRequired': - return 'Action Required' + return 'Task Required' case 'updating': - return 'Updating...' + return 'Updating' case 'stopping': - return 'Stopping...' + return 'Stopping' case 'starting': - return 'Starting...' + return 'Starting' case 'backingUp': - return 'Backing Up...' + return 'Backing Up' case 'restarting': - return 'Restarting...' + return 'Restarting' case 'removing': - return 'Removing...' + return 'Removing' case 'restoring': - return 'Restoring...' + return 'Restoring' default: return 'Unknown' } } + get dots(): '...' | '' { + switch (this.getStatus(this.pkg).primary) { + case 'updating': + case 'stopping': + case 'starting': + case 'backingUp': + case 'restarting': + case 'removing': + case 'restoring': + return '...' + default: + return '' + } + } + get color(): string { switch (this.getStatus(this.pkg).primary) { case 'running': diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui.component.ts index 6053ce806..45f0fbba8 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui.component.ts @@ -4,6 +4,7 @@ import { inject, Input, } from '@angular/core' +import { i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { tuiPure } from '@taiga-ui/cdk' import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core' @@ -22,7 +23,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' [disabled]="!isRunning" [tuiDropdown]="content" > - Launch UI + {{ 'Launch UI' | i18n }} @@ -56,7 +57,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' } `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiButton, TuiDropdown, TuiDataList], + imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe], }) export class UILaunchComponent { private readonly config = inject(ConfigService) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts index 3239bef05..a0abb549c 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts @@ -1,15 +1,18 @@ import { AsyncPipe } from '@angular/common' import { Component, inject } from '@angular/core' -import { getErrorMessage } from '@start9labs/shared' +import { + DialogService, + getErrorMessage, + i18nKey, + i18nPipe, +} from '@start9labs/shared' import { T, utils } from '@start9labs/start-sdk' import { TuiButton, TuiDialogContext, - TuiDialogService, TuiLoader, TuiNotification, } from '@taiga-ui/core' -import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' import { injectContext } from '@taiga-ui/polymorpheus' import * as json from 'fast-json-patch' import { compare } from 'fast-json-patch' @@ -78,7 +81,7 @@ export type PackageActionData = { type="reset" [style.margin-right]="'auto'" > - Reset Defaults + {{ 'Reset defaults' | i18n }} } @else { @@ -113,16 +116,18 @@ export type PackageActionData = { TuiButton, ActionRequestInfoComponent, FormComponent, + i18nPipe, ], providers: [InvalidService], }) export class ActionInputModal { - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly api = inject(ApiService) private readonly patch = inject>(PatchDB) private readonly actionService = inject(ActionService) private readonly context = injectContext>() + private readonly i18n = inject(i18nPipe) readonly actionId = this.context.data.actionInfo.id readonly warning = this.context.data.actionInfo.metadata.warning @@ -198,15 +203,18 @@ export class ActionInputModal { if (!breakages.length) return true - const message = - 'As a result of this change, the following services will no longer work properly and may crash:
    ' + const message = `${this.i18n.transform('As a result of this change, the following services will no longer work properly and may crash')}:
      ` const content = `${message}${breakages.map( id => `
    • ${getManifest(packages[id]!).title}
    • `, - )}
    ` - const data: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' } + )}
` as i18nKey return firstValueFrom( - this.dialogs.open(TUI_CONFIRM, { data }).pipe(endWith(false)), + this.dialog + .openConfirm({ + label: 'Warning', + data: { content, yes: 'Continue', no: 'Cancel' }, + }) + .pipe(endWith(false)), ) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-group.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-group.component.ts index 53b2a15d1..655975129 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-group.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-group.component.ts @@ -1,5 +1,4 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { TuiTitle } from '@taiga-ui/core' import { TuiAccordion, TuiFade } from '@taiga-ui/kit' import { ActionSuccessMemberComponent } from './action-success-member.component' import { GroupResult } from './types' @@ -36,7 +35,7 @@ import { GroupResult } from './types' `, ], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TuiTitle, ActionSuccessMemberComponent, TuiAccordion, TuiFade], + imports: [ActionSuccessMemberComponent, TuiAccordion, TuiFade], }) export class ActionSuccessGroupComponent { @Input() diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-member.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-member.component.ts index 8f61fcb90..2dfaa23b5 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-member.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-member.component.ts @@ -8,8 +8,9 @@ import { ViewChild, } from '@angular/core' import { FormsModule } from '@angular/forms' +import { DialogService, i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -import { TuiButton, TuiDialogService, TuiTitle } from '@taiga-ui/core' +import { TuiButton, TuiTitle } from '@taiga-ui/core' import { TuiInputModule, TuiTextfieldComponent, @@ -50,7 +51,7 @@ import { QrCodeModule } from 'ng-qrcode' [style.pointer-events]="'auto'" (click)="masked = !masked" > - Reveal/Hide + {{ 'Reveal/Hide' | i18n }} } @if (member.copyable) { @@ -64,7 +65,7 @@ import { QrCodeModule } from 'ng-qrcode' [style.pointer-events]="'auto'" (click)="copy()" > - Copy + {{ 'Copy' | i18n }} } @if (member.qr) { @@ -78,7 +79,7 @@ import { QrCodeModule } from 'ng-qrcode' [style.pointer-events]="'auto'" (click)="show(qr)" > - Show QR + {{ 'Show QR' | i18n }} }
@@ -96,7 +97,7 @@ import { QrCodeModule } from 'ng-qrcode' [style.border-radius.%]="100" (click)="masked = false" > - Reveal + {{ 'Reveal' | i18n }} } @@ -123,12 +124,13 @@ import { QrCodeModule } from 'ng-qrcode' TuiButton, QrCodeModule, TuiTitle, + i18nPipe, ], }) export class ActionSuccessMemberComponent { @ViewChild(TuiTextfieldComponent, { read: ElementRef }) private readonly input!: ElementRef - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) @Input() member!: T.ActionResultMember & { type: 'single' } @@ -149,8 +151,8 @@ export class ActionSuccessMemberComponent { const masked = this.masked this.masked = this.member.masked - this.dialogs - .open(template, { label: 'Scan this QR', size: 's' }) + this.dialog + .openComponent(template, { label: 'Scan this QR', size: 's' }) .subscribe({ complete: () => (this.masked = masked), }) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-single.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-single.component.ts index b1ddc7487..1843906e5 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-single.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-success/action-success-single.component.ts @@ -15,6 +15,7 @@ import { } from '@taiga-ui/legacy' import { QrCodeModule } from 'ng-qrcode' import { SingleResult } from './types' +import { i18nPipe } from '@start9labs/shared' @Component({ standalone: true, @@ -45,7 +46,7 @@ import { SingleResult } from './types' [style.pointer-events]="'auto'" (click)="masked = !masked" > - Reveal/Hide + {{ 'Reveal/Hide' | i18n }} } @if (single.copyable) { @@ -59,7 +60,7 @@ import { SingleResult } from './types' [style.pointer-events]="'auto'" (click)="copy()" > - Copy + {{ 'Copy' | i18n }} } @@ -77,7 +78,7 @@ import { SingleResult } from './types' [style.border-radius.%]="100" (click)="masked = false" > - Reveal + {{ 'Reveal' | i18n }} } @@ -104,6 +105,7 @@ import { SingleResult } from './types' TuiTextfieldControllerModule, TuiButton, QrCodeModule, + i18nPipe, ], }) export class ActionSuccessSingleComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts b/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts index 6816af944..5c044128e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/pipes/install-progress.pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' +import { i18nKey } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' @Pipe({ @@ -13,11 +14,11 @@ export class InstallingProgressPipe implements PipeTransform { } } -export function getProgressText(progress: T.Progress): string { +export function getProgressText(progress: T.Progress): i18nKey { if (progress === true) return 'finalizing' if (!progress || !progress.total) return 'unknown %' const percentage = Math.round((100 * progress.done) / progress.total) - return percentage < 99 ? `${percentage}%` : 'finalizing' + return percentage < 99 ? (`${percentage}%` as i18nKey) : 'finalizing' } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts index fb21726cd..45f90da64 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts @@ -5,8 +5,14 @@ import { INJECTOR, } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { CopyService, getPkgId, MarkdownComponent } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' +import { + CopyService, + DialogService, + getPkgId, + i18nKey, + i18nPipe, + MarkdownComponent, +} from '@start9labs/shared' import { TuiCell } from '@taiga-ui/layout' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' @@ -15,25 +21,24 @@ import { DataModel } from 'src/app/services/patch-db/data-model' import { getManifest } from 'src/app/utils/get-package-data' import { AdditionalItem, - FALLBACK_URL, + NOT_PROVIDED, ServiceAdditionalItemComponent, } from '../components/additional-item.component' -import { KeyValuePipe } from '@angular/common' @Component({ template: ` - @for (group of items() | keyvalue; track $index) { + @for (group of groups(); track $index) {
-
{{ group.key }}
- @for (additional of group.value; track $index) { - @if (additional.description.startsWith('http')) { - +
{{ group.header | i18n }}
+ @for (item of group.items; track $index) { + @if (item.value.startsWith('http')) { + } @else { } } @@ -51,71 +56,77 @@ import { KeyValuePipe } from '@angular/common' host: { class: 'g-subpage' }, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [ServiceAdditionalItemComponent, TuiCell, KeyValuePipe], + imports: [ServiceAdditionalItemComponent, TuiCell, i18nPipe], }) export default class ServiceAboutRoute { private readonly copyService = inject(CopyService) - private readonly markdown = inject(TuiDialogService).open( + private readonly markdown = inject(DialogService).openComponent( new PolymorpheusComponent(MarkdownComponent, inject(INJECTOR)), { label: 'License', size: 'l' }, ) - readonly items = toSignal>( + readonly groups = toSignal<{ header: i18nKey; items: AdditionalItem[] }[]>( inject>(PatchDB) .watch$('packageData', getPkgId()) .pipe( map(pkg => { const manifest = getManifest(pkg) - return { - General: [ - { - name: 'Version', - description: manifest.version, - icon: '@tui.copy', - action: () => this.copyService.copy(manifest.version), - }, - { - name: 'Git Hash', - description: manifest.gitHash || 'Unknown', - icon: manifest.gitHash ? '@tui.copy' : '', - action: () => - manifest.gitHash && this.copyService.copy(manifest.gitHash), - }, - { - name: 'License', - description: manifest.license, - icon: '@tui.chevron-right', - action: () => this.markdown.subscribe(), - }, - ], - Links: [ - { - name: 'Installed From', - description: pkg.registry || FALLBACK_URL, - }, - { - name: 'Service Repository', - description: manifest.upstreamRepo, - }, - { - name: 'Package Repository', - description: manifest.wrapperRepo, - }, - { - name: 'Marketing Site', - description: manifest.marketingSite || FALLBACK_URL, - }, - { - name: 'Support Site', - description: manifest.supportSite || FALLBACK_URL, - }, - { - name: 'Donation Link', - description: manifest.donationUrl || FALLBACK_URL, - }, - ], - } + return [ + { + header: 'General', + items: [ + { + name: 'Version', + value: manifest.version, + icon: '@tui.copy', + action: () => this.copyService.copy(manifest.version), + }, + { + name: 'Git Hash', + value: manifest.gitHash || 'Unknown', + icon: manifest.gitHash ? '@tui.copy' : '', + action: () => + manifest.gitHash && this.copyService.copy(manifest.gitHash), + }, + { + name: 'License', + value: manifest.license, + icon: '@tui.chevron-right', + action: () => this.markdown.subscribe(), + }, + ], + }, + { + header: 'Links', + items: [ + { + name: 'Installed From', + value: pkg.registry || NOT_PROVIDED, + }, + { + name: 'Service Repository', + value: manifest.upstreamRepo, + }, + { + name: 'Package Repository', + value: manifest.wrapperRepo, + }, + { + name: 'Marketing Site', + value: manifest.marketingSite || NOT_PROVIDED, + }, + { + name: 'Support Site', + value: manifest.supportSite || NOT_PROVIDED, + }, + { + name: 'Donation Link', + value: manifest.donationUrl || NOT_PROVIDED, + }, + ], + }, + ] }), ), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts index 532ed0b7c..6a9967bda 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts @@ -1,7 +1,7 @@ import { KeyValuePipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' -import { getPkgId } from '@start9labs/shared' +import { getPkgId, i18nPipe } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { TuiCell } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' @@ -34,7 +34,7 @@ const OTHER = 'Custom Actions' } }
-
Standard Actions
+
{{ 'Standard Actions' | i18n }}
} @else { @@ -43,23 +43,25 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils' (ngModelChange)="onFile($event)" /> - @if (error()) { + @if (error(); as err) {
-

{{ error() }}

- +

{{ err | i18n }}

+
} @else {
-

Upload .s9pk package file

+

{{ 'Upload .s9pk package file' | i18n }}

@if (isTor) {

- Warning: package upload will be slow over Tor. Switch to local - for a better experience. + {{ + 'Warning: package upload will be slow over Tor. Switch to local for a better experience.' + | i18n + }}

} - +
}
@@ -90,6 +92,7 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils' TuiButton, SideloadPackageComponent, TitleDirective, + i18nPipe, ], }) export default class SideloadComponent { @@ -97,12 +100,12 @@ export default class SideloadComponent { file: File | null = null readonly package = signal(null) - readonly error = signal('') + readonly error = signal(null) clear() { this.file = null this.package.set(null) - this.error.set('') + this.error.set(null) } async onFile(file: File | null) { @@ -111,6 +114,6 @@ export default class SideloadComponent { const parsed = file ? await validateS9pk(file) : '' this.package.set(tuiIsString(parsed) ? null : parsed) - this.error.set(tuiIsString(parsed) ? parsed : '') + this.error.set(tuiIsString(parsed) ? (parsed as i18nKey) : null) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts index 73a855f86..ad1e76901 100644 --- a/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts +++ b/web/projects/ui/src/app/routes/portal/routes/sideload/sideload.utils.ts @@ -1,4 +1,5 @@ import { MarketplacePkgBase } from '@start9labs/marketplace' +import { i18nKey } from '@start9labs/shared' import { S9pk, ExtendedVersion } from '@start9labs/start-sdk' const MAGIC = new Uint8Array([59, 59]) @@ -7,31 +8,27 @@ const VERSION_2 = new Uint8Array([2]) export async function validateS9pk( file: File, -): Promise { - const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) - const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) +): Promise { + try { + const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) + const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) - if (compare(magic, MAGIC)) { - try { + if (compare(magic, MAGIC)) { if (compare(version, VERSION_1)) { return 'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.' } else if (compare(version, VERSION_2)) { return await parseS9pk(file) } else { console.error(version) - return 'Invalid package file' } - } catch (e) { - console.error(e) - - return e instanceof Error - ? `Invalid package file: ${e.message}` - : 'Invalid package file' + } else { + return 'Invalid package file' } + } catch (e) { + console.error(e) + return 'Invalid package file' } - - return 'Invalid package file' } async function parseS9pk(file: File): Promise { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts index 5e947f810..629f158b4 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/acme/acme.component.ts @@ -1,15 +1,9 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { RouterLink } from '@angular/router' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared' import { ISB, utils } from '@start9labs/start-sdk' -import { - TuiButton, - TuiIcon, - TuiLink, - TuiLoader, - TuiTitle, -} from '@taiga-ui/core' +import { TuiButton, TuiLink, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiCell, TuiHeader } from '@taiga-ui/layout' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' @@ -24,15 +18,19 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' @Component({ template: ` - Back + + {{ 'Back' | i18n }} + ACME

ACME

- Add ACME providers in order to generate SSL (https) certificates for - clearnet access. + {{ + 'Add ACME providers in order to generate SSL (https) certificates for clearnet access.' + | i18n + }}

- Saved Providers + {{ 'Saved Providers' | i18n }} @if (acme(); as value) { }
@@ -66,7 +64,9 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
{{ toAcmeName(provider.url) }} - Contact: {{ provider.contactString }} + + {{ 'Contact' | i18n }}: {{ provider.contactString }} +
} @@ -107,6 +107,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' TuiLink, RouterLink, TitleDirective, + i18nPipe, ], }) export default class SystemAcmeComponent { @@ -115,6 +116,7 @@ export default class SystemAcmeComponent { private readonly errorService = inject(ErrorService) private readonly patch = inject>(PatchDB) private readonly api = inject(ApiService) + private readonly i18n = inject(i18nPipe) acme = toSignal( this.patch.watch$('serverInfo', 'network', 'acme').pipe( @@ -146,13 +148,13 @@ export default class SystemAcmeComponent { label: 'Add ACME Provider', data: { spec: await configBuilderToSpec( - getAddAcmeSpec(providers.map(p => p.url)), + this.addAcmeSpec(providers.map(p => p.url)), ), buttons: [ { - text: 'Save', + text: this.i18n.transform('Save'), handler: async ( - val: ReturnType['_TYPE'], + val: ReturnType['_TYPE'], ) => { const providerUrl = val.provider.selection === 'other' @@ -171,12 +173,13 @@ export default class SystemAcmeComponent { this.formDialog.open(FormComponent, { label: 'Edit ACME Provider', data: { - spec: await configBuilderToSpec(editAcmeSpec), + spec: await configBuilderToSpec(this.editAcmeSpec()), buttons: [ { - text: 'Save', - handler: async (val: typeof editAcmeSpec._TYPE) => - this.saveAcme(provider, val.contact), + text: this.i18n.transform('Save'), + handler: async ( + val: ReturnType['_TYPE'], + ) => this.saveAcme(provider, val.contact), }, ], value: { contact }, @@ -213,58 +216,68 @@ export default class SystemAcmeComponent { loader.unsubscribe() } } -} -const emailListSpec = ISB.Value.list( - ISB.List.text( - { - name: 'Contact Emails', - description: - 'Needed to obtain a certificate from a Certificate Authority', - minLength: 1, - }, - { - inputmode: 'email', - patterns: [utils.Patterns.email], - }, - ), -) + private addAcmeSpec(providers: string[]) { + const availableAcme = knownACME.filter( + acme => !providers.includes(acme.url), + ) -function getAddAcmeSpec(providers: string[]) { - const availableAcme = knownACME.filter(acme => !providers.includes(acme.url)) - - return ISB.InputSpec.of({ - provider: ISB.Value.union( - { name: 'Provider', default: (availableAcme[0]?.url as any) || 'other' }, - ISB.Variants.of({ - ...availableAcme.reduce( - (obj, curr) => ({ - ...obj, - [curr.url]: { - name: curr.name, - spec: ISB.InputSpec.of({}), - }, - }), - {}, - ), - other: { - name: 'Other', - spec: ISB.InputSpec.of({ - url: ISB.Value.text({ - name: 'URL', - default: null, - required: true, - inputmode: 'url', - patterns: [utils.Patterns.url], - }), - }), + return ISB.InputSpec.of({ + provider: ISB.Value.union( + { + name: 'Provider', + default: (availableAcme[0]?.url as any) || 'other', }, - }), - ), - contact: emailListSpec, - }) -} + ISB.Variants.of({ + ...availableAcme.reduce( + (obj, curr) => ({ + ...obj, + [curr.url]: { + name: curr.name, + spec: ISB.InputSpec.of({}), + }, + }), + {}, + ), + other: { + name: 'Other', + spec: ISB.InputSpec.of({ + url: ISB.Value.text({ + name: 'URL', + default: null, + required: true, + inputmode: 'url', + patterns: [utils.Patterns.url], + }), + }), + }, + }), + ), + contact: this.emailListSpec(), + }) + } -const editAcmeSpec = ISB.InputSpec.of({ - contact: emailListSpec, -}) + private editAcmeSpec() { + return ISB.InputSpec.of({ + contact: this.emailListSpec(), + }) + } + + private emailListSpec() { + return ISB.Value.list( + ISB.List.text( + { + name: this.i18n.transform('Contact Emails')!, + description: this.i18n.transform( + 'Needed to obtain a certificate from a Certificate Authority', + ), + minLength: 1, + }, + { + inputmode: 'email', + patterns: [utils.Patterns.email], + }, + ), + ) + } +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.component.ts index 5b1efcfd7..76191edea 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.component.ts @@ -1,20 +1,22 @@ import { Component, inject } from '@angular/core' import { FormsModule } from '@angular/forms' import * as argon2 from '@start9labs/argon2' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' +import { + DialogService, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { TuiButton, TuiGroup, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiBlock, TuiCheckbox } from '@taiga-ui/kit' import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PatchDB } from 'patch-db-client' import { firstValueFrom, map } from 'rxjs' -import { PROMPT } from 'src/app/routes/portal/modals/prompt.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { getManifest } from 'src/app/utils/get-package-data' import { getServerInfo } from 'src/app/utils/get-server-info' import { verifyPassword } from 'src/app/utils/verify-password' -import { PASSWORD_OPTIONS } from './backup.const' import { BackupService } from './backup.service' import { BackupContext } from './backup.types' @@ -43,7 +45,7 @@ interface Package { /> } @empty { - No services installed! + {{ 'No services installed' | i18n }} } } @else { @@ -51,10 +53,10 @@ interface Package {
`, @@ -84,10 +86,11 @@ interface Package { TuiBlock, TuiCheckbox, TuiTitle, + i18nPipe, ], }) export class BackupsBackupComponent { - private readonly dialogs = inject(TuiResponsiveDialogService) + private readonly dialog = inject(DialogService) private readonly loader = inject(LoadingService) private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) @@ -126,8 +129,17 @@ export class BackupsBackupComponent { const { passwordHash, id } = await getServerInfo(this.patch) const { entry } = this.context.data - this.dialogs - .open(PROMPT, PASSWORD_OPTIONS) + this.dialog + .openPrompt({ + label: 'Master Password Needed', + data: { + message: 'Enter your master password to encrypt this backup.', + label: 'Master Password', + placeholder: 'Enter master password', + useMask: true, + buttonText: 'Create Backup', + }, + }) .pipe(verifyPassword(passwordHash, e => this.errorService.handleError(e))) .subscribe(async password => { // first time backup @@ -158,8 +170,18 @@ export class BackupsBackupComponent { const { id } = await getServerInfo(this.patch) const { passwordHash = '' } = this.context.data.entry.startOs[id] || {} - this.dialogs - .open(PROMPT, PASSWORD_OPTIONS) + this.dialog + .openPrompt({ + label: 'Original Password Needed', + data: { + message: + 'This backup was created with a different password. Enter the original password that was used to encrypt this backup.', + label: 'Original Password', + placeholder: 'Enter original password', + useMask: true, + buttonText: 'Create Backup', + }, + }) .pipe(verifyPassword(passwordHash, e => this.errorService.handleError(e))) .subscribe(oldPassword => this.createBackup(password, oldPassword)) } @@ -168,7 +190,7 @@ export class BackupsBackupComponent { password: string, oldPassword: string | null = null, ) { - const loader = this.loader.open('Beginning backup...').subscribe() + const loader = this.loader.open('Beginning backup').subscribe() const packageIds = this.pkgs?.filter(p => p.checked).map(p => p.id) || [] const params = { targetId: this.context.data.id, diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.const.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.const.ts deleted file mode 100644 index b94c61f94..000000000 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backup.const.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { TuiDialogOptions } from '@taiga-ui/core' -import { PromptOptions } from 'src/app/routes/portal/modals/prompt.component' - -export const PASSWORD_OPTIONS: Partial> = { - label: 'Master Password Needed', - data: { - message: 'Enter your master password to encrypt this backup.', - label: 'Master Password', - placeholder: 'Enter master password', - useMask: true, - buttonText: 'Create Backup', - }, -} - -export const OLD_OPTIONS: Partial> = { - label: 'Original Password Needed', - data: { - message: - 'This backup was created with a different password. Enter the ORIGINAL password that was used to encrypt this backup.', - label: 'Original Password', - placeholder: 'Enter original password', - useMask: true, - buttonText: 'Create Backup', - }, -} - -export const RESTORE_OPTIONS: Partial> = { - label: 'Password Required', - data: { - message: `Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.`, - label: 'Master Password', - placeholder: 'Enter master password', - useMask: true, - }, -} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts index e095d7947..e12091604 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/backups.component.ts @@ -7,8 +7,11 @@ import { } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { ActivatedRoute, RouterLink } from '@angular/router' -import { UnitConversionPipesModule } from '@start9labs/shared' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' +import { + DialogService, + i18nPipe, + UnitConversionPipesModule, +} from '@start9labs/shared' import { TuiMapperPipe } from '@taiga-ui/cdk' import { TuiButton, @@ -36,17 +39,29 @@ import { BACKUP_RESTORE } from './restore.component' @Component({ template: ` - Back - {{ type === 'create' ? 'Create Backup' : 'Restore Backup' }} + + {{ 'Back' | i18n }} + + {{ + type === 'create' ? ('Create Backup' | i18n) : ('Restore Backup' | i18n) + }}
-

{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}

+

+ {{ + type === 'create' + ? ('Create Backup' | i18n) + : ('Restore Backup' | i18n) + }} +

@if (type === 'create') { - Back up StartOS and service data by connecting to a device on your - local network or a physical drive connected to your server. + {{ + 'Back up StartOS and service data by connecting to a device on your local network or a physical drive connected to your server.' + | i18n + }} } @else { - Restore StartOS and service data from a device on your local network - or a physical drive connected to your server that contains an - existing backup. + {{ + 'Restore StartOS and service data from a device on your local network or a physical drive connected to your server that contains an existing backup.' + | i18n + }} }

@@ -79,7 +95,7 @@ import { BACKUP_RESTORE } from './restore.component' @if (type === 'create' && server(); as s) {
- Last Backup + {{ 'Last Backup' | i18n }}
{{ s.lastBackup ? (s.lastBackup | date: 'medium') : 'never' }}
@@ -98,19 +114,26 @@ import { BACKUP_RESTORE } from './restore.component' /> } @else {
- A folder on another computer that is connected to the same network as - your Start9 server. View the + {{ + 'A folder on another computer that is connected to the same network as your Start9 server.' + | i18n + }}
- A physical drive that is plugged directly into your Start9 Server. + {{ + 'A physical drive that is plugged directly into your Start9 Server.' + | i18n + }}
} } @@ -133,10 +156,11 @@ import { BACKUP_RESTORE } from './restore.component' BackupNetworkComponent, BackupPhysicalComponent, BackupProgressComponent, + i18nPipe, ], }) export default class SystemBackupComponent implements OnInit { - readonly dialogs = inject(TuiResponsiveDialogService) + readonly dialog = inject(DialogService) readonly type = inject(ActivatedRoute).snapshot.data['type'] readonly service = inject(BackupService) readonly eos = inject(EOSService) @@ -172,6 +196,6 @@ export default class SystemBackupComponent implements OnInit { ? 'Select Services to Back Up' : 'Select server backup' - this.dialogs.open(component, { label, data: target }).subscribe() + this.dialog.openComponent(component, { label, data: target }).subscribe() } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts index d8a461e53..010fdf4bf 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts @@ -5,11 +5,15 @@ import { output, } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { + DialogService, + ErrorService, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { ISB } from '@start9labs/start-sdk' -import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' -import { TuiAlertService, TuiButton, TuiIcon } from '@taiga-ui/core' -import { TUI_CONFIRM, TuiTooltip } from '@taiga-ui/kit' +import { TuiButton, TuiIcon } from '@taiga-ui/core' +import { TuiTooltip } from '@taiga-ui/kit' import { filter } from 'rxjs' import { FormComponent } from 'src/app/routes/portal/components/form.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' @@ -29,15 +33,15 @@ const ERROR = selector: '[networkFolders]', template: `
- Network Folders + {{ 'Network Folders' | i18n }}
-
NameVersionUptimeStatusControls + {{ 'Name' | i18n }} + {{ 'Version' | i18n }} + {{ 'Uptime' | i18n }} + + {{ 'Status' | i18n }} + + {{ 'Controls' | i18n }} +
- {{ services() ? 'No services installed' : 'Loading...' }} + {{ + services() + ? ('No services installed' | i18n) + : ('Loading' | i18n) + }}
+
@for (target of service.cifs(); track $index) { >() select(target: MappedBackupTarget) { if (!target.entry.mountable) { - this.alerts - .open(ERROR, { + this.dialog + .openAlert(ERROR, { appearance: 'negative', label: 'Unable to connect', autoClose: 0, }) .subscribe() } else if (this.type === 'restore' && !target.hasAnyBackup) { - this.alerts - .open('Network Folder does not contain a valid backup', { + this.dialog + .openAlert('Network Folder does not contain a valid backup', { appearance: 'negative', }) .subscribe() @@ -201,10 +206,10 @@ export class BackupNetworkComponent { this.formDialog.open(FormComponent, { label: 'New Network Folder', data: { - spec: await configBuilderToSpec(cifsSpec), + spec: await configBuilderToSpec(this.cifsSpec()), buttons: [ { - text: 'Execute', + text: this.i18n.transform('Connect'), handler: (value: RR.AddBackupTargetReq) => this.addTarget(value), }, ], @@ -216,13 +221,13 @@ export class BackupNetworkComponent { this.formDialog.open(FormComponent, { label: 'Update Network Folder', data: { - spec: await configBuilderToSpec(cifsSpec), + spec: await configBuilderToSpec(this.cifsSpec()), buttons: [ { - text: 'Execute', + text: this.i18n.transform('Connect'), handler: async (value: RR.AddBackupTargetReq) => { const loader = this.loader - .open('Testing connectivity to shared folder...') + .open('Testing connectivity to shared folder') .subscribe() try { @@ -249,11 +254,11 @@ export class BackupNetworkComponent { } forget({ id }: MappedBackupTarget, index: number) { - this.dialogs - .open(TUI_CONFIRM, { label: 'Are you sure?', size: 's' }) + this.dialog + .openConfirm({ label: 'Are you sure?', size: 's' }) .pipe(filter(Boolean)) .subscribe(async () => { - const loader = this.loader.open('Removing...').subscribe() + const loader = this.loader.open('Removing').subscribe() try { await this.api.removeBackupTarget({ id }) @@ -268,7 +273,7 @@ export class BackupNetworkComponent { private async addTarget(v: RR.AddBackupTargetReq): Promise { const loader = this.loader - .open('Testing connectivity to shared folder...') + .open('Testing connectivity to shared folder') .subscribe() try { @@ -290,39 +295,48 @@ export class BackupNetworkComponent { loader.unsubscribe() } } -} -const cifsSpec = ISB.InputSpec.of({ - hostname: ISB.Value.text({ - name: 'Hostname', - description: - 'The hostname of your target device on the Local Area Network.', - warning: null, - placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, - required: true, - default: null, - patterns: [], - }), - path: ISB.Value.text({ - name: 'Path', - description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, - placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', - required: true, - default: null, - }), - username: ISB.Value.text({ - name: 'Username', - description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, - required: true, - default: null, - placeholder: 'My Network Folder', - }), - password: ISB.Value.text({ - name: 'Password', - description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, - required: false, - default: null, - masked: true, - placeholder: 'My Network Folder', - }), -}) + cifsSpec() { + return ISB.InputSpec.of({ + hostname: ISB.Value.text({ + name: this.i18n.transform('Hostname')!, + description: this.i18n.transform( + 'The hostname of your target device on the Local Area Network.', + ), + warning: null, + placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, + required: true, + default: null, + patterns: [], + }), + path: ISB.Value.text({ + name: this.i18n.transform('Path')!, + description: this.i18n.transform( + 'On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder). On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).', + ), + placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', + required: true, + default: null, + }), + username: ISB.Value.text({ + name: this.i18n.transform('Username')!, + description: this.i18n.transform( + 'On Linux, this is the samba username you created when sharing the folder. On Mac and Windows, this is the username of the user who is sharing the folder.', + ), + required: true, + default: null, + placeholder: 'My Network Folder', + }), + password: ISB.Value.text({ + name: this.i18n.transform('Password')!, + description: this.i18n.transform( + 'On Linux, this is the samba password you created when sharing the folder. On Mac and Windows, this is the password of the user who is sharing the folder.', + ), + required: false, + default: null, + masked: true, + placeholder: 'My Network Folder', + }), + }) + } +} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts index 86a1424f4..36fac8734 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts @@ -5,8 +5,12 @@ import { output, } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { UnitConversionPipesModule } from '@start9labs/shared' -import { TuiAlertService, TuiButton, TuiIcon } from '@taiga-ui/core' +import { + DialogService, + i18nPipe, + UnitConversionPipesModule, +} from '@start9labs/shared' +import { TuiButton, TuiIcon } from '@taiga-ui/core' import { TuiTooltip } from '@taiga-ui/kit' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { TableComponent } from 'src/app/routes/portal/components/table.component' @@ -19,7 +23,7 @@ import { BackupStatusComponent } from './status.component' selector: '[physicalFolders]', template: `
- Physical Drives + {{ 'Physical Drives' | i18n }}
@@ -45,13 +49,13 @@ import { BackupStatusComponent } from './status.component'
@@ -115,10 +119,11 @@ import { BackupStatusComponent } from './status.component' PlaceholderComponent, BackupStatusComponent, TableComponent, + i18nPipe, ], }) export class BackupPhysicalComponent { - private readonly alerts = inject(TuiAlertService) + private readonly dialog = inject(DialogService) private readonly type = inject(ActivatedRoute).snapshot.data['type'] readonly service = inject(BackupService) @@ -126,8 +131,8 @@ export class BackupPhysicalComponent { select(target: MappedBackupTarget) { if (this.type === 'restore' && !target.hasAnyBackup) { - this.alerts - .open('Drive partition does not contain a valid backup', { + this.dialog + .openAlert('Drive partition does not contain a valid backup', { appearance: 'negative', }) .subscribe() diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts index 5afbf80c3..98828b810 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts @@ -1,6 +1,7 @@ import { AsyncPipe, KeyValuePipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { i18nPipe } from '@start9labs/shared' import { TuiMapperPipe } from '@taiga-ui/cdk' import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiAvatar } from '@taiga-ui/kit' @@ -14,7 +15,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' standalone: true, selector: '[backupProgress]', template: ` -
Backup Progress
+
{{ 'Backup Progress' | i18n }}
@for (pkg of pkgs() | keyvalue; track $index) { @if (backupProgress()?.[pkg.key]; as progress) {
@@ -26,13 +27,13 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @if (progress.complete) { - Complete + {{ 'Complete' | i18n }} } @else { @if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') { - Backing up + {{ 'Backing up' | i18n }} } @else { - Waiting... + {{ 'Waiting' | i18n }}... } } @@ -53,6 +54,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' TuiLoader, TuiMapperPipe, ToManifestPipe, + i18nPipe, ], }) export class BackupProgressComponent { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/recover.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/recover.component.ts index cf891145d..758173ba0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/recover.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/recover.component.ts @@ -3,7 +3,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { FormsModule } from '@angular/forms' import { Router } from '@angular/router' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { + ErrorService, + i18nKey, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { Version } from '@start9labs/start-sdk' import { TuiMapperPipe } from '@taiga-ui/cdk' import { TuiButton, TuiDialogContext, TuiGroup, TuiTitle } from '@taiga-ui/core' @@ -24,12 +29,17 @@ import { RecoverData, RecoverOption } from './backup.types'
} @empty { @if (sessions) { - + + + } @else { @for (item of single ? [''] : ['', '']; track $index) { - + } } @@ -138,6 +143,7 @@ import { PlatformInfoPipe } from './platform-info.pipe' TuiFade, TuiSkeleton, TableComponent, + i18nPipe, ], }) export class SessionsTableComponent implements OnChanges { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/table.component.ts index 5124ae8da..cd285b7e0 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/ssh/table.component.ts @@ -24,7 +24,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' selector: '[keys]', template: `
- No drives detected + {{ 'No drives detected' | i18n }}
No sessions
{{ 'No sessions' | i18n }}
Loading
+
{{ 'Loading' | i18n }}
+
@for (key of keys; track $index) { @@ -119,7 +119,7 @@ export class SSHTableComponent { .open(PROMPT, ADD_OPTIONS) .pipe(take(1)) .subscribe(async key => { - const loader = this.loader.open('Saving...').subscribe() + const loader = this.loader.open('Saving').subscribe() try { this.keys?.push(await this.api.addSshKey({ key })) @@ -135,7 +135,7 @@ export class SSHTableComponent { .open(TUI_CONFIRM, DELETE_OPTIONS) .pipe(filter(Boolean)) .subscribe(async () => { - const loader = this.loader.open('Deleting...').subscribe() + const loader = this.loader.open('Deleting').subscribe() try { await this.api.deleteSshKey({ fingerprint: key.fingerprint }) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/table.component.ts index a907027bd..3c33dd9b3 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/table.component.ts @@ -6,8 +6,8 @@ import { inject, Input, } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { TuiButton, TuiDialogOptions, TuiIcon, TuiTitle } from '@taiga-ui/core' +import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared' +import { TuiButton, TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiBadge, TuiFade } from '@taiga-ui/kit' import { TuiCell } from '@taiga-ui/layout' import { @@ -16,8 +16,9 @@ import { } from 'src/app/routes/portal/components/form.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' -import { Wifi, WiFiForm, wifiSpec } from './utils' +import { Wifi, WiFiForm } from './utils' import SystemWifiComponent from './wifi.component' +import { wifiSpec } from './wifi.const' @Component({ selector: '[wifi]', @@ -33,7 +34,9 @@ import SystemWifiComponent from './wifi.component' {{ network.ssid }} @if (network.connected) { - Connected + + {{ 'Connected' | i18n }} + } @@ -45,7 +48,7 @@ import SystemWifiComponent from './wifi.component' iconStart="@tui.trash-2" (click.stop)="forget(network)" > - Forget + {{ 'Forget' | i18n }} } @else { { - const loader = this.loader.open('Deleting...').subscribe() + const loader = this.loader.open('Deleting').subscribe() try { await this.api.deleteWifi({ ssid }) @@ -149,21 +154,19 @@ export class WifiTableComponent { if (!network.security.length) { await this.component.saveAndConnect(network.ssid) } else { - const options: Partial>> = { + this.formDialog.open>(FormComponent, { label: 'Password Needed', data: { spec: wifiSpec.spec, buttons: [ { - text: 'Connect', + text: this.i18n.transform('Connect')!, handler: async ({ ssid, password }) => this.component.saveAndConnect(ssid, password), }, ], }, - } - - this.formDialog.open(FormComponent, options) + }) } } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/utils.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/utils.ts index 8993c9b65..8f3fadd2e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/utils.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/utils.ts @@ -1,4 +1,3 @@ -import { IST } from '@start9labs/start-sdk' import { AvailableWifi } from 'src/app/services/api/api.types' import { RR } from 'src/app/services/api/api.types' @@ -27,52 +26,3 @@ export function parseWifi(res: RR.GetWifiRes): WifiData { })), } } - -export const wifiSpec: IST.ValueSpecObject = { - type: 'object', - name: 'WiFi Credentials', - description: - 'Enter the network SSID and password. You can connect now or save the network for later.', - warning: null, - spec: { - ssid: { - type: 'text', - minLength: null, - maxLength: null, - patterns: [], - name: 'Network SSID', - description: null, - inputmode: 'text', - placeholder: null, - required: true, - masked: false, - default: null, - warning: null, - disabled: false, - immutable: false, - generate: null, - }, - password: { - type: 'text', - minLength: null, - maxLength: null, - patterns: [ - { - regex: '^.{8,}$', - description: 'Must be longer than 8 characters', - }, - ], - name: 'Password', - description: null, - inputmode: 'text', - placeholder: null, - required: true, - masked: true, - default: null, - warning: null, - disabled: false, - immutable: false, - generate: null, - }, - }, -} diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts index 69858b535..62c2683bf 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts @@ -7,13 +7,17 @@ import { import { toSignal } from '@angular/core/rxjs-interop' import { FormsModule } from '@angular/forms' import { RouterLink } from '@angular/router' -import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' +import { + ErrorService, + i18nKey, + i18nPipe, + LoadingService, + pauseFor, +} from '@start9labs/shared' import { TuiAlertService, TuiAppearance, TuiButton, - TuiDialogOptions, - TuiLink, TuiLoader, TuiNotification, TuiTitle, @@ -44,13 +48,12 @@ import { wifiSpec } from './wifi.const'
- Deprecated + {{ 'Deprecated' | i18n }}
- WiFi support will be removed in StartOS v0.4.1. If you do not have - access to Ethernet, you can use a WiFi extender to connect to the - local network, then connect your server to the extender via - Ethernet. Please contact Start9 support with any questions or - concerns. + {{ + 'WiFi support will be removed in StartOS v0.4.1. If you do not have access to Ethernet, you can use a WiFi extender to connect to the local network, then connect your server to the extender via Ethernet. Please contact Start9 support with any questions or concerns.' + | i18n + }}
@@ -70,7 +73,7 @@ import { wifiSpec } from './wifi.const' @if (status()?.enabled) { @if (wifi(); as data) { @if (data.known.length) { -

KNOWN NETWORKS

+

{{ 'Known Networks' | i18n }}

} @if (data.available.length) { -

OTHER NETWORKS

+

{{ 'Other Networks' | i18n }}

}

- +

} @else { } } @else { - WiFi is disabled + + {{ 'WiFi is disabled' | i18n }} + } } @else { - No wireless interface detected + {{ 'No wireless interface detected' | i18n }} } `, @@ -121,8 +128,8 @@ import { wifiSpec } from './wifi.const' PlaceholderComponent, TuiHeader, TuiTitle, - TuiLink, TuiNotification, + i18nPipe, ], }) export default class SystemWifiComponent { @@ -134,13 +141,14 @@ export default class SystemWifiComponent { private readonly formDialog = inject(FormDialogService) private readonly cdr = inject(ChangeDetectorRef) private readonly patch = inject>(PatchDB) + private readonly i18n = inject(i18nPipe) readonly status = toSignal(this.patch.watch$('serverInfo', 'network', 'wifi')) readonly wifi = toSignal(merge(this.getWifi$(), this.update$)) async onToggle(enable: boolean) { const loader = this.loader - .open(enable ? 'Enabling Wifi' : 'Disabling WiFi') + .open(enable ? 'Enabling WiFi' : 'Disabling WiFi') .subscribe() try { @@ -153,31 +161,29 @@ export default class SystemWifiComponent { } other(wifi: WifiData) { - const options: Partial>> = { - label: wifiSpec.name, + this.formDialog.open>(FormComponent, { + label: wifiSpec.name as i18nKey, data: { spec: wifiSpec.spec, buttons: [ { - text: 'Save for Later', + text: this.i18n.transform('Save for later')!, handler: async ({ ssid, password }) => this.save(ssid, password, wifi), }, { - text: 'Save and Connect', + text: this.i18n.transform('Save and connect')!, handler: async ({ ssid, password }) => this.saveAndConnect(ssid, password), }, ], }, - } - - this.formDialog.open(FormComponent, options) + }) } async saveAndConnect(ssid: string, password?: string): Promise { const loader = this.loader - .open('Connecting. This could take a while...') + .open('Connecting. This could take a while') .subscribe() try { @@ -255,7 +261,7 @@ export default class SystemWifiComponent { password: string, wifi: WifiData, ): Promise { - const loader = this.loader.open('Saving...').subscribe() + const loader = this.loader.open('Saving').subscribe() try { await this.api.addWifi({ diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.const.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.const.ts index 80e0e2056..1969c2a86 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.const.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.const.ts @@ -3,8 +3,7 @@ import { IST } from '@start9labs/start-sdk' export const wifiSpec: IST.ValueSpecObject = { type: 'object', name: 'WiFi Credentials', - description: - 'Enter the network SSID and password. You can connect now or save the network for later.', + description: null, warning: null, spec: { ssid: { @@ -28,12 +27,7 @@ export const wifiSpec: IST.ValueSpecObject = { type: 'text', minLength: null, maxLength: null, - patterns: [ - { - regex: '^.{8,}$', - description: 'Must be longer than 8 characters', - }, - ], + patterns: [], name: 'Password', description: null, inputmode: 'text', diff --git a/web/projects/ui/src/app/routes/portal/routes/system/system.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/system.component.ts index 5279b1d56..385f016ed 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/system.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/system.component.ts @@ -1,20 +1,20 @@ +import { AsyncPipe } from '@angular/common' import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' import { RouterModule } from '@angular/router' +import { i18nPipe } from '@start9labs/shared' import { TuiIcon, TuiTitle } from '@taiga-ui/core' import { TuiBadgeNotification } from '@taiga-ui/kit' import { TuiCell } from '@taiga-ui/layout' -import { i18nPipe } from 'src/app/i18n/i18n.pipe' +import { PatchDB } from 'patch-db-client' import { BadgeService } from 'src/app/services/badge.service' +import { DataModel } from 'src/app/services/patch-db/data-model' import { TitleDirective } from 'src/app/services/title.service' import { SYSTEM_MENU } from './system.const' -import { PatchDB } from 'patch-db-client' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { AsyncPipe } from '@angular/common' @Component({ template: ` - {{ 'system.outlet.system' | i18n }} + {{ 'System' | i18n }}
@if ( data()?.marketplace?.[current()?.url || '']?.packages; @@ -111,16 +112,18 @@ interface UpdatesData { /> } @empty { - + } } } @else { - + - + }
All services are up to date! + {{ 'All services are up to date!' | i18n }} +
Loading{{ 'Loading' | i18n }}
Loading{{ 'Loading' | i18n }}
@@ -211,6 +214,7 @@ interface UpdatesData { UpdatesItemComponent, TitleDirective, TableComponent, + i18nPipe, ], }) export default class UpdatesComponent { diff --git a/web/projects/ui/src/app/services/action.service.ts b/web/projects/ui/src/app/services/action.service.ts index 0b416e6de..b71812acd 100644 --- a/web/projects/ui/src/app/services/action.service.ts +++ b/web/projects/ui/src/app/services/action.service.ts @@ -1,7 +1,11 @@ import { inject, Injectable } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { TUI_CONFIRM } from '@taiga-ui/kit' +import { + DialogService, + ErrorService, + i18nKey, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { filter } from 'rxjs' import { @@ -31,10 +35,11 @@ const allowedStatuses = { }) export class ActionService { private readonly api = inject(ApiService) - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly errorService = inject(ErrorService) private readonly loader = inject(LoadingService) private readonly formDialog = inject(FormDialogService) + private readonly i18n = inject(i18nPipe) async present(data: PackageActionData) { const { pkgInfo, actionInfo } = data @@ -46,19 +51,19 @@ export class ActionService { ) { if (actionInfo.metadata.hasInput) { this.formDialog.open(ActionInputModal, { - label: actionInfo.metadata.name, + label: actionInfo.metadata.name as i18nKey, data, }) } else { if (actionInfo.metadata.warning) { - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', data: { no: 'Cancel', yes: 'Run', - content: actionInfo.metadata.warning, + content: actionInfo.metadata.warning as i18nKey, }, }) .pipe(filter(Boolean)) @@ -71,7 +76,6 @@ export class ActionService { const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]] const last = statuses.pop() let statusesStr = statuses.join(', ') - let error = '' if (statuses.length) { if (statuses.length > 1) { // oxford comma @@ -79,18 +83,14 @@ export class ActionService { } statusesStr += ` or ${last}` } else if (last) { - statusesStr = `${last}` - } else { - error = `There is no status for which this action may be run. This is a bug. Please file an issue with the service maintainer.` + statusesStr = last } - this.dialogs - .open( - error || - `Action "${actionInfo.metadata.name}" can only be executed when service is ${statusesStr}`, + this.dialog + .openAlert( + `${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey, { label: 'Forbidden', - size: 's', }, ) .pipe(filter(Boolean)) @@ -99,7 +99,7 @@ export class ActionService { } async execute(packageId: string, actionId: string, input?: object) { - const loader = this.loader.open('Loading...').subscribe() + const loader = this.loader.open('Loading').subscribe() try { const res = await this.api.runAction({ @@ -111,14 +111,16 @@ export class ActionService { if (!res) return if (res.result) { - this.dialogs - .open(new PolymorpheusComponent(ActionSuccessPage), { - label: res.title, + this.dialog + .openComponent(new PolymorpheusComponent(ActionSuccessPage), { + label: res.title as i18nKey, data: res, }) .subscribe() } else if (res.message) { - this.dialogs.open(res.message, { label: res.title }).subscribe() + this.dialog + .openAlert(res.message as i18nKey, { label: res.title as i18nKey }) + .subscribe() } } catch (e: any) { this.errorService.handleError(e) diff --git a/web/projects/ui/src/app/services/controls.service.ts b/web/projects/ui/src/app/services/controls.service.ts index 54185ad27..b46f92c54 100644 --- a/web/projects/ui/src/app/services/controls.service.ts +++ b/web/projects/ui/src/app/services/controls.service.ts @@ -1,17 +1,14 @@ import { inject, Injectable } from '@angular/core' -import { ErrorService, LoadingService } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' -import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' -import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' -import { PatchDB } from 'patch-db-client' import { - defaultIfEmpty, - defer, - filter, - firstValueFrom, - of, - switchMap, -} from 'rxjs' + DialogService, + ErrorService, + i18nKey, + i18nPipe, + LoadingService, +} from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { PatchDB } from 'patch-db-client' +import { defaultIfEmpty, defer, filter, firstValueFrom, of } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { getAllPackages } from 'src/app/utils/get-package-data' @@ -21,23 +18,25 @@ import { hasCurrentDeps } from 'src/app/utils/has-deps' providedIn: 'root', }) export class ControlsService { - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly errorService = inject(ErrorService) private readonly loader = inject(LoadingService) private readonly api = inject(ApiService) private readonly patch = inject>(PatchDB) + private readonly i18n = inject(i18nPipe) async start({ title, alerts, id }: T.Manifest, unmet: boolean) { - const deps = `${title} has unmet dependencies. It will not work as expected.` + const deps = + `${title} ${this.i18n.transform('has unmet dependencies. It will not work as expected.')}` as i18nKey if ( (unmet && !(await this.alert(deps))) || - (alerts.start && !(await this.alert(alerts.start))) + (alerts.start && !(await this.alert(alerts.start as i18nKey))) ) { return } - const loader = this.loader.open(`Starting...`).subscribe() + const loader = this.loader.open('Starting').subscribe() try { await this.api.startPackage({ id }) @@ -49,7 +48,7 @@ export class ControlsService { } async stop({ id, title, alerts }: T.Manifest) { - const depMessage = `Services that depend on ${title} will no longer work properly and may crash` + const depMessage = `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` let content = alerts.stop || '' if (hasCurrentDeps(id, await getAllPackages(this.patch))) { @@ -58,12 +57,20 @@ export class ControlsService { defer(() => content - ? this.dialogs - .open(TUI_CONFIRM, getOptions(content, 'Stop')) + ? this.dialog + .openConfirm({ + label: 'Warning', + size: 's', + data: { + content: content as i18nKey, + yes: 'Stop', + no: 'Cancel', + }, + }) .pipe(filter(Boolean)) : of(null), ).subscribe(async () => { - const loader = this.loader.open(`Stopping...`).subscribe() + const loader = this.loader.open('Stopping').subscribe() try { await this.api.stopPackage({ id }) @@ -77,17 +84,24 @@ export class ControlsService { async restart({ id, title }: T.Manifest) { const packages = await getAllPackages(this.patch) - const options = getOptions( - `Services that depend on ${title} may temporarily experiences issues`, - 'Restart', - ) defer(() => hasCurrentDeps(id, packages) - ? this.dialogs.open(TUI_CONFIRM, options).pipe(filter(Boolean)) + ? this.dialog + .openConfirm({ + label: 'Warning', + size: 's', + data: { + content: + `${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('may temporarily experiences issues')}` as i18nKey, + yes: 'Restart', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) : of(null), ).subscribe(async () => { - const loader = this.loader.open(`Restarting...`).subscribe() + const loader = this.loader.open('Restarting').subscribe() try { await this.api.restartPackage({ id }) @@ -99,26 +113,19 @@ export class ControlsService { }) } - private alert(content: string): Promise { + private alert(content: i18nKey): Promise { return firstValueFrom( - this.dialogs - .open(TUI_CONFIRM, getOptions(content)) + this.dialog + .openConfirm({ + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', + }, + }) .pipe(defaultIfEmpty(false)), ) } } - -function getOptions( - content: string, - yes = 'Continue', -): Partial> { - return { - label: 'Warning', - size: 's', - data: { - content, - yes, - no: 'Cancel', - }, - } -} diff --git a/web/projects/ui/src/app/services/form-dialog.service.ts b/web/projects/ui/src/app/services/form-dialog.service.ts index 7891a7a0b..c68f32111 100644 --- a/web/projects/ui/src/app/services/form-dialog.service.ts +++ b/web/projects/ui/src/app/services/form-dialog.service.ts @@ -1,22 +1,26 @@ import { inject, Injectable, Injector, Type } from '@angular/core' -import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' +import { TuiDialogOptions } from '@taiga-ui/core' import { TuiConfirmData, TuiConfirmService } from '@taiga-ui/kit' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' -const PROMPT: Partial> = { - label: 'Unsaved Changes', - data: { - content: 'You have unsaved changes. Are you sure you want to leave?', - yes: 'Leave', - no: 'Cancel', - }, -} - @Injectable({ providedIn: 'root' }) export class FormDialogService { - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) + private readonly i18n = inject(i18nPipe) private readonly formService = new TuiConfirmService() - private readonly prompt = this.formService.withConfirm(PROMPT) + private readonly PROMPT: Partial> = { + label: this.i18n.transform('Unsaved changes'), + data: { + content: this.i18n.transform( + 'You have unsaved changes. Are you sure you want to leave?', + ), + yes: this.i18n.transform('Leave'), + no: this.i18n.transform('Cancel'), + }, + } + + private readonly prompt = this.formService.withConfirm(this.PROMPT) private readonly injector = Injector.create({ parent: inject(Injector), providers: [ @@ -27,9 +31,14 @@ export class FormDialogService { ], }) - open(component: Type, options: Partial> = {}) { - this.dialogs - .open(new PolymorpheusComponent(component, this.injector), { + open( + component: Type, + options: Partial> & { + label?: i18nKey + } = {}, + ) { + this.dialog + .openComponent(new PolymorpheusComponent(component, this.injector), { closeable: this.prompt, dismissible: this.prompt, ...options, diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index 1b1457c26..b015d6a91 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -243,15 +243,15 @@ function listValidators(spec: IST.ValueSpecList): ValidatorFn[] { return validators } -function fileValidators(spec: IST.ValueSpecFile): ValidatorFn[] { - const validators: ValidatorFn[] = [] +// function fileValidators(spec: IST.ValueSpecFile): ValidatorFn[] { +// const validators: ValidatorFn[] = [] - if (spec.required) { - validators.push(Validators.required) - } +// if (spec.required) { +// validators.push(Validators.required) +// } - return validators -} +// return validators +// } export function numberInRange( min: number | null, diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index cbba2d55e..7f27a467e 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -49,6 +49,7 @@ export class MarketplaceService { switchMap(url => this.fetchRegistry$(url)), filter(Boolean), map(registry => { + // @TODO Aiden let's drop description. We do not use it. categories should just be Record registry.info.categories = { all: { name: 'All', @@ -178,12 +179,13 @@ export class MarketplaceService { return from(this.api.getRegistryInfo(url)).pipe( map(info => ({ ...info, + // @TODO Aiden let's drop description. We do not use it. categories should just be Record categories: { all: { name: 'All', description: { - short: 'All services', - long: 'An unfiltered list of all services available on this registry.', + short: '', + long: '', }, }, ...info.categories, diff --git a/web/projects/ui/src/app/services/notification.service.ts b/web/projects/ui/src/app/services/notification.service.ts index 799088f59..156a5fa6e 100644 --- a/web/projects/ui/src/app/services/notification.service.ts +++ b/web/projects/ui/src/app/services/notification.service.ts @@ -1,9 +1,9 @@ import { inject, Injectable } from '@angular/core' import { ErrorService, MARKDOWN } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' +import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { PatchDB } from 'patch-db-client' import { firstValueFrom, merge, of, shareReplay, Subject } from 'rxjs' -import { REPORT } from 'src/app/components/report.component' +import { REPORT } from 'src/app/components/backup-report.component' import { ServerNotification, ServerNotifications, @@ -16,7 +16,7 @@ export class NotificationService { private readonly patch = inject>(PatchDB) private readonly errorService = inject(ErrorService) private readonly api = inject(ApiService) - private readonly dialogs = inject(TuiDialogService) + private readonly dialogs = inject(TuiResponsiveDialogService) private readonly localUnreadCount$ = new Subject() readonly unreadCount$ = merge( diff --git a/web/projects/ui/src/app/services/patch-data.service.ts b/web/projects/ui/src/app/services/patch-data.service.ts index 5ce107490..3c2b9eda6 100644 --- a/web/projects/ui/src/app/services/patch-data.service.ts +++ b/web/projects/ui/src/app/services/patch-data.service.ts @@ -7,7 +7,7 @@ import { EOSService } from 'src/app/services/eos.service' import { ConnectionService } from 'src/app/services/connection.service' import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' -// Get data from PatchDb after is starts and act upon it +// @TODO Alex this file has just become checking for StartOS updates. Maybe it can be removed/simplified. I'm not sure why getMarketplace$() line is commented out, I assume we are checking for service updates somewhere else? @Injectable({ providedIn: 'root', }) @@ -19,7 +19,6 @@ export class PatchDataService extends Observable { this.bootstrapper.update(cache) if (index === 0) { - // check for updates to StartOS and services this.checkForUpdates() } }), diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index adb6b882b..5c8731167 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -1,3 +1,4 @@ +import { Languages } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData } @@ -12,7 +13,7 @@ export type UIData = { } ackInstructions: Record theme: string - language: 'english' | 'spanish' + language: Languages } export type UIMarketplaceData = { diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index c10f2f809..dfc665806 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,6 +1,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PkgDependencyErrors } from './dep-error.service' import { T } from '@start9labs/start-sdk' +import { i18nKey } from '@start9labs/shared' export interface PackageStatus { primary: PrimaryStatus @@ -65,7 +66,7 @@ function getHealthStatus(status: T.MainStatus): T.HealthStatus | null { } export interface StatusRendering { - display: string + display: i18nKey color: string showDots?: boolean } @@ -138,7 +139,7 @@ export const PrimaryRendering: Record = { showDots: false, }, actionRequired: { - display: 'Action Required', + display: 'Task Required', color: 'warning', showDots: false, }, diff --git a/web/projects/ui/src/app/services/standard-actions.service.ts b/web/projects/ui/src/app/services/standard-actions.service.ts index ef2e177d9..f6cffcb20 100644 --- a/web/projects/ui/src/app/services/standard-actions.service.ts +++ b/web/projects/ui/src/app/services/standard-actions.service.ts @@ -1,9 +1,13 @@ import { inject, Injectable } from '@angular/core' import { Router } from '@angular/router' -import { ErrorService, LoadingService } from '@start9labs/shared' +import { + DialogService, + ErrorService, + i18nKey, + i18nPipe, + LoadingService, +} from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -import { TuiDialogService } from '@taiga-ui/core' -import { TUI_CONFIRM } from '@taiga-ui/kit' import { PatchDB } from 'patch-db-client' import { filter } from 'rxjs' import { getAllPackages } from '../utils/get-package-data' @@ -17,13 +21,14 @@ import { DataModel } from './patch-db/data-model' export class StandardActionsService { private readonly patch = inject>(PatchDB) private readonly api = inject(ApiService) - private readonly dialogs = inject(TuiDialogService) + private readonly dialog = inject(DialogService) private readonly errorService = inject(ErrorService) private readonly loader = inject(LoadingService) private readonly router = inject(Router) + private readonly i18n = inject(i18nPipe) async rebuild(id: string) { - const loader = this.loader.open(`Rebuilding Container...`).subscribe() + const loader = this.loader.open('Rebuilding container').subscribe() try { await this.api.rebuildPackage({ id }) @@ -38,18 +43,18 @@ export class StandardActionsService { async uninstall({ id, title, alerts }: T.Manifest): Promise { let content = alerts.uninstall || - `Uninstalling ${title} will permanently delete its data` + `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}` if (hasCurrentDeps(id, await getAllPackages(this.patch))) { - content = `${content}. Services that depend on ${title} will no longer work properly and may crash` + content = `${content}. ${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}` } - this.dialogs - .open(TUI_CONFIRM, { + this.dialog + .openConfirm({ label: 'Warning', size: 's', data: { - content, + content: content as i18nKey, yes: 'Uninstall', no: 'Cancel', }, @@ -59,7 +64,7 @@ export class StandardActionsService { } private async doUninstall(id: string) { - const loader = this.loader.open(`Beginning uninstall...`).subscribe() + const loader = this.loader.open('Beginning uninstall').subscribe() try { await this.api.uninstallPackage({ id }) diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts index af7854bfd..c20df8a14 100644 --- a/web/projects/ui/src/app/services/state.service.ts +++ b/web/projects/ui/src/app/services/state.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core' import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router' +import { DialogService } from '@start9labs/shared' import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk' -import { TuiAlertService } from '@taiga-ui/core' import { BehaviorSubject, combineLatest, @@ -41,7 +41,7 @@ const OPTIONS: IsActiveMatchOptions = { providedIn: 'root', }) export class StateService extends Observable { - private readonly alerts = inject(TuiAlertService) + private readonly dialog = inject(DialogService) private readonly api = inject(ApiService) private readonly router = inject(Router) private readonly network$ = inject(NetworkService) @@ -91,8 +91,8 @@ export class StateService extends Observable { .pipe( exhaustMap(() => concat( - this.alerts - .open('Trying to reach server', { + this.dialog + .openAlert('Trying to reach server', { label: 'State unknown', autoClose: 0, appearance: 'negative', @@ -104,8 +104,8 @@ export class StateService extends Observable { ), ), ), - this.alerts.open('Connection restored', { - label: 'Server reached', + this.dialog.openAlert('Connection restored', { + label: 'Server connected', appearance: 'positive', }), ), diff --git a/web/projects/ui/src/app/services/status.service.ts b/web/projects/ui/src/app/services/status.service.ts index a30a78739..7b2676314 100644 --- a/web/projects/ui/src/app/services/status.service.ts +++ b/web/projects/ui/src/app/services/status.service.ts @@ -5,6 +5,7 @@ import { combineLatest, map, startWith } from 'rxjs' import { ConnectionService } from './connection.service' import { NetworkService } from './network.service' import { DataModel } from './patch-db/data-model' +import { i18nKey } from '@start9labs/shared' export const STATUS = new InjectionToken('', { factory: () => @@ -29,33 +30,40 @@ export const STATUS = new InjectionToken('', { ), }) -const OFFLINE = { +const OFFLINE: ServerStatus = { message: 'No Internet', color: 'var(--tui-status-negative)', icon: '@tui.cloud-off', status: 'error', } -const CONNECTING = { +const CONNECTING: ServerStatus = { message: 'Connecting', color: 'var(--tui-status-warning)', icon: '@tui.cloud-off', status: 'warning', } -const SHUTTING_DOWN = { - message: 'Shutting Down', +const SHUTTING_DOWN: ServerStatus = { + message: 'Shutting down', color: 'var(--tui-status-neutral)', icon: '@tui.power', status: 'neutral', } -const RESTARTING = { +const RESTARTING: ServerStatus = { message: 'Restarting', color: 'var(--tui-status-neutral)', icon: '@tui.power', status: 'neutral', } -const CONNECTED = { +const CONNECTED: ServerStatus = { message: 'Connected', color: 'var(--tui-status-positive)', icon: '@tui.cloud', status: 'success', } + +type ServerStatus = { + message: i18nKey + color: string + icon: string + status: 'error' | 'warning' | 'neutral' | 'success' +} diff --git a/web/projects/ui/src/app/utils/resources.ts b/web/projects/ui/src/app/utils/resources.ts index 1c5096261..09f56d1a0 100644 --- a/web/projects/ui/src/app/utils/resources.ts +++ b/web/projects/ui/src/app/utils/resources.ts @@ -1,11 +1,13 @@ -export const RESOURCES = [ +import { i18nKey } from '@start9labs/shared' + +export const RESOURCES: { name: i18nKey; icon: string; href: string }[] = [ { - name: 'User Manual', + name: 'User manual', icon: '@tui.book-open', href: 'https://docs.start9.com/0.3.5.x/user-manual', }, { - name: 'Contact Support', + name: 'Contact support', icon: '@tui.headphones', href: 'https://start9.com/contact', }, diff --git a/web/projects/ui/src/app/utils/system-utilities.ts b/web/projects/ui/src/app/utils/system-utilities.ts index cb90c48cc..b8c977864 100644 --- a/web/projects/ui/src/app/utils/system-utilities.ts +++ b/web/projects/ui/src/app/utils/system-utilities.ts @@ -1,53 +1,56 @@ import { inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { i18nKey } from '@start9labs/shared' import { BadgeService } from 'src/app/services/badge.service' -export const SYSTEM_UTILITIES: Record = - { - '/portal/services': { - icon: '@tui.layout-grid', - title: 'Services', - }, - '/portal/marketplace': { - icon: '@tui.shopping-cart', - title: 'Marketplace', - }, - '/portal/sideload': { - icon: '@tui.upload', - title: 'Sideload', - }, - '/portal/updates': { - icon: '@tui.globe', - title: 'Updates', - }, - // @TODO 041 - // '/portal/backups': { - // icon: '@tui.save', - // title: 'Backups', - // }, - '/portal/metrics': { - icon: '@tui.activity', - title: 'Metrics', - }, - '/portal/logs': { - icon: '@tui.file-text', - title: 'Logs', - }, - '/portal/system': { - icon: '@tui.settings', - title: 'System', - }, - '/portal/notifications': { - icon: '@tui.bell', - title: 'Notifications', - }, - } +export const SYSTEM_UTILITIES: Record< + string, + { icon: string; title: i18nKey } +> = { + '/portal/services': { + icon: '@tui.layout-grid', + title: 'Services', + }, + '/portal/marketplace': { + icon: '@tui.shopping-cart', + title: 'Marketplace', + }, + '/portal/sideload': { + icon: '@tui.upload', + title: 'Sideload', + }, + '/portal/updates': { + icon: '@tui.globe', + title: 'Updates', + }, + // @TODO 041 + // '/portal/backups': { + // icon: '@tui.save', + // title: 'Backups', + // }, + '/portal/metrics': { + icon: '@tui.activity', + title: 'Metrics', + }, + '/portal/logs': { + icon: '@tui.file-text', + title: 'Logs', + }, + '/portal/system': { + icon: '@tui.settings', + title: 'System', + }, + '/portal/notifications': { + icon: '@tui.bell', + title: 'Notifications', + }, +} export function getMenu() { const badge = inject(BadgeService) return Object.keys(SYSTEM_UTILITIES).map(key => ({ - name: SYSTEM_UTILITIES[key]?.title || '', + name: SYSTEM_UTILITIES[key]?.title || ('' as i18nKey), icon: SYSTEM_UTILITIES[key]?.icon || '', routerLink: key, badge: toSignal(badge.getCount(key), { initialValue: 0 }), diff --git a/web/tslint.json b/web/tslint.json index ad6856103..52bb46240 100644 --- a/web/tslint.json +++ b/web/tslint.json @@ -29,6 +29,6 @@ "singleline": "never" } ], - "quotemark": [true, "single"] + "quotemark": [true, "single", "avoid-escape"] } }