diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html
index 4b3d5fb26..33b9d45e3 100644
--- a/web/projects/marketplace/src/pages/list/item/item.component.html
+++ b/web/projects/marketplace/src/pages/list/item/item.component.html
@@ -12,7 +12,7 @@
{{ pkg.title }}
- {{ pkg.description.short }}
+ {{ pkg.description.short | localize }}
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 e6989b562..682f24de5 100644
--- a/web/projects/marketplace/src/pages/list/item/item.module.ts
+++ b/web/projects/marketplace/src/pages/list/item/item.module.ts
@@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
-import { SharedPipesModule, TickerComponent } from '@start9labs/shared'
+import { LocalizePipe, SharedPipesModule, TickerComponent } from '@start9labs/shared'
import { ItemComponent } from './item.component'
@NgModule({
declarations: [ItemComponent],
exports: [ItemComponent],
- imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent],
+ imports: [CommonModule, RouterModule, SharedPipesModule, TickerComponent, LocalizePipe],
})
export class ItemModule {}
diff --git a/web/projects/marketplace/src/pages/show/about.component.ts b/web/projects/marketplace/src/pages/show/about.component.ts
index edbe5c5a5..a4f7a9135 100644
--- a/web/projects/marketplace/src/pages/show/about.component.ts
+++ b/web/projects/marketplace/src/pages/show/about.component.ts
@@ -6,7 +6,7 @@ import {
output,
} from '@angular/core'
import { MarketplacePkgBase } from '../../types'
-import { CopyService, i18nPipe } from '@start9labs/shared'
+import { CopyService, i18nPipe, LocalizePipe } from '@start9labs/shared'
import { DatePipe } from '@angular/common'
import { MarketplaceItemComponent } from './item.component'
@@ -71,7 +71,7 @@ import { MarketplaceItemComponent } from './item.component'
{{ 'Description' | i18n }}
-
+
`,
@@ -129,7 +129,7 @@ import { MarketplaceItemComponent } from './item.component'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [MarketplaceItemComponent, DatePipe, i18nPipe],
+ imports: [MarketplaceItemComponent, DatePipe, i18nPipe, LocalizePipe],
})
export class MarketplaceAboutComponent {
readonly copyService = inject(CopyService)
diff --git a/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts
new file mode 100644
index 000000000..0daef9e94
--- /dev/null
+++ b/web/projects/setup-wizard/src/app/components/remove-media.dialog.ts
@@ -0,0 +1,121 @@
+import { Component } from '@angular/core'
+import { i18nPipe } from '@start9labs/shared'
+import { TuiButton, TuiDialogContext } from '@taiga-ui/core'
+import { injectContext } from '@taiga-ui/polymorpheus'
+
+@Component({
+ standalone: true,
+ imports: [TuiButton, i18nPipe],
+ template: `
+
+
+ {{
+ 'Remove USB stick or other installation media from your server' | i18n
+ }}
+
+
+ `,
+ styles: `
+ :host {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ }
+
+ .animation-container {
+ position: relative;
+ width: 160px;
+ height: 69px;
+ }
+
+ .port {
+ position: absolute;
+ left: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 28px;
+ height: 18px;
+ background: var(--tui-background-neutral-1);
+ border: 2px solid var(--tui-border-normal);
+ border-radius: 2px;
+ }
+
+ .port-inner {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ right: 3px;
+ bottom: 3px;
+ background: var(--tui-background-neutral-2);
+ border-radius: 1px;
+ }
+
+ .usb-stick {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ animation: slide-out 2s ease-in-out 0.5s infinite;
+ left: 32px;
+ }
+
+ .usb-connector {
+ width: 20px;
+ height: 12px;
+ background: var(--tui-text-secondary);
+ border-radius: 1px;
+ }
+
+ .usb-body {
+ width: 40px;
+ height: 20px;
+ background: var(--tui-status-info);
+ border-radius: 2px 4px 4px 2px;
+ }
+
+ @keyframes slide-out {
+ 0% {
+ left: 32px;
+ opacity: 0;
+ }
+ 5% {
+ left: 32px;
+ opacity: 1;
+ }
+ 80% {
+ left: 130px;
+ opacity: 0;
+ }
+ 100% {
+ left: 130px;
+ opacity: 0;
+ }
+ }
+
+ p {
+ margin: 0 0 2rem;
+ }
+
+ footer {
+ display: flex;
+ justify-content: center;
+ }
+ `,
+})
+export class RemoveMediaDialog {
+ protected readonly context = injectContext>()
+}
diff --git a/web/projects/setup-wizard/src/app/pages/drives.page.ts b/web/projects/setup-wizard/src/app/pages/drives.page.ts
index f3c7ded42..2790fc306 100644
--- a/web/projects/setup-wizard/src/app/pages/drives.page.ts
+++ b/web/projects/setup-wizard/src/app/pages/drives.page.ts
@@ -1,4 +1,9 @@
-import { ChangeDetectorRef, Component, inject } from '@angular/core'
+import {
+ ChangeDetectorRef,
+ Component,
+ HostListener,
+ inject,
+} from '@angular/core'
import { Router } from '@angular/router'
import { FormsModule } from '@angular/forms'
import {
@@ -21,13 +26,14 @@ import {
import { TuiDataListWrapper, TuiSelect, TuiTooltip } from '@taiga-ui/kit'
import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
-import { filter } from 'rxjs'
+import { filter, Subscription } from 'rxjs'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog'
@Component({
template: `
+ @if (!shuttingDown) {
{{ 'Select Drives' | i18n }}
@@ -132,6 +138,7 @@ import { PreserveOverwriteDialog } from '../components/preserve-overwrite.dialog
}
+ }
`,
styles: `
.no-drives {
@@ -176,6 +183,14 @@ export default class DrivesPage {
protected readonly mobile = inject(TUI_IS_MOBILE)
+ @HostListener('document:keydown', ['$event'])
+ onKeydown(event: KeyboardEvent) {
+ if (event.ctrlKey && event.shiftKey && event.key === 'X') {
+ event.preventDefault()
+ this.shutdownServer()
+ }
+ }
+
readonly osDriveTooltip = this.i18n.transform(
'The drive where the StartOS operating system will be installed.',
)
@@ -185,6 +200,8 @@ export default class DrivesPage {
drives: DiskInfo[] = []
loading = true
+ shuttingDown = false
+ private dialogSub?: Subscription
selectedOsDrive: DiskInfo | null = null
selectedDataDrive: DiskInfo | null = null
preserveData: boolean | null = null
@@ -339,22 +356,18 @@ export default class DrivesPage {
loader.unsubscribe()
// Show success dialog
- this.dialogs
- .openConfirm({
+ this.dialogSub = this.dialogs
+ .openAlert('StartOS has been installed successfully.', {
label: 'Installation Complete!',
size: 's',
- data: {
- content: 'StartOS has been installed successfully.',
- yes: 'Continue to Setup',
- no: 'Shutdown',
- },
+ dismissible: false,
+ closeable: true,
+ data: { button: this.i18n.transform('Continue to Setup') },
})
- .subscribe(continueSetup => {
- if (continueSetup) {
+ .subscribe({
+ complete: () => {
this.navigateToNextStep(result.attach)
- } else {
- this.shutdownServer()
- }
+ },
})
} catch (e: any) {
loader.unsubscribe()
@@ -372,10 +385,12 @@ export default class DrivesPage {
}
private async shutdownServer() {
+ this.dialogSub?.unsubscribe()
const loader = this.loader.open('Beginning shutdown').subscribe()
try {
await this.api.shutdown()
+ this.shuttingDown = true
} catch (e: any) {
this.errorService.handleError(e)
} finally {
diff --git a/web/projects/setup-wizard/src/app/pages/success.page.ts b/web/projects/setup-wizard/src/app/pages/success.page.ts
index 8f84f788b..03dae0203 100644
--- a/web/projects/setup-wizard/src/app/pages/success.page.ts
+++ b/web/projects/setup-wizard/src/app/pages/success.page.ts
@@ -6,7 +6,12 @@ import {
ViewChild,
DOCUMENT,
} from '@angular/core'
-import { DownloadHTMLService, ErrorService, i18nPipe } from '@start9labs/shared'
+import {
+ DialogService,
+ DownloadHTMLService,
+ ErrorService,
+ i18nPipe,
+} from '@start9labs/shared'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
@@ -14,7 +19,9 @@ import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { DocumentationComponent } from '../components/documentation.component'
import { MatrixComponent } from '../components/matrix.component'
+import { RemoveMediaDialog } from '../components/remove-media.dialog'
import { SetupCompleteRes } from '../types'
+import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@Component({
template: `
@@ -29,12 +36,8 @@ import { SetupCompleteRes } from '../types'
@if (!stateService.kiosk) {
{{
- stateService.setupType === 'restore'
- ? ('You can unplug your backup drive' | i18n)
- : stateService.setupType === 'transfer'
- ? ('You can unplug your transfer drive' | i18n)
- : ('http://start.local was for setup only. It will no longer work.'
- | i18n)
+ 'http://start.local was for setup only. It will no longer work.'
+ | i18n
}}
}
@@ -69,14 +72,15 @@ import { SetupCompleteRes } from '../types'
tuiCell="l"
[class.disabled]="!stateService.kiosk && !downloaded"
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
- (click)="usbRemoved = true"
+ (click)="removeMedia()"
>
- {{ 'USB Removed' | i18n }}
+ {{ 'Remove Installation Media' | i18n }}
{{
- 'Remove the USB installation media from your server' | i18n
+ 'Remove USB stick or other installation media from your server'
+ | i18n
}}
@@ -184,6 +188,7 @@ export default class SuccessPage implements AfterViewInit {
private readonly errorService = inject(ErrorService)
private readonly api = inject(ApiService)
private readonly downloadHtml = inject(DownloadHTMLService)
+ private readonly dialogs = inject(DialogService)
private readonly i18n = inject(i18nPipe)
readonly stateService = inject(StateService)
@@ -225,6 +230,21 @@ export default class SuccessPage implements AfterViewInit {
})
}
+ removeMedia() {
+ this.dialogs
+ .openComponent(
+ new PolymorpheusComponent(RemoveMediaDialog),
+ {
+ size: 's',
+ dismissible: false,
+ closeable: false,
+ },
+ )
+ .subscribe(() => {
+ this.usbRemoved = true
+ })
+ }
+
exitKiosk() {
this.api.exit()
}
diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts
index e229d9f24..16d7b9473 100644
--- a/web/projects/shared/src/i18n/dictionaries/de.ts
+++ b/web/projects/shared/src/i18n/dictionaries/de.ts
@@ -100,6 +100,7 @@ export default {
102: 'Verlassen',
103: 'Sind Sie sicher?',
104: 'Neues Netzwerk-Gateway',
+ 107: 'Onion-Domains',
108: 'Öffentlich',
109: 'privat',
111: 'Keine Onion-Domains',
@@ -639,13 +640,11 @@ export default {
667: 'Einrichtung wird gestartet',
670: 'Warten Sie 1–2 Minuten und aktualisieren Sie die Seite',
672: 'Einrichtung abgeschlossen!',
- 673: 'Sie können Ihr Backup-Laufwerk entfernen',
- 674: 'Sie können Ihr Übertragungs-Laufwerk entfernen',
675: 'http://start.local war nur für die Einrichtung gedacht. Es funktioniert nicht mehr.',
676: 'Adressinformationen herunterladen',
677: 'Enthält die permanente lokale Adresse Ihres Servers und die Root-CA',
- 678: 'USB entfernt',
- 679: 'Entfernen Sie das USB-Installationsmedium aus Ihrem Server',
+ 678: 'Installationsmedium entfernen',
+ 679: 'Entfernen Sie den USB-Stick oder andere Installationsmedien von Ihrem Server',
680: 'Server neu starten',
681: 'Warten, bis der Server wieder online ist',
682: 'Server ist wieder online',
diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts
index b7e05c7b6..d769ded05 100644
--- a/web/projects/shared/src/i18n/dictionaries/en.ts
+++ b/web/projects/shared/src/i18n/dictionaries/en.ts
@@ -99,6 +99,7 @@ export const ENGLISH: Record = {
'Leave': 102,
'Are you sure?': 103,
'New gateway': 104, // as in, a network gateway
+ 'Tor Domains': 107,
'public': 108,
'private': 109,
'No Tor domains': 111,
@@ -639,13 +640,11 @@ export const ENGLISH: Record = {
'Starting setup': 667,
'Wait 1-2 minutes and refresh the page': 670,
'Setup Complete!': 672,
- 'You can unplug your backup drive': 673,
- 'You can unplug your transfer drive': 674,
'http://start.local was for setup only. It will no longer work.': 675,
'Download Address Info': 676,
"Contains your server's permanent local address and Root CA": 677,
- 'USB Removed': 678,
- 'Remove the USB installation media from your server': 679,
+ 'Remove Installation Media': 678,
+ 'Remove USB stick or other installation media from your server': 679,
'Restart Server': 680,
'Waiting for server to come back online': 681,
'Server is back online': 682,
diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts
index 3233d1496..1f82b395e 100644
--- a/web/projects/shared/src/i18n/dictionaries/es.ts
+++ b/web/projects/shared/src/i18n/dictionaries/es.ts
@@ -100,6 +100,7 @@ export default {
102: 'Salir',
103: '¿Estás seguro?',
104: 'Nueva puerta de enlace de red',
+ 107: 'dominios onion',
108: 'público',
109: 'privado',
111: 'Sin dominios onion',
@@ -639,13 +640,11 @@ export default {
667: 'Iniciando configuración',
670: 'Espere 1–2 minutos y actualice la página',
672: '¡Configuración completa!',
- 673: 'Puede desconectar su unidad de copia de seguridad',
- 674: 'Puede desconectar su unidad de transferencia',
675: 'http://start.local era solo para la configuración. Ya no funcionará.',
676: 'Descargar información de direcciones',
677: 'Contiene la dirección local permanente de su servidor y la CA raíz',
- 678: 'USB retirado',
- 679: 'Retire el medio de instalación USB de su servidor',
+ 678: 'Retirar medio de instalación',
+ 679: 'Retire la memoria USB u otro medio de instalación de su servidor',
680: 'Reiniciar servidor',
681: 'Esperando a que el servidor vuelva a estar en línea',
682: 'El servidor ha vuelto a estar en línea',
diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts
index fc86d901c..db13e7d97 100644
--- a/web/projects/shared/src/i18n/dictionaries/fr.ts
+++ b/web/projects/shared/src/i18n/dictionaries/fr.ts
@@ -100,6 +100,7 @@ export default {
102: 'Quitter',
103: 'Êtes-vous sûr ?',
104: 'Nouvelle passerelle réseau',
+ 107: 'domaine onion',
108: 'public',
109: 'privé',
111: 'Aucune domaine onion',
@@ -639,13 +640,11 @@ export default {
667: 'Démarrage de la configuration',
670: 'Attendez 1 à 2 minutes puis actualisez la page',
672: 'Configuration terminée !',
- 673: 'Vous pouvez débrancher votre disque de sauvegarde',
- 674: 'Vous pouvez débrancher votre disque de transfert',
675: 'http://start.local était réservé à la configuration. Il ne fonctionnera plus.',
676: 'Télécharger les informations d’adresse',
- 677: 'Contient l’adresse locale permanente de votre serveur et la CA racine',
- 678: 'USB retiré',
- 679: 'Retirez le support d’installation USB de votre serveur',
+ 677: 'Contient l\u2019adresse locale permanente de votre serveur et la CA racine',
+ 678: 'Retirer le support d\u2019installation',
+ 679: 'Retirez la clé USB ou tout autre support d\u2019installation de votre serveur',
680: 'Redémarrer le serveur',
681: 'En attente du retour en ligne du serveur',
682: 'Le serveur est de nouveau en ligne',
diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts
index 3911af7bb..13a3c4671 100644
--- a/web/projects/shared/src/i18n/dictionaries/pl.ts
+++ b/web/projects/shared/src/i18n/dictionaries/pl.ts
@@ -100,6 +100,7 @@ export default {
102: 'Opuść',
103: 'Czy jesteś pewien?',
104: 'Nowa brama sieciowa',
+ 107: 'domeny onion',
108: 'publiczny',
109: 'prywatny',
111: 'Brak domeny onion',
@@ -639,13 +640,11 @@ export default {
667: 'Rozpoczynanie konfiguracji',
670: 'Poczekaj 1–2 minuty i odśwież stronę',
672: 'Konfiguracja zakończona!',
- 673: 'Możesz odłączyć dysk kopii zapasowej',
- 674: 'Możesz odłączyć dysk transferowy',
675: 'http://start.local służył tylko do konfiguracji. Nie będzie już działać.',
676: 'Pobierz informacje adresowe',
677: 'Zawiera stały lokalny adres serwera oraz główny urząd certyfikacji (Root CA)',
- 678: 'USB usunięty',
- 679: 'Usuń instalacyjny nośnik USB z serwera',
+ 678: 'Usuń nośnik instalacyjny',
+ 679: 'Usuń pamięć USB lub inny nośnik instalacyjny z serwera',
680: 'Uruchom ponownie serwer',
681: 'Oczekiwanie na ponowne połączenie serwera',
682: 'Serwer jest ponownie online',
diff --git a/web/projects/shared/src/i18n/localize.pipe.ts b/web/projects/shared/src/i18n/localize.pipe.ts
index e9f439ec0..1ad76bb0b 100644
--- a/web/projects/shared/src/i18n/localize.pipe.ts
+++ b/web/projects/shared/src/i18n/localize.pipe.ts
@@ -1,5 +1,6 @@
import { inject, Injectable, Pipe, PipeTransform } from '@angular/core'
import { i18nService } from './i18n.service'
+import { I18N } from './i18n.providers'
import { T } from '@start9labs/start-sdk'
@Pipe({
@@ -9,8 +10,10 @@ import { T } from '@start9labs/start-sdk'
@Injectable({ providedIn: 'root' })
export class LocalizePipe implements PipeTransform {
private readonly i18nService = inject(i18nService)
+ private readonly i18n = inject(I18N)
transform(string: T.LocaleString): string {
+ this.i18n() // read signal to trigger change detection on language switch
return this.i18nService.localize(string)
}
}
diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts
index 9fb233423..001bcd71a 100644
--- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts
+++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor-domains.component.ts
@@ -35,7 +35,7 @@ type OnionForm = {
selector: 'section[torDomains]',
template: `
- Tor Domains
+ {{ 'Tor Domains' | i18n }}
>(PatchDB)
private readonly i18n = inject(i18nPipe)
+ private readonly i18nService = inject(i18nService)
async start({ title, alerts, id }: T.Manifest, unmet: boolean) {
const deps =
@@ -31,7 +33,7 @@ export class ControlsService {
if (
(unmet && !(await this.alert(deps))) ||
- (alerts.start && !(await this.alert(alerts.start as i18nKey)))
+ (alerts.start && !(await this.alert(alerts.start)))
) {
return
}
@@ -49,7 +51,7 @@ export class ControlsService {
async stop({ id, title, alerts }: T.Manifest) {
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 || ''
+ let content = alerts.stop ? this.i18nService.localize(alerts.stop) : ''
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
content = content ? `${content}.\n\n${depMessage}` : depMessage
@@ -113,14 +115,14 @@ export class ControlsService {
})
}
- private alert(content: i18nKey): Promise {
+ private alert(content: T.LocaleString): Promise {
return firstValueFrom(
this.dialog
.openConfirm({
label: 'Warning',
size: 's',
data: {
- content,
+ content: this.i18nService.localize(content),
yes: 'Continue',
no: 'Cancel',
},
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 daaacc705..b0902944c 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
@@ -31,7 +31,7 @@ export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
(!statusInfo.started ||
Object.values(statusInfo.health)
.filter(h => !!h)
- .some(h => h.result === 'starting'))
+ .some(h => h.result === 'starting' || h.result === 'waiting'))
) {
return 'starting'
}
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 6b3386877..5cd002139 100644
--- a/web/projects/ui/src/app/services/standard-actions.service.ts
+++ b/web/projects/ui/src/app/services/standard-actions.service.ts
@@ -5,6 +5,7 @@ import {
ErrorService,
i18nKey,
i18nPipe,
+ i18nService,
LoadingService,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
@@ -27,6 +28,7 @@ export class StandardActionsService {
private readonly loader = inject(LoadingService)
private readonly router = inject(Router)
private readonly i18n = inject(i18nPipe)
+ private readonly i18nService = inject(i18nService)
async rebuild(id: string) {
const loader = this.loader.open('Rebuilding container').subscribe()
@@ -50,11 +52,12 @@ export class StandardActionsService {
): Promise {
let content = soft
? ''
- : alerts.uninstall ||
- `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
+ : alerts.uninstall
+ ? this.i18nService.localize(alerts.uninstall)
+ : `${this.i18n.transform('Uninstalling')} ${title} ${this.i18n.transform('will permanently delete its data.')}`
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
- content = `${content}${content ? ' ' : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
+ content = `${content ? `${content} ` : ''}${this.i18n.transform('Services that depend on')} ${title} ${this.i18n.transform('will no longer work properly and may crash.')}`
}
if (!content) {