From 8ca3d56aa9520a21f66bcfcb7b9ecfb4c4ec7873 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 16 Jan 2026 15:41:28 -0700 Subject: [PATCH 1/3] remove start-tunnel readme --- START-TUNNEL.md | 95 ------------------------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 START-TUNNEL.md diff --git a/START-TUNNEL.md b/START-TUNNEL.md deleted file mode 100644 index 8fe02e755..000000000 --- a/START-TUNNEL.md +++ /dev/null @@ -1,95 +0,0 @@ -# StartTunnel - -A self-hosted WireGuard VPN optimized for creating VLANs and reverse tunneling to personal servers. - -You can think of StartTunnel as "virtual router in the cloud". - -Use it for private remote access to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address. - -## Features - -- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router. - -- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique WireGuard config that must be copied, downloaded, or scanned into the device. - -- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet. - -## Installation - -1. Rent a low cost VPS. For most use cases, the cheapest option should be enough. - - - It must have a dedicated public IP address. - - For compute (CPU), memory (RAM), and storage (disk), choose the minimum spec. - - For transfer (bandwidth), it depends on (1) your use case and (2) your home Internet's _upload_ speed. Even if you intend to serve large files or stream content from your server, there is no reason to pay for speeds that exceed your home Internet's upload speed. - -1. Provision the VPS with the latest version of Debian. - -1. Access the VPS via SSH. - -1. Run the StartTunnel install script: - - curl -fsSL https://start9labs.github.io/start-tunnel | sh - -1. [Initialize the web interface](#web-interface) (recommended) - -## Updating - -Simply re-run the install command: - -```sh -curl -fsSL https://start9labs.github.io/start-tunnel | sh -``` - -## CLI - -By default, StartTunnel is managed via the `start-tunnel` command line interface, which is self-documented. - -``` -start-tunnel --help -``` - -## Web Interface - -Enable the web interface (recommended in most cases) to access your StartTunnel from the browser or via API. - -1. Initialize the web interface. - - start-tunnel web init - -1. If your VPS has multiple public IP addresses, you will be prompted to select the IP address at which to host the web interface. - -1. When prompted, enter the port at which to host the web interface. The default is 8443, and we recommend using it. If you change the default, choose an uncommon port to avoid future conflicts. - -1. To access your StartTunnel web interface securely over HTTPS, you need an SSL certificate. When prompted, select whether to autogenerate a certificate or provide your own. _This is only for accessing your StartTunnel web interface_. - -1. You will receive a success message with 3 pieces of information: - - - ****: the URL where you can reach your personal web interface. - - **Password**: an autogenerated password for your interface. If you lose/forget it, you can reset it using the start-tunnel CLI. - - **Root Certificate Authority**: the Root CA of your StartTunnel instance. - -1. If you autogenerated your SSL certificate, visiting the `https://IP:port` URL in the browser will warn you that the website is insecure. This is expected. You have two options for getting past this warning: - - option 1 (recommended): [Trust your StartTunnel Root CA on your connecting device](#trusting-your-starttunnel-root-ca). - - Option 2: bypass the warning in the browser, creating a one-time security exception. - -### Trusting your StartTunnel Root CA - -1. Copy the contents of your Root CA (starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----). - -2. Open a text editor: - - - Linux: gedit, nano, or any editor - - Mac: TextEdit - - Windows: Notepad - -3. Paste the contents of your Root CA. - -4. Save the file with a `.crt` extension (e.g. `start-tunnel.crt`) (make sure it saves as plain text, not rich text). - -5. Trust the Root CA on your client device(s): - - - [Linux](https://staging.docs.start9.com/device-guides/linux/ca.html) - - [Mac](https://staging.docs.start9.com/device-guides/mac/ca.html) - - [Windows](https://staging.docs.start9.com/device-guides/windows/ca.html) - - [Android/Graphene](https://staging.docs.start9.com/device-guides/android/ca.html) - - [iOS](https://staging.docs.start9.com/device-guides/ios/ca.html) From fee03ef4077049513b9f1b161358c44c6b618db5 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 16 Jan 2026 16:25:08 -0700 Subject: [PATCH 2/3] switch to posix strings for language internal --- .../src/app/pages/language.page.ts | 4 +- .../shared/src/i18n/dictionaries/en.ts | 14 +++--- .../shared/src/i18n/i18n.providers.ts | 4 +- web/projects/shared/src/i18n/i18n.service.ts | 45 +++++++++++++------ web/projects/shared/src/util/languages.ts | 22 ++++----- web/projects/ui/src/app/app.component.ts | 2 +- .../routes/general/general.component.ts | 36 +++++++-------- .../ui/src/app/services/api/mock-patch.ts | 2 +- 8 files changed, 71 insertions(+), 58 deletions(-) diff --git a/web/projects/setup-wizard/src/app/pages/language.page.ts b/web/projects/setup-wizard/src/app/pages/language.page.ts index b14cc3d0b..f30953d1e 100644 --- a/web/projects/setup-wizard/src/app/pages/language.page.ts +++ b/web/projects/setup-wizard/src/app/pages/language.page.ts @@ -122,13 +122,13 @@ export default class LanguagePage { constructor() { if (this.selected) { - this.i18nService.setLanguage(this.selected.name) + this.i18nService.setLang(this.selected.name) } } onLanguageChange(language: Language) { if (language) { - this.i18nService.setLanguage(language.name) + this.i18nService.setLang(language.name) } } diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index 1aef6d5e3..a2bc6e74b 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -56,7 +56,7 @@ export const ENGLISH = { 'Beginning shutdown': 57, 'Add': 58, 'Ok': 59, - 'french': 60, + 'fr_FR': 60, 'This value cannot be changed once set': 61, 'Continue': 62, 'Click or drop file here': 63, @@ -462,10 +462,10 @@ export const ENGLISH = { 'StartOS UI': 485, 'WiFi': 486, 'Documentation': 487, // as in, a website to view documentation - 'spanish': 488, - 'polish': 489, - 'german': 490, - 'english': 491, + 'es_ES': 488, + 'pl_PL': 489, + 'de_DE': 490, + 'en_US': 491, 'Start Menu': 492, 'Install Progress': 493, 'Downloading': 494, @@ -670,9 +670,9 @@ export const ENGLISH = { 'Preserve': 706, 'Overwrite': 707, 'Unlock': 708, - 'Drive': 709, // as in, a storage device + 'Drive': 709, // the noun, a storage device 'Transfer': 710, // the verb 'The list is empty': 711, 'Restart now': 712, 'Later': 713, // as in, (do it) later -} as const +} as Record diff --git a/web/projects/shared/src/i18n/i18n.providers.ts b/web/projects/shared/src/i18n/i18n.providers.ts index cee7f1aa9..5aa38998c 100644 --- a/web/projects/shared/src/i18n/i18n.providers.ts +++ b/web/projects/shared/src/i18n/i18n.providers.ts @@ -6,7 +6,7 @@ import { TuiLanguageSwitcherService, } from '@taiga-ui/i18n' import { ENGLISH } from './dictionaries/en' -import { i18nService } from './i18n.service' +import { i18nService, Languages } from './i18n.service' export type i18nKey = keyof typeof ENGLISH export type i18n = Record<(typeof ENGLISH)[i18nKey], string> @@ -20,7 +20,7 @@ export const I18N_LOADER = new InjectionToken< >('') export const I18N_STORAGE = new InjectionToken< - (lang: TuiLanguageName) => Promise + (lang: Languages) => Promise >('', { factory: () => () => Promise.resolve(), }) diff --git a/web/projects/shared/src/i18n/i18n.service.ts b/web/projects/shared/src/i18n/i18n.service.ts index b627cddbf..28f1bc86a 100644 --- a/web/projects/shared/src/i18n/i18n.service.ts +++ b/web/projects/shared/src/i18n/i18n.service.ts @@ -2,6 +2,20 @@ import { inject, Injectable, signal } from '@angular/core' import { TuiLanguageName, TuiLanguageSwitcherService } from '@taiga-ui/i18n' import { I18N, I18N_LOADER, I18N_STORAGE } from './i18n.providers' +export const languages = ['en_US', 'es_ES', 'de_DE', 'fr_FR', 'pl_PL'] as const +export type Languages = (typeof languages)[number] + +/** + * Maps POSIX locale strings to TUI language names + */ +export const LANGUAGE_TO_TUI: Record = { + en_US: 'english', + es_ES: 'spanish', + de_DE: 'german', + fr_FR: 'french', + pl_PL: 'polish', +} + @Injectable({ providedIn: 'root', }) @@ -12,20 +26,32 @@ export class i18nService extends TuiLanguageSwitcherService { readonly loading = signal(false) - override setLanguage(language: TuiLanguageName = 'english'): void { + /** + * Current language as POSIX locale string + */ + get lang(): Languages { + return ( + (Object.entries(LANGUAGE_TO_TUI).find( + ([, tui]) => tui === this.language, + )?.[0] as Languages) || 'en_US' + ) + } + + setLang(language: Languages = 'en_US'): void { + const tuiLang = LANGUAGE_TO_TUI[language] const current = this.language - super.setLanguage(language) + super.setLanguage(tuiLang) this.loading.set(true) - if (current === language) { - this.i18nLoader(language).then(value => { + if (current === tuiLang) { + this.i18nLoader(tuiLang).then(value => { this.i18n.set(value) this.loading.set(false) }) } else { this.store(language).then(() => - this.i18nLoader(language).then(value => { + this.i18nLoader(tuiLang).then(value => { this.i18n.set(value) this.loading.set(false) }), @@ -33,12 +59,3 @@ export class i18nService extends TuiLanguageSwitcherService { } } } - -export const languages = [ - 'english', - 'spanish', - 'polish', - 'german', - 'french', -] as const -export type Languages = (typeof languages)[number] diff --git a/web/projects/shared/src/util/languages.ts b/web/projects/shared/src/util/languages.ts index b2b24c78a..baf1e03a4 100644 --- a/web/projects/shared/src/util/languages.ts +++ b/web/projects/shared/src/util/languages.ts @@ -18,22 +18,22 @@ export interface Language { * Available languages with their metadata */ export const LANGUAGES: Language[] = [ - { code: 'en', name: 'english', nativeName: 'English' }, - { code: 'es', name: 'spanish', nativeName: 'Español' }, - { code: 'de', name: 'german', nativeName: 'Deutsch' }, - { code: 'fr', name: 'french', nativeName: 'Français' }, - { code: 'pl', name: 'polish', nativeName: 'Polski' }, + { code: 'en', name: 'en_US', nativeName: 'English' }, + { code: 'es', name: 'es_ES', nativeName: 'Español' }, + { code: 'de', name: 'de_DE', nativeName: 'Deutsch' }, + { code: 'fr', name: 'fr_FR', nativeName: 'Français' }, + { code: 'pl', name: 'pl_PL', nativeName: 'Polski' }, ] /** - * Maps i18n language names to ISO language codes + * Maps POSIX locale strings to ISO language codes */ export const LANGUAGE_TO_CODE: Record = { - english: 'en', - spanish: 'es', - german: 'de', - french: 'fr', - polish: 'pl', + en_US: 'en', + es_ES: 'es', + de_DE: 'de', + fr_FR: 'fr', + pl_PL: 'pl', } /** diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 0c359da07..1ef65c6fb 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -42,6 +42,6 @@ export class AppComponent { .watch$('serverInfo', 'language') .pipe(takeUntilDestroyed()) .subscribe(language => { - this.i18n.setLanguage(language || 'english') + this.i18n.setLang(language || 'en_US') }) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index d77640356..3adf6fc83 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -19,13 +19,13 @@ import { i18nService, Keyboard, KeyboardCode, - languages, - Languages, + Language, + LANGUAGES, LANGUAGE_TO_CODE, LoadingService, } from '@start9labs/shared' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' -import { TuiAnimated, TuiContext, TuiStringHandler } from '@taiga-ui/cdk' +import { TuiAnimated } from '@taiga-ui/cdk' import { TuiAppearance, TuiButton, @@ -110,20 +110,16 @@ import { KeyboardSelectComponent } from './keyboard-select.component' {{ 'Language' | i18n }} - - @if (language; as lang) { - {{ lang | i18n }} - } @else { - {{ i18nService.language }} - } + + {{ currentLanguage?.nativeName }} + + {{ item.nativeName }} +
@@ -297,17 +296,14 @@ export default class SystemGeneralComponent { readonly score = toSignal(this.patch.watch$('ui', 'snakeHighScore')) readonly os = inject(OSService) readonly i18nService = inject(i18nService) - readonly languages = languages - readonly translation: TuiStringHandler> = ({ - $implicit, - }) => { - const [head = '', ...result] = this.i18n.transform($implicit) || '' + readonly languages = LANGUAGES - return [head.toUpperCase(), ...result].join('') + get currentLanguage(): Language | undefined { + return LANGUAGES.find(lang => lang.name === this.i18nService.lang) } - get language(): Languages | undefined { - return this.languages.find(lang => lang === this.i18nService.language) + onLanguageChange(language: Language) { + this.i18nService.setLang(language.name) } // Expose shared utilities for template use diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 86e124d8a..06b1ef711 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -219,7 +219,7 @@ export const mockPatchData: DataModel = { ram: 8 * 1024 * 1024 * 1024, devices: [], kiosk: true, - language: 'english', + language: 'en_US', keyboard: { layout: 'us', model: null, From 5ecb230bcc6b9ed9ee9529c9f3032083911adc71 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 16 Jan 2026 16:25:36 -0700 Subject: [PATCH 3/3] revert mock --- web/projects/shared/src/i18n/dictionaries/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index a2bc6e74b..08e9382be 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -675,4 +675,4 @@ export const ENGLISH = { 'The list is empty': 711, 'Restart now': 712, 'Later': 713, // as in, (do it) later -} as Record +} as const