From f6546bb3ff1e791dbfdf024ffe6982116d1dd5a3 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Mon, 8 Jun 2026 12:35:07 +0200 Subject: [PATCH] Add Ukrainian translation --- README.en.md | 6 +- README.md | 6 +- package-lock.json | 4 +- package.json | 2 +- public/service-worker.js | 2 +- src/App.tsx | 87 +++++--- src/graph.test.ts | 7 + src/i18n.test.ts | 50 +++++ src/i18n.tsx | 148 +++++++++++-- src/i18n.uk.ts | 438 +++++++++++++++++++++++++++++++++++++++ src/labels.uk.ts | 134 ++++++++++++ src/styles.css | 7 + 12 files changed, 836 insertions(+), 55 deletions(-) create mode 100644 src/i18n.test.ts create mode 100644 src/i18n.uk.ts create mode 100644 src/labels.uk.ts diff --git a/README.en.md b/README.en.md index 8f08c4f..cb6e981 100644 --- a/README.en.md +++ b/README.en.md @@ -51,9 +51,11 @@ The user owns their data locally in the browser and actively shares it by export - advisor notes per user, report and follow-up - fullscreen graph analysis for advisors - local report integrity checks -- Norwegian and English user interface +- Norwegian, English and Ukrainian user interface - installable as a PWA/WebApp on phones and tablets +The language selector only translates static UI text and display labels. Report content, comments, advisor notes and imported JSON are never changed. + --- ## Screenshots @@ -325,7 +327,7 @@ Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), - JSON import/export - local advisor view - optional FastAPI/SQLite backend for encrypted one-time sharing -- Norwegian/English UI +- Norwegian/English/Ukrainian UI ### Production diff --git a/README.md b/README.md index d886c46..1817a33 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,11 @@ Brukeren eier dataene sine lokalt i nettleseren og deler dem aktivt ved å ekspo - veiledernotater per bruker, rapport og oppfølging - fullskjerm grafanalyse for veileder - lokal integritetskontroll av rapporter -- norsk og engelsk brukergrensesnitt +- norsk, engelsk og ukrainsk brukergrensesnitt - kan installeres som PWA/WebApp på telefon og nettbrett +Språkvalget oversetter bare statiske tekster og visningsetiketter. Rapportinnhold, kommentarer, veiledernotater og importert JSON endres ikke. + --- ## Skjermbilder @@ -325,7 +327,7 @@ Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), - JSON import/eksport - lokal veiledervisning - valgfri FastAPI/SQLite-backend for kryptert engangsdeling -- norsk/engelsk UI +- norsk/engelsk/ukrainsk UI ### Produksjon diff --git a/package-lock.json b/package-lock.json index c5cd0c4..c34f50d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arbeidspuls", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arbeidspuls", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "lucide-react": "^0.475.0", "react": "^19.0.0", diff --git a/package.json b/package.json index d039014..3058d64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arbeidspuls", - "version": "1.3.0", + "version": "1.3.1", "private": true, "type": "module", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 41193af..a80345d 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE_NAME = "arbeidspuls-v9"; +const CACHE_NAME = "arbeidspuls-v10"; const ASSETS = ["/", "/index.html", "/manifest.webmanifest", "/icon.svg"]; self.addEventListener("install", (event) => { diff --git a/src/App.tsx b/src/App.tsx index b24b71e..b093489 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -71,6 +71,8 @@ import { } from "./feedback"; import { I18nProvider, + localeForLanguage, + readStoredLanguage, translateScoreBand, translateScoreBandDescription, translateVerificationStatus, @@ -129,7 +131,7 @@ type LineVisibility = "normal" | "dim" | "hidden"; type TrendMetric = { key: keyof Report | "total_score_percent"; label: string; short: string; color: string }; const PERIOD_KEY = "arbeidsevne-egenvurdering:trend-period"; -const APP_VERSION = "1.3.0"; +const APP_VERSION = "1.3.1"; const COMMERCIAL_LICENSE_URL = "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Arbeidspuls/src/branch/main/about-commercial-license.md"; const taskTypeOptions = Object.keys(taskTypeLabels) as TaskType[]; @@ -137,14 +139,23 @@ const limitationOptions = Object.keys(limitationLabels) as Limitation[]; const accommodationOptions = Object.keys(accommodationLabels) as Accommodation[]; const delayedSymptomOptions = Object.keys(delayedSymptomLabels) as DelayedSymptom[]; const visibleStatusOptions: ReportStatus[] = ["kan_fortsette", "trenger_pause", "trenger_enklere_oppgave", "bør_stoppe"]; -const trendMetrics: TrendMetric[] = [ - { key: "total_score_percent", label: "Totalscore", short: "T", color: "#235b5e" }, - { key: "work_ability", label: "Arbeidsevne", short: "A", color: "#307f78" }, - { key: "energy_level", label: "Energi", short: "E", color: "#b2642a" }, - { key: "mental_clarity", label: "Klarhet", short: "K", color: "#4f5c9c" }, - { key: "symptom_burden", label: "Symptomer", short: "S", color: "#b23b4a" }, - { key: "effort_strain", label: "Belastning", short: "B", color: "#6b5a3a" } -]; +const trendMetricDefinitions = [ + { key: "total_score_percent", labelKey: "metricTotalScore", shortKey: "metricTotalScoreShort", color: "#235b5e" }, + { key: "work_ability", labelKey: "metricWorkAbility", shortKey: "metricWorkAbilityShort", color: "#307f78" }, + { key: "energy_level", labelKey: "metricEnergy", shortKey: "metricEnergyShort", color: "#b2642a" }, + { key: "mental_clarity", labelKey: "metricClarity", shortKey: "metricClarityShort", color: "#4f5c9c" }, + { key: "symptom_burden", labelKey: "metricSymptoms", shortKey: "metricSymptomsShort", color: "#b23b4a" }, + { key: "effort_strain", labelKey: "metricStrain", shortKey: "metricStrainShort", color: "#6b5a3a" } +] as const; + +function localizedTrendMetrics(t: ReturnType["t"]): TrendMetric[] { + return trendMetricDefinitions.map((metric) => ({ + key: metric.key, + label: t(`graphModal.${metric.labelKey}`), + short: t(`graphModal.${metric.shortKey}`), + color: metric.color + })); +} function App() { return ( @@ -609,6 +620,16 @@ function AppFooter({ > {t("footer.english")} 🇬🇧 + | +
© {yearLabel} {t("footer.developedBy")} @@ -1511,7 +1532,7 @@ function Trend({ entries, period, showScoreBands = false }: { entries: StoredEnt const workTimeBands = useMemo(() => filterWorkTimeBandsByPeriod(buildWorkTimeBands(entries), period), [entries, period]); const periodAssessment = showScoreBands ? calculatePeriodWorkAbilityAssessment(reports) : null; - const metrics = trendMetrics; + const metrics = localizedTrendMetrics(t); if (reports.length < 2) { return ( @@ -1724,6 +1745,7 @@ function FullscreenGraphModal({ onClose: () => void; }) { const { t } = useI18n(); + const metrics = localizedTrendMetrics(t); const [period, setPeriod] = useState(initialPeriod); const [visibility, setVisibility] = useState>({ total_score_percent: "dim", followup_response: "normal" }); const [inspect, setInspect] = useState<{ x: number; report: Report | null } | null>(null); @@ -1825,7 +1847,7 @@ function FullscreenGraphModal({

{t("graphModal.visibleHelp")}

- {trendMetrics.map((metric) => { + {metrics.map((metric) => { const state = visibility[metric.key] ?? "normal"; return (