Files
DriverVault/API Server/panel/src/components/PocketBaseCard.vue
T
tajniak81andClaude Opus 4.8 b6bb6b1df0 Add a language-switch system with per-language files
Introduce a hand-rolled i18n layer across all three UIs, each reading its
text from per-language JSON files (English base + Polish + Danish). Nothing
in the converted screens hardcodes English any more.

- Web App (Vue): src/i18n/{en,pl,da}.json + index.js exposing t()/tSplit(),
  reactive to the signed-in profile locale. Every view, component, form and
  the status labels in lib/format.js go through t().
- API Server panel (Vue): src/i18n/ with its own localStorage-persisted
  language (the panel has no user profile) and a header language picker.
  Chrome, cards, login and API section titles translated; endpoint reference
  descriptions intentionally kept in English. Rebuilt embedded dist.
- Phone App (Flutter): assets/i18n/ + lib/i18n.dart loaded at startup,
  driven by AppSettings.locale. Nav, login, lock, dashboard, the full
  Settings panel (incl. language picker) and format.dart status labels
  translated; remaining detail screens fall back to English.

Language = the language half of the existing BCP-47 locale; the region half
still drives date/number/currency formatting. Missing keys fall back to
English, and plurals use Intl.PluralRules / Intl.plural so Polish gets the
correct one/few/many forms. Settings flags languages without a translation.

Tests updated to assert the localized (Polish) status wording; all pass.
See TRANSLATIONS.md for the format and how to add a language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:07:48 +02:00

128 lines
4.2 KiB
Vue

<script setup>
import { ref, onMounted } from "vue";
import { request } from "../api";
import { t } from "../i18n";
// Superadmin-only: retarget the PocketBase this server talks to. The change is
// applied at runtime AND persisted to the server's .env, so it survives a
// restart. A blank password means "keep the stored one".
const cfg = ref(null);
const form = ref({ url: "", adminEmail: "", adminPassword: "" });
const probe = ref(null);
const error = ref("");
const notice = ref("");
const busy = ref(false);
async function load() {
try {
cfg.value = await request("/api/admin/pb-config");
form.value = {
url: cfg.value.url,
adminEmail: cfg.value.adminEmail,
adminPassword: "",
};
probe.value = cfg.value.probe;
} catch (e) {
error.value = e.message;
}
}
onMounted(load);
async function test() {
error.value = "";
notice.value = "";
busy.value = true;
try {
probe.value = await request("/api/admin/pb-config/test", {
method: "POST",
body: form.value,
});
notice.value = probe.value.superuser
? t("pocketbase.testOk")
: probe.value.reachable
? t("pocketbase.testReachableNoAuth")
: t("pocketbase.testUnreachable");
} catch (e) {
error.value = e.message;
} finally {
busy.value = false;
}
}
async function save() {
error.value = "";
notice.value = "";
busy.value = true;
try {
const out = await request("/api/admin/pb-config", { method: "PUT", body: form.value });
cfg.value = out.config;
probe.value = out.config.probe;
form.value.adminPassword = "";
notice.value = out.warning || t("pocketbase.savedNotice");
} catch (e) {
error.value = e.message;
} finally {
busy.value = false;
}
}
</script>
<template>
<div class="dh-card overflow-hidden">
<div class="flex items-center justify-between border-b border-subtle px-5 py-4">
<div>
<div class="text-base font-bold tracking-[-0.02em] text-strong">{{ t("pocketbase.title") }}</div>
<p class="mt-0.5 text-xs text-muted">{{ t("pocketbase.subtitle") }}</p>
</div>
<span
v-if="probe"
class="dh-pill"
:class="probe.superuser
? 'bg-success-soft text-success'
: probe.reachable
? 'bg-warning-soft text-warning'
: 'bg-danger-soft text-danger'"
>
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
{{ probe.superuser ? t("pocketbase.connected") : probe.reachable ? t("pocketbase.noSuperuser") : t("pocketbase.unreachable") }}
</span>
</div>
<div class="flex flex-col gap-4 px-5 py-4">
<div>
<label class="dh-label" for="pb-url">{{ t("pocketbase.baseUrl") }}</label>
<input id="pb-url" v-model="form.url" class="dh-input" placeholder="http://10.2.1.10:8027" />
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="dh-label" for="pb-email">{{ t("pocketbase.superuserEmail") }}</label>
<input id="pb-email" v-model="form.adminEmail" class="dh-input" autocomplete="off" />
</div>
<div>
<label class="dh-label" for="pb-password">{{ t("pocketbase.superuserPassword") }}</label>
<input
id="pb-password"
v-model="form.adminPassword"
class="dh-input"
type="password"
autocomplete="new-password"
:placeholder="cfg?.adminConfigured ? t('pocketbase.passwordUnchanged') : t('pocketbase.passwordNotSet')"
/>
</div>
</div>
<p v-if="probe?.detail" class="data text-xs text-muted">{{ probe.detail }}</p>
<p v-if="notice" class="rounded-control bg-info-soft px-3 py-2 text-xs text-info">{{ notice }}</p>
<p v-if="error" class="rounded-control bg-danger-soft px-3 py-2 text-xs text-danger">{{ error }}</p>
<div class="flex items-center gap-2">
<button class="dh-btn" :disabled="busy" @click="save">{{ t("pocketbase.saveApply") }}</button>
<button class="dh-btn-ghost" :disabled="busy" @click="test">{{ t("pocketbase.testConnection") }}</button>
<span class="flex-1"></span>
<span class="eyebrow">{{ t("pocketbase.persistedEnv") }}</span>
</div>
</div>
</div>
</template>