From f620f87432e5affb84bdc14ca0bb0f5a8bb5c534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Wed, 30 Sep 2026 12:27:21 +0500 Subject: [PATCH] v0.1.1: language picker on welcome screen, friendly errors for unpublished package / unreachable server, backlog notes Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- crates/coa-core/src/error.rs | 18 ++++++++++++++++++ crates/coa-core/src/pkgsource.rs | 5 ++++- docs/ARCHITECTURE.md | 13 +++++++++++++ package.json | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/LanguagePicker.tsx | 24 ++++++++++++++++++++++++ src/i18n/locales/de.ts | 6 ++++++ src/i18n/locales/en.ts | 6 ++++++ src/i18n/locales/es.ts | 6 ++++++ src/i18n/locales/fr.ts | 6 ++++++ src/i18n/locales/ru.ts | 6 ++++++ src/screens/SettingsHome.tsx | 18 ++++-------------- src/screens/Welcome.tsx | 4 +++- 15 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 src/components/LanguagePicker.tsx diff --git a/Cargo.lock b/Cargo.lock index 6382546..a88640e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,7 @@ dependencies = [ [[package]] name = "coa-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "base64 0.22.1", "chrono", @@ -375,7 +375,7 @@ dependencies = [ [[package]] name = "coa-release" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "coa-core", @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "coa-server-manager" -version = "0.1.0" +version = "0.1.1" dependencies = [ "coa-core", "serde", diff --git a/Cargo.toml b/Cargo.toml index fe5a2c0..9de8b54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/coa-core", "crates/coa-release", "src-tauri"] [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2021" [profile.release] diff --git a/crates/coa-core/src/error.rs b/crates/coa-core/src/error.rs index cb94b4e..8be23ed 100644 --- a/crates/coa-core/src/error.rs +++ b/crates/coa-core/src/error.rs @@ -20,6 +20,10 @@ pub enum Error { UnknownInstallation(String), #[error("{0}")] Invalid(String), + #[error("package not published: {0}")] + PackageNotPublished(String), + #[error("download server unreachable: {0}")] + NetworkUnreachable(String), #[error("invalid values: {}", .0.iter().map(|f| format!("{}: {}", f.key, f.message)).collect::>().join("; "))] Validation(Vec), } @@ -46,6 +50,8 @@ pub enum ErrorCode { StartupFailed, HashMismatch, PathRejected, + PackageNotPublished, + NetworkUnreachable, Unknown, } @@ -126,6 +132,16 @@ impl ErrorCode { "The requested file location is outside the server folder.", &[ShowDetails], ), + ErrorCode::PackageNotPublished => ( + "This server package is not available yet", + "Nothing has been published at the download location. Try again later, or install from a local package folder (Advanced).", + &[Retry, ShowDetails], + ), + ErrorCode::NetworkUnreachable => ( + "Could not reach the download server", + "Check your internet connection and try again.", + &[Retry, ShowDetails], + ), ErrorCode::Unknown => ("Something went wrong", "See the technical details.", &[ShowDetails]), }; Human { code: self, title, message, actions } @@ -139,6 +155,8 @@ impl Error { Error::HashMismatch { .. } => ErrorCode::HashMismatch, Error::PathRejected(_) => ErrorCode::PathRejected, Error::Validation(_) => ErrorCode::InvalidConfigValue, + Error::PackageNotPublished(_) => ErrorCode::PackageNotPublished, + Error::NetworkUnreachable(_) => ErrorCode::NetworkUnreachable, _ => ErrorCode::Unknown, } } diff --git a/crates/coa-core/src/pkgsource.rs b/crates/coa-core/src/pkgsource.rs index 567da7a..4e61471 100644 --- a/crates/coa-core/src/pkgsource.rs +++ b/crates/coa-core/src/pkgsource.rs @@ -28,7 +28,10 @@ pub fn fetch_small(source: &Source, name: &str) -> Result> { let url = format!("{}/{name}", base.trim_end_matches('/')); download::check_url(&url)?; let t = download::HttpTransport::new()?; - let reply = download::Transport::get(&t, &url, 0).map_err(|e| Error::Invalid(format!("Could not reach the download server: {e}")))?; + let reply = download::Transport::get(&t, &url, 0).map_err(|e| Error::NetworkUnreachable(format!("{e}")))?; + if reply.status == 404 || reply.status == 410 { + return Err(Error::PackageNotPublished(format!("the download server answered {} for {name} ({url})", reply.status))); + } if reply.status != 200 { return Err(Error::Invalid(format!("The download server answered {} for {name}.", reply.status))); } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 38786a4..514ba08 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -483,3 +483,16 @@ The reference install classifies as **Healthy (customised)**: repack shape, bina `Intl`; settings-schema titles/descriptions (`schemas/*.json`) get per-locale overlays (`schemas/i18n/.json`); translations are reviewed by native speakers before release, with an English fallback for missing keys and a CI check that every locale has every key. Do this before the first public release so strings are not retro-fitted. + + **Status (2026-09-30):** catalogue + picker + all screens translated (en/ru/de/fr/es drafts, `tools/check-i18n.mjs` checks + keys/placeholders). Still English: settings-schema titles/descriptions/presets, companion size names, diagnostics check + texts and update step names coming from the backend (need `schemas/i18n/.json` overlays and code-keyed backend strings). + +* **Language choice on the very first screen** (requested 2026-09-30): the welcome screen must offer the language picker + right away (before any other text is read), preselected from the OS language; the choice is persisted and shared with + Settings. **Done 2026-09-30:** shared `LanguagePicker` (top-right of the welcome screen and in Settings). + +* **Clean-install gap found by the owner (2026-09-30):** installing from the default URL fails with 404 because the `base` + release does not exist yet (only `edge` is published). The message is now human ("package not available yet"). Also open: + the base package has no `Data\` (maps/dbc/vmaps, ~3.8 GB); the installer needs a "use my existing Data folder / download" + step, and the owner must decide hosting/legal for game data before `base` is published. diff --git a/package.json b/package.json index 1b01a47..394ac8c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coa-server-manager", "private": true, - "version": "0.1.0", + "version": "0.1.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 26de351..c0296a7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CoA Server Manager", - "version": "0.1.0", + "version": "0.1.1", "identifier": "com.coa.servermanager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/LanguagePicker.tsx b/src/components/LanguagePicker.tsx new file mode 100644 index 0000000..70a32b9 --- /dev/null +++ b/src/components/LanguagePicker.tsx @@ -0,0 +1,24 @@ +import { Globe } from "lucide-react"; +import { LOCALES, useI18n, type Locale } from "@/i18n"; + +/** Language switch shared by the welcome screen and Settings. Names are shown in their own language. */ +export function LanguagePicker({ className }: { className?: string }) { + const { t, locale, setLocale } = useI18n(); + return ( + + ); +} diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 07ab57b..91b501a 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -416,6 +416,12 @@ const de: Partial> = { "set.unsaved_other": "{n} nicht gespeicherte Änderungen", "set.discard": "Verwerfen", "set.save": "Änderungen speichern", + + // Download errors + "err.package_not_published.title": "Dieses Serverpaket ist noch nicht verfügbar", + "err.package_not_published.message": "Am Download-Ort wurde noch nichts veröffentlicht. Versuche es später erneut oder installiere aus einem lokalen Paketordner (Erweitert).", + "err.network_unreachable.title": "Der Download-Server war nicht erreichbar", + "err.network_unreachable.message": "Prüfe deine Internetverbindung und versuche es erneut.", }; export default de; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index db5c990..3109abb 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -416,6 +416,12 @@ const en = { "set.unsaved_other": "{n} unsaved changes", "set.discard": "Discard", "set.save": "Save changes", + + // Download errors + "err.package_not_published.title": "This server package is not available yet", + "err.package_not_published.message": "Nothing has been published at the download location. Try again later, or install from a local package folder (Advanced).", + "err.network_unreachable.title": "Could not reach the download server", + "err.network_unreachable.message": "Check your internet connection and try again.", } as const; export default en; diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 91b0929..19b86eb 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -416,6 +416,12 @@ const es: Partial> = { "set.unsaved_other": "{n} cambios sin guardar", "set.discard": "Descartar", "set.save": "Guardar cambios", + + // Download errors + "err.package_not_published.title": "Este paquete del servidor aún no está disponible", + "err.package_not_published.message": "Aún no se ha publicado nada en la ubicación de descarga. Inténtalo más tarde o instala desde una carpeta de paquete local (Avanzado).", + "err.network_unreachable.title": "No se pudo contactar con el servidor de descargas", + "err.network_unreachable.message": "Comprueba tu conexión a internet e inténtalo de nuevo.", }; export default es; diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 56c4a3a..4dc36a0 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -416,6 +416,12 @@ const fr: Partial> = { "set.unsaved_other": "{n} modifications non enregistrées", "set.discard": "Annuler les modifications", "set.save": "Enregistrer les modifications", + + // Download errors + "err.package_not_published.title": "Ce paquet serveur n’est pas encore disponible", + "err.package_not_published.message": "Rien n’a encore été publié à l’emplacement de téléchargement. Réessayez plus tard ou installez depuis un dossier de paquet local (Avancé).", + "err.network_unreachable.title": "Impossible de joindre le serveur de téléchargement", + "err.network_unreachable.message": "Vérifiez votre connexion internet et réessayez.", }; export default fr; diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 105eff2..1731c8e 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -416,6 +416,12 @@ const ru: Partial> = { "set.unsaved_other": "{n} несохранённых изменений", "set.discard": "Отменить", "set.save": "Сохранить изменения", + + // Download errors + "err.package_not_published.title": "Этот пакет сервера пока недоступен", + "err.package_not_published.message": "В месте загрузки пока ничего не опубликовано. Повторите позже или установите из локальной папки с пакетом (Дополнительно).", + "err.network_unreachable.title": "Не удалось связаться с сервером загрузки", + "err.network_unreachable.message": "Проверьте подключение к интернету и повторите попытку.", }; export default ru; diff --git a/src/screens/SettingsHome.tsx b/src/screens/SettingsHome.tsx index 9b4af71..3ddceeb 100644 --- a/src/screens/SettingsHome.tsx +++ b/src/screens/SettingsHome.tsx @@ -8,14 +8,15 @@ import { Card } from "@/components/ui/card"; import { ClientCard } from "@/screens/ClientCard"; import { DiagnosticsCard } from "@/screens/DiagnosticsCard"; import { AboutCard } from "@/screens/AboutCard"; -import { LOCALES, useHuman, useI18n, type Key } from "@/i18n"; +import { useHuman, useI18n, type Key } from "@/i18n"; +import { LanguagePicker } from "@/components/LanguagePicker"; function mb(bytes: number) { return bytes >= 1 << 20 ? `${(bytes / (1 << 20)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`; } export function SettingsHome({ serverId }: { serverId: string }) { - const { t, locale, setLocale } = useI18n(); + const { t } = useI18n(); const human = useHuman(); const [preview, setPreview] = useState(null); const [pending, setPending] = useState(null); @@ -111,18 +112,7 @@ export function SettingsHome({ serverId }: { serverId: string }) {

{t("settings.language")}

{t("settings.languageHint")}

- +
diff --git a/src/screens/Welcome.tsx b/src/screens/Welcome.tsx index 809589b..9e2127a 100644 --- a/src/screens/Welcome.tsx +++ b/src/screens/Welcome.tsx @@ -1,10 +1,12 @@ import { FolderSearch, Download } from "lucide-react"; import { useT } from "@/i18n"; +import { LanguagePicker } from "@/components/LanguagePicker"; export function Welcome({ onImport, onInstall, onBack }: { onImport: () => void; onInstall: () => void; onBack?: () => void }) { const t = useT(); return ( -
+
+

{t("welcome.title")}