Webbplatsen på alla tio språken
Sajten var hårdkodad engelska medan diagnostikytan redan talade tio språk. Nu hämtar hela den publika webben — startsidan, ansökan, inloggningen, språksidan, navigationen och sidfoten — sin text ur samma katalog som resten av produkten, under nytt prefix webb. (153 nycklar × 10 språk). Språkval på webben: besökarens eget val (väljare i sidfoten, sparat i localStorage med minnesfallback för privat läge) → webbläsarens språk → engelska. Rangordningen bor kvar i valjSprak; ny modul alva/webbsprak.ts bär bara lagring och React-kroken. Portalens länkar står medvetet kvar på engelska — portalen är produkten och styrs av konto och organisation, inte av ett besöksval i en sidfot. Gränsen håller: ordmärket, utläsningen och fasnamnen är ALVA:s struktur och står likadant i varje språk; lang-attributet följer valet; språksidans förhandsvisning behåller egen väljare och utgår från besökarens språk. Test: startsidans hjälterad och faskort låses mot systemdefinitionen (två källor för samma mening driver isär tyst), webbnycklarna krävs i varje språk, och designtesterna jämför nu katalognycklar i stället för strängar — starkare, eftersom samma nyckel är samma ord på alla språk. Ny språkrunda mot den bakade artefakten: väljare, sidbyte, webbläsarspråk och engelsk fallback, 11/11. 775 enhetstester, genomgång 4/4, portalspärr, integrationstest, artefaktmätning, typkontroll och lint gröna. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// ALVA-SPEC-060 · Webbplatsens språk.
|
||||
//
|
||||
// Plattformen väljer språk ur konto → organisation → webbläsare. Den
|
||||
// publika webbplatsen har inget konto och ingen organisation — där är
|
||||
// kedjan besökarens eget val (sparat lokalt) → webbläsaren → engelska.
|
||||
// Rangordningen ligger kvar i `valjSprak`, så att "hur språk väljs" bara
|
||||
// finns implementerat på ett ställe.
|
||||
//
|
||||
// Valet lagras i localStorage men hålls också i minnet: en webbläsare i
|
||||
// privat läge kan neka lagring, och då ska väljaren ändå fungera för
|
||||
// sidvisningen i stället för att tyst göra ingenting.
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { STANDARD, valjSprak } from "../../../services/gemensam/sprak/index.mjs";
|
||||
|
||||
const NYCKEL = "alva.webbsprak";
|
||||
const lyssnare = new Set<() => void>();
|
||||
let minne: string | undefined;
|
||||
|
||||
function lagratVal(): string | undefined {
|
||||
try {
|
||||
return localStorage.getItem(NYCKEL) ?? minne;
|
||||
} catch {
|
||||
return minne;
|
||||
}
|
||||
}
|
||||
|
||||
/** Webbplatsens språk just nu: eget val → webbläsare → engelska. */
|
||||
export function webbSprak(): string {
|
||||
const webblasare =
|
||||
typeof navigator === "undefined" ? [] : (navigator.languages ?? [navigator.language]);
|
||||
return valjSprak({ anvandare: lagratVal(), webblasare }) as string;
|
||||
}
|
||||
|
||||
/** Sätter besökarens val och meddelar alla vyer som lyssnar. */
|
||||
export function sattWebbSprak(kod: string): void {
|
||||
minne = kod;
|
||||
try {
|
||||
localStorage.setItem(NYCKEL, kod);
|
||||
} catch {
|
||||
// Se filhuvudet: minnesvärdet bär valet när lagringen nekar.
|
||||
}
|
||||
for (const meddela of lyssnare) meddela();
|
||||
}
|
||||
|
||||
/** React-krok: webbplatsens språk, med omritning vid byte. */
|
||||
export function useWebbSprak(): string {
|
||||
return useSyncExternalStore(
|
||||
(meddela) => {
|
||||
lyssnare.add(meddela);
|
||||
return () => lyssnare.delete(meddela);
|
||||
},
|
||||
webbSprak,
|
||||
() => STANDARD as string,
|
||||
);
|
||||
}
|
||||
@@ -339,22 +339,30 @@ describe("ytor utan verklig funktion är märkta som demonstration", () => {
|
||||
// En märkning som inte går att missförstå säger två saker: att ingenting
|
||||
// sker, och vad läsaren ska göra i stället.
|
||||
describe("demonstrationsmärkningen är entydig", () => {
|
||||
// Texterna bor sedan flerspråkigheten i katalogen. Garantin är därför
|
||||
// tvådelad: källtexten säger det som ska sägas, OCH sidan hämtar just
|
||||
// den nyckeln — annars kan en riktig text stå oanvänd i katalogen.
|
||||
const sida = (namn: string) => filer.find(([n]) => n.endsWith(`pages/alva/${namn}`))![1];
|
||||
|
||||
it("inloggningen säger vad besökaren ska göra", () => {
|
||||
const kod = sida("LoggaIn.tsx");
|
||||
expect(kod).toContain("Type anything");
|
||||
expect(kod).toContain("no account is needed");
|
||||
it("inloggningen säger vad besökaren ska göra", async () => {
|
||||
const { EN } = await import("../../../../services/gemensam/sprak/en.mjs");
|
||||
expect(EN["webb.login.demo"]).toContain("Type anything");
|
||||
expect(EN["webb.login.demo"]).toContain("no account is needed");
|
||||
expect(sida("LoggaIn.tsx")).toContain('t("webb.login.demo")');
|
||||
});
|
||||
|
||||
it("ansökan säger att det inte finns någon mottagning", () => {
|
||||
expect(sida("Ansokan.tsx")).toContain("there is no intake behind it yet");
|
||||
it("ansökan säger att det inte finns någon mottagning", async () => {
|
||||
const { EN } = await import("../../../../services/gemensam/sprak/en.mjs");
|
||||
expect(EN["webb.ansokan.demo"]).toContain("there is no intake behind it yet");
|
||||
expect(sida("Ansokan.tsx")).toContain('t("webb.ansokan.demo")');
|
||||
});
|
||||
|
||||
it("beskrivningen av avsedd drift är märkt som avsedd, inte som gällande", () => {
|
||||
it("beskrivningen av avsedd drift är märkt som avsedd, inte som gällande", async () => {
|
||||
// Två meningar bredvid varandra, där den ena säger att inget händer
|
||||
// och den andra beskriver en handläggningsprocess, upphäver varandra.
|
||||
expect(sida("Ansokan.tsx")).toContain("Intended operation:");
|
||||
const { EN } = await import("../../../../services/gemensam/sprak/en.mjs");
|
||||
expect(EN["webb.ansokan.avsikt"]).toContain("Intended operation:");
|
||||
expect(sida("Ansokan.tsx")).toContain('t("webb.ansokan.avsikt")');
|
||||
});
|
||||
|
||||
it("ingen märkning hänvisar till något som läsaren inte kan nå", () => {
|
||||
@@ -432,7 +440,7 @@ describe("portalen är stängd, eller uttryckligen en demonstration", () => {
|
||||
expect(loggaIn).toContain("loggaInPlattform");
|
||||
// Och skylten står kvar exakt när den ska: bara i det läge där
|
||||
// inloggningen inte prövar något.
|
||||
expect(loggaIn).toMatch(/!skarpt && \(\s*<Demonstration>/);
|
||||
expect(loggaIn).toMatch(/!skarpt && (\(\s*)?<Demonstration>/);
|
||||
});
|
||||
|
||||
it("fakturavyn visar organisationens egna fakturor mot en plattform", () => {
|
||||
@@ -465,18 +473,22 @@ describe("inloggningen heter samma sak överallt", () => {
|
||||
const start = readFileSync("src/pages/alva/Start.tsx", "utf8");
|
||||
|
||||
it("navigationens namn på inloggningen är det som används", () => {
|
||||
const namn = ram.match(/\{ till: "\/alva\/logga-in", text: "([^"]+)" \}/)?.[1];
|
||||
expect(namn).toBeTruthy();
|
||||
// Varje länk till inloggningen på startsidan bär samma text.
|
||||
// Sedan flerspråkigheten jämförs KATALOGNYCKLAR, inte strängar — det
|
||||
// är starkare: samma nyckel är samma ord på vartenda språk, medan två
|
||||
// nycklar med samma engelska text hade kunnat glida isär i en enda
|
||||
// översättning.
|
||||
const nyckel = ram.match(/\{ till: "\/alva\/logga-in", nyckel: "([^"]+)" \}/)?.[1];
|
||||
expect(nyckel).toBeTruthy();
|
||||
// Varje länk till inloggningen på startsidan bär samma nyckel.
|
||||
//
|
||||
// Mönstret tål attribut på länken. Första versionen gjorde inte det,
|
||||
// och när <Link> fick en className matchade den noll knappar — testet
|
||||
// slutade mäta i stället för att falla. Det enda som avslöjade det var
|
||||
// kravet nedan på att den ska hitta NÅGOT, vilket är därför det står
|
||||
// där och inte som en självklarhet.
|
||||
const knappar = [...start.matchAll(/to="\/alva\/logga-in"[^>]*>\s*<Knapp[^>]*>([^<]+)<\/Knapp>/g)];
|
||||
const knappar = [...start.matchAll(/to="\/alva\/logga-in"[^>]*>\s*<Knapp[^>]*>\{t\("([^"]+)"\)\}<\/Knapp>/g)];
|
||||
expect(knappar.length).toBeGreaterThan(0);
|
||||
for (const [, text] of knappar) expect(text.trim()).toBe(namn);
|
||||
for (const [, n] of knappar) expect(n).toBe(nyckel);
|
||||
});
|
||||
|
||||
it("ingen ingång kallar verkstaden för kund", () => {
|
||||
|
||||
@@ -194,6 +194,31 @@ describe("metodikvarningen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("webbplatsens källtext", () => {
|
||||
// Hjälteraden och metodkorten på startsidan hämtas ur katalogen, men
|
||||
// systemdefinitionen i @/alva/system är normen. Två källor för samma
|
||||
// mening driver isär tyst — testet gör skillnaden till ett byggfel.
|
||||
it("hjälteraden och faskorten säger samma sak som systemdefinitionen", async () => {
|
||||
const { ALVA, FASER } = await import("../../alva/system");
|
||||
expect(EN["webb.hero.position"]).toBe(ALVA.position);
|
||||
expect(EN["webb.hero.definition"]).toBe(ALVA.definition);
|
||||
for (const f of FASER) {
|
||||
expect(EN[`webb.fas.${f.id}.syfte`], f.id).toBe(f.syfte);
|
||||
expect(EN[`webb.fas.${f.id}.avgransning`], f.id).toBe(f.avgransning);
|
||||
}
|
||||
});
|
||||
|
||||
it("webbnycklarna finns i varje språk — sajten är inte engelska med undantag", () => {
|
||||
const webb = NYCKLAR.filter((n) => n.startsWith("webb."));
|
||||
expect(webb.length).toBeGreaterThan(140);
|
||||
for (const kod of OVRIGA) {
|
||||
for (const n of webb) {
|
||||
expect(KATALOGER[kod][n], `${kod}: ${n}`).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function egetNamn(kod: string): string {
|
||||
return SPRAK.find((s) => s.kod === kod)!.egetNamn;
|
||||
}
|
||||
|
||||
@@ -7,29 +7,32 @@
|
||||
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { Demonstration, Block, Etikett, FARG, Knapp, Rubrik, Statusmärke } from "@/alva/komponenter";
|
||||
import { useWebbSprak } from "@/alva/webbsprak";
|
||||
import { oversattare } from "../../../../services/gemensam/sprak/index.mjs";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
interface Falt {
|
||||
namn: string;
|
||||
etikett: string;
|
||||
nyckel: string;
|
||||
typ?: string;
|
||||
kravs?: boolean;
|
||||
hjalp?: string;
|
||||
}
|
||||
|
||||
const FALT: Falt[] = [
|
||||
{ namn: "foretag", etikett: "Company", kravs: true },
|
||||
{ namn: "orgnummer", etikett: "Organization number", kravs: true },
|
||||
{ namn: "kontakt", etikett: "Contact person", kravs: true },
|
||||
{ namn: "epost", etikett: "Email", typ: "email", kravs: true },
|
||||
{ namn: "telefon", etikett: "Phone", typ: "tel", kravs: true },
|
||||
{ namn: "tekniker", etikett: "Number of technicians", typ: "number", kravs: true },
|
||||
{ namn: "bransch", etikett: "Industry", kravs: true },
|
||||
{ namn: "land", etikett: "Country", kravs: true },
|
||||
{ namn: "anvandare", etikett: "Expected number of users", typ: "number" },
|
||||
{ namn: "foretag", nyckel: "webb.falt.foretag", kravs: true },
|
||||
{ namn: "orgnummer", nyckel: "webb.falt.orgnummer", kravs: true },
|
||||
{ namn: "kontakt", nyckel: "webb.falt.kontakt", kravs: true },
|
||||
{ namn: "epost", nyckel: "webb.falt.epost", typ: "email", kravs: true },
|
||||
{ namn: "telefon", nyckel: "webb.falt.telefon", typ: "tel", kravs: true },
|
||||
{ namn: "tekniker", nyckel: "webb.falt.tekniker", typ: "number", kravs: true },
|
||||
{ namn: "bransch", nyckel: "webb.falt.bransch", kravs: true },
|
||||
{ namn: "land", nyckel: "webb.falt.land", kravs: true },
|
||||
{ namn: "anvandare", nyckel: "webb.falt.anvandare", typ: "number" },
|
||||
];
|
||||
|
||||
export default function Ansokan() {
|
||||
const sprak = useWebbSprak();
|
||||
const t = oversattare(sprak) as (nyckel: string) => string;
|
||||
const [skickad, setSkickad] = useState(false);
|
||||
const [referens, setReferens] = useState("");
|
||||
|
||||
@@ -47,31 +50,26 @@ export default function Ansokan() {
|
||||
return (
|
||||
<Ram>
|
||||
<div className="mx-auto max-w-[680px] px-6 py-16">
|
||||
<Etikett>Application</Etikett>
|
||||
<Etikett>{t("webb.ansokan.mottagen.etikett")}</Etikett>
|
||||
<div className="mt-2 mb-8">
|
||||
<Rubrik niva={1}>Submitted</Rubrik>
|
||||
<Rubrik niva={1}>{t("webb.ansokan.mottagen")}</Rubrik>
|
||||
</div>
|
||||
<Demonstration>
|
||||
Nothing was submitted and no one was notified. The reference below was computed in your
|
||||
browser from what you typed, and exists nowhere else — it will be gone when you close
|
||||
this page.
|
||||
</Demonstration>
|
||||
<Block rubrik="Registration" beteckning={referens}>
|
||||
<Demonstration>{t("webb.ansokan.mottagen.demo")}</Demonstration>
|
||||
<Block rubrik={t("webb.ansokan.etikett")} beteckning={referens}>
|
||||
<dl className="text-[14px] leading-[24px]">
|
||||
<div className="flex justify-between border-b py-2" style={{ borderColor: FARG.lightSteel }}>
|
||||
<dt style={{ color: FARG.steel }}>Status</dt>
|
||||
<dt style={{ color: FARG.steel }}>{t("webb.ansokan.status")}</dt>
|
||||
<dd>
|
||||
<Statusmärke status="pending" />
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between py-2">
|
||||
<dt style={{ color: FARG.steel }}>Reference</dt>
|
||||
<dt style={{ color: FARG.steel }}>{t("webb.ansokan.referens")}</dt>
|
||||
<dd className="font-mono">{referens}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-4 border-t pt-4 text-[13px] leading-[20px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
|
||||
Applications are reviewed manually. An invoice is issued on approval. The organization is activated
|
||||
once payment is registered.
|
||||
{t("webb.ansokan.granskning")}
|
||||
</p>
|
||||
</Block>
|
||||
</div>
|
||||
@@ -82,18 +80,13 @@ export default function Ansokan() {
|
||||
return (
|
||||
<Ram>
|
||||
<div className="mx-auto max-w-[680px] px-6 py-16">
|
||||
<Etikett>Registration</Etikett>
|
||||
<Etikett>{t("webb.ansokan.etikett")}</Etikett>
|
||||
<div className="mt-2 mb-2">
|
||||
<Rubrik niva={1}>Request account</Rubrik>
|
||||
<Rubrik niva={1}>{t("webb.ansok")}</Rubrik>
|
||||
</div>
|
||||
<Demonstration>
|
||||
Nothing is sent. Submitting this form stores nothing, notifies no one, and creates no
|
||||
application — there is no intake behind it yet. The description below states how
|
||||
registration is intended to work, not what happens today.
|
||||
</Demonstration>
|
||||
<Demonstration>{t("webb.ansokan.demo")}</Demonstration>
|
||||
<p className="mb-8 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
Intended operation: applications are reviewed manually and no payment is taken at this
|
||||
stage.
|
||||
{t("webb.ansokan.avsikt")}
|
||||
</p>
|
||||
|
||||
<form onSubmit={skicka} noValidate={false}>
|
||||
@@ -101,9 +94,9 @@ export default function Ansokan() {
|
||||
{FALT.map((f) => (
|
||||
<div key={f.namn} className="border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
|
||||
<label htmlFor={f.namn} className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
{f.etikett}
|
||||
{t(f.nyckel)}
|
||||
{f.kravs && <span aria-hidden="true"> ·</span>}
|
||||
{f.kravs && <span className="sr-only"> (required)</span>}
|
||||
{f.kravs && <span className="sr-only"> {t("webb.falt.kravs")}</span>}
|
||||
</label>
|
||||
<input
|
||||
id={f.namn}
|
||||
@@ -117,7 +110,7 @@ export default function Ansokan() {
|
||||
))}
|
||||
<div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
|
||||
<label htmlFor="noteringar" className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
Notes
|
||||
{t("webb.falt.noteringar")}
|
||||
</label>
|
||||
<textarea
|
||||
id="noteringar"
|
||||
@@ -129,7 +122,7 @@ export default function Ansokan() {
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<Knapp type="submit">Submit application</Knapp>
|
||||
<Knapp type="submit">{t("webb.ansokan.skicka")}</Knapp>
|
||||
</form>
|
||||
</div>
|
||||
</Ram>
|
||||
|
||||
@@ -20,15 +20,19 @@ import { type FormEvent, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { loggaInPlattform, plattformAktiv } from "@/felsokning/plattform";
|
||||
import { Demonstration, Block, Etikett, FARG, Knapp, Rubrik } from "@/alva/komponenter";
|
||||
import { useWebbSprak } from "@/alva/webbsprak";
|
||||
import { oversattare } from "../../../../services/gemensam/sprak/index.mjs";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
const FALT = [
|
||||
{ namn: "epost", etikett: "Email", typ: "email", komplettering: "username" },
|
||||
{ namn: "losenord", etikett: "Password", typ: "password", komplettering: "current-password" },
|
||||
{ namn: "epost", nyckel: "webb.falt.epost", typ: "email", komplettering: "username" },
|
||||
{ namn: "losenord", nyckel: "webb.login.losenord", typ: "password", komplettering: "current-password" },
|
||||
];
|
||||
|
||||
export default function LoggaIn() {
|
||||
const navigera = useNavigate();
|
||||
const sprak = useWebbSprak();
|
||||
const t = oversattare(sprak) as (nyckel: string) => string;
|
||||
const [fel, setFel] = useState("");
|
||||
const [arbetar, setArbetar] = useState(false);
|
||||
const skarpt = plattformAktiv();
|
||||
@@ -47,7 +51,7 @@ export default function LoggaIn() {
|
||||
|
||||
if (!epost || !losenord) {
|
||||
// Statusspråket gäller även fel: ett konstaterande, inte en ursäkt.
|
||||
setFel("Authentication incomplete. Email and password required.");
|
||||
setFel(t("webb.login.ofullstandigt"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +64,7 @@ export default function LoggaIn() {
|
||||
// Serverns egen text visas oförändrad. Den vet vad som hände —
|
||||
// avstängt konto, spärrat efter för många försök, fel uppgifter —
|
||||
// och en omskrivning här skulle bara göra beskedet vagare.
|
||||
setFel(orsak instanceof Error ? orsak.message : "Authentication failed.");
|
||||
setFel(orsak instanceof Error ? orsak.message : t("webb.login.misslyckades"));
|
||||
} finally {
|
||||
setArbetar(false);
|
||||
}
|
||||
@@ -69,18 +73,11 @@ export default function LoggaIn() {
|
||||
return (
|
||||
<Ram>
|
||||
<div className="mx-auto max-w-[440px] px-6 py-24">
|
||||
<Etikett>Access</Etikett>
|
||||
<Etikett>{t("webb.login.etikett")}</Etikett>
|
||||
<div className="mt-2 mb-8">
|
||||
<Rubrik niva={1}>Login</Rubrik>
|
||||
<Rubrik niva={1}>{t("webb.loggain")}</Rubrik>
|
||||
</div>
|
||||
{!skarpt && (
|
||||
<Demonstration>
|
||||
This login authenticates nothing. Type anything into Email and Password and press Sign in
|
||||
— no account is needed, and the portal behind it presents fixed example data. Connected to
|
||||
a platform instance this page authenticates against it, and the portal is closed without a
|
||||
valid session.
|
||||
</Demonstration>
|
||||
)}
|
||||
{!skarpt && <Demonstration>{t("webb.login.demo")}</Demonstration>}
|
||||
|
||||
<form onSubmit={skicka}>
|
||||
<Block>
|
||||
@@ -91,7 +88,7 @@ export default function LoggaIn() {
|
||||
className="block text-[11px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
{f.etikett}
|
||||
{t(f.nyckel)}
|
||||
</label>
|
||||
<input
|
||||
id={f.namn}
|
||||
@@ -113,10 +110,10 @@ export default function LoggaIn() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Knapp type="submit" disabled={arbetar}>
|
||||
{arbetar ? "Signing in" : "Sign in"}
|
||||
{arbetar ? t("webb.login.loggar_in") : t("webb.login.logga_in")}
|
||||
</Knapp>
|
||||
<a href="/alva/ansokan" className="text-[12px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
Forgot password
|
||||
{t("webb.login.glomt")}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,17 +3,29 @@
|
||||
//
|
||||
// Navigationen är en rad text, inte en meny med ikoner. Den som söker
|
||||
// något i ett industrisystem vet vad det heter.
|
||||
//
|
||||
// ---- Språket -----------------------------------------------------------
|
||||
//
|
||||
// Den publika navigationen och sidfoten talar besökarens språk (val i
|
||||
// sidfoten → webbläsare → engelska). Portalens länkar står MEDVETET kvar
|
||||
// på engelska: portalen är produkten, och dess språk styrs av konto och
|
||||
// organisation (ALVA-SPEC-060) — inte av ett besöksval i en sidfot.
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { loggaUtPlattform, plattformKonto } from "@/felsokning/plattform";
|
||||
import { ALVA, PLATTFORMSVERSION } from "@/alva/system";
|
||||
import { FARG } from "@/alva/komponenter";
|
||||
import { useWebbSprak, sattWebbSprak } from "@/alva/webbsprak";
|
||||
import { SPRAK, oversattare } from "../../../../services/gemensam/sprak/index.mjs";
|
||||
|
||||
type Sprakpost = { kod: string; namn: string; egetNamn: string; granskat: boolean };
|
||||
const SPRAKEN = SPRAK as Sprakpost[];
|
||||
|
||||
const PUBLIKT = [
|
||||
{ till: "/alva", text: "Overview" },
|
||||
{ till: "/alva/ansokan", text: "Request account" },
|
||||
{ till: "/alva/logga-in", text: "Login" },
|
||||
{ till: "/alva", nyckel: "webb.nav.oversikt" },
|
||||
{ till: "/alva/ansokan", nyckel: "webb.ansok" },
|
||||
{ till: "/alva/logga-in", nyckel: "webb.loggain" },
|
||||
];
|
||||
|
||||
const PORTAL = [
|
||||
@@ -29,10 +41,20 @@ const PORTAL = [
|
||||
{ till: "/felsokning", text: "Diagnostics" },
|
||||
];
|
||||
|
||||
const FOT = [
|
||||
["/alva/impressum", "webb.fot.impressum"],
|
||||
["/alva/dataskydd", "webb.fot.dataskydd"],
|
||||
["/alva/villkor", "webb.fot.villkor"],
|
||||
["/alva/tillganglighet", "webb.fot.tillganglighet"],
|
||||
["/alva/sprak", "webb.fot.sprak"],
|
||||
["/alva/utgavor", "webb.fot.utgavor"],
|
||||
] as const;
|
||||
|
||||
export function Ram({ children, portal = false }: { children: ReactNode; portal?: boolean }) {
|
||||
const plats = useLocation();
|
||||
const navigera = useNavigate();
|
||||
const lankar = portal ? PORTAL : PUBLIKT;
|
||||
const sprak = useWebbSprak();
|
||||
const t = oversattare(sprak) as (nyckel: string) => string;
|
||||
// Finns en riktig session ska den gå att avsluta. Utan konfigurerad
|
||||
// plattform finns ingen, och då visas ingen utloggning heller — en
|
||||
// knapp som inte loggar ut något är samma sorts sken som m-9 gällde.
|
||||
@@ -41,7 +63,7 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
|
||||
return (
|
||||
// `alva-yta` bär typografin ur ALVA-SPEC-001 och håller den skild
|
||||
// från värdapplikationens antikva. Se src/index.css.
|
||||
<div className="alva-yta min-h-screen" style={{ background: FARG.background, color: FARG.graphite }}>
|
||||
<div lang={sprak} className="alva-yta min-h-screen" style={{ background: FARG.background, color: FARG.graphite }}>
|
||||
<header className="border-b" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
|
||||
<div className="mx-auto flex max-w-[1040px] flex-wrap items-center justify-between gap-4 px-6 py-4">
|
||||
<Link to={portal ? "/alva/portal" : "/alva"} className="flex items-baseline gap-4">
|
||||
@@ -63,8 +85,9 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
|
||||
av marginalerna, inte av att posterna klumpas ihop. */}
|
||||
<nav aria-label={portal ? "Portal" : "Site"} className="w-full sm:w-auto">
|
||||
<ul className="flex flex-wrap justify-between gap-x-4 gap-y-2 sm:justify-end sm:gap-6">
|
||||
{lankar.map((l) => {
|
||||
{(portal ? PORTAL : PUBLIKT).map((l) => {
|
||||
const aktiv = plats.pathname === l.till;
|
||||
const text = "nyckel" in l ? t(l.nyckel) : l.text;
|
||||
return (
|
||||
<li key={l.till}>
|
||||
<Link
|
||||
@@ -73,7 +96,7 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
|
||||
className="text-[12px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: aktiv ? FARG.blue : FARG.steel }}
|
||||
>
|
||||
{l.text}
|
||||
{text}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
@@ -122,30 +145,50 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
|
||||
<div className="mx-auto max-w-[1040px] px-6 py-6">
|
||||
<nav aria-label="Legal">
|
||||
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-[12px]">
|
||||
{[
|
||||
["/alva/impressum", "Impressum"],
|
||||
["/alva/dataskydd", "Privacy"],
|
||||
["/alva/villkor", "Terms"],
|
||||
["/alva/tillganglighet", "Accessibility"],
|
||||
["/alva/sprak", "Languages"],
|
||||
["/alva/utgavor", "Release notes"],
|
||||
].map(([till, text]) => (
|
||||
{FOT.map(([till, nyckel]) => (
|
||||
<li key={till}>
|
||||
<Link to={till} style={{ color: FARG.steel }}>
|
||||
{text}
|
||||
{t(nyckel)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div
|
||||
className="mt-6 flex flex-wrap justify-between gap-4 border-t pt-6 font-mono text-[11px]"
|
||||
className="mt-6 flex flex-wrap items-center justify-between gap-4 border-t pt-6"
|
||||
style={{ borderColor: FARG.lightSteel, color: FARG.steel }}
|
||||
>
|
||||
<Link to="/alva/utgavor" style={{ color: FARG.steel }}>
|
||||
{PLATTFORMSVERSION}
|
||||
</Link>
|
||||
<span>{ALVA.position}</span>
|
||||
{/* Väljaren sätter besökarens val, som väger tyngre än
|
||||
webbläsarens språk men aldrig rör plattformens eget
|
||||
språkval — se filhuvudet. */}
|
||||
<div className="flex items-center gap-4">
|
||||
<label
|
||||
htmlFor="webbsprak"
|
||||
className="text-[11px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
{t("sprak.valj")}
|
||||
</label>
|
||||
<select
|
||||
id="webbsprak"
|
||||
value={sprak}
|
||||
onChange={(h) => sattWebbSprak(h.target.value)}
|
||||
className="border px-4 py-2 text-[12px]"
|
||||
style={{ borderColor: FARG.lightSteel, background: FARG.white, color: FARG.graphite }}
|
||||
>
|
||||
{SPRAKEN.map((s) => (
|
||||
<option key={s.kod} value={s.kod}>
|
||||
{s.egetNamn}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-2 font-mono text-[11px]">
|
||||
<Link to="/alva/utgavor" style={{ color: FARG.steel }}>
|
||||
{PLATTFORMSVERSION}
|
||||
</Link>
|
||||
<span>{t("webb.hero.position")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
// inte är fackgranskad på rumänska kan planera för det. Samma chef som
|
||||
// får veta det av en tekniker som missförstått en säkerhetsinstruktion
|
||||
// kan inte.
|
||||
//
|
||||
// Sidans egen text står på besökarens språk (webb.sprak.-nycklarna).
|
||||
// Förhandsvisningen har en EGEN väljare: den visar hur grindens hinder
|
||||
// ser ut på ett språk man granskar, inte det språk man läser sidan på.
|
||||
|
||||
import { useState } from "react";
|
||||
import { SPRAK, metodikvarning, oversattare, tackning } from "../../../../services/gemensam/sprak/index.mjs";
|
||||
import { Block, Etikett, FARG, Rubrik, Tabell } from "@/alva/komponenter";
|
||||
import { useWebbSprak } from "@/alva/webbsprak";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
type Sprakpost = { kod: string; namn: string; egetNamn: string; granskat: boolean };
|
||||
@@ -54,8 +59,14 @@ function Sektion({ etikett, rubrik, children }: { etikett: string; rubrik: strin
|
||||
}
|
||||
|
||||
export default function Sprak() {
|
||||
const [valt, setValt] = useState("de");
|
||||
const t = oversattare(valt) as (nyckel: string) => string;
|
||||
const sprak = useWebbSprak();
|
||||
const t = oversattare(sprak) as (nyckel: string, variabler?: Record<string, string>) => string;
|
||||
|
||||
// Förhandsvisningens språk är inte sidans. Den som läser på engelska
|
||||
// ska kunna granska den polska översättningen — utgångsvalet är därför
|
||||
// besökarens eget språk när det inte är källspråket, annars tyska.
|
||||
const [valt, setValt] = useState(sprak !== "en" ? sprak : "de");
|
||||
const prov = oversattare(valt) as (nyckel: string) => string;
|
||||
const post = SPRAKEN.find((s) => s.kod === valt)!;
|
||||
const varning = metodikvarning(valt) as string | null;
|
||||
|
||||
@@ -63,56 +74,49 @@ export default function Sprak() {
|
||||
<Ram>
|
||||
<section className="border-b" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
|
||||
<div className="mx-auto max-w-[1040px] px-6 py-16">
|
||||
<Etikett>Localization</Etikett>
|
||||
<Etikett>{t("webb.sprak.etikett")}</Etikett>
|
||||
<div className="mt-2">
|
||||
<Rubrik niva={1}>Languages</Rubrik>
|
||||
<Rubrik niva={1}>{t("webb.sprak.rubrik")}</Rubrik>
|
||||
</div>
|
||||
<p className="mt-6 max-w-[680px] text-[16px] leading-[26px]" style={{ color: FARG.graphite }}>
|
||||
English is the default and the source language. Nine translations follow. What the platform does
|
||||
not do is claim that a translated interface means a translated method.
|
||||
{t("webb.sprak.ingress")}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---- Den avgränsning som styr allt annat ---- */}
|
||||
<Sektion etikett="Principle" rubrik="Two kinds of text">
|
||||
<Sektion etikett={t("webb.sprak.princip.etikett")} rubrik={t("webb.sprak.princip.rubrik")}>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<Block rubrik="Interface text" beteckning="Falls back silently">
|
||||
<Block rubrik={t("webb.sprak.granssnitt.rubrik")} beteckning={t("webb.sprak.granssnitt.beteckning")}>
|
||||
<p className="text-[14px] leading-[22px]" style={{ color: FARG.graphite }}>
|
||||
Labels, buttons, statuses. Finite, rarely changed. An English string reaching a German user is
|
||||
an irritation, not a hazard — so a missing translation falls back to English without comment.
|
||||
{t("webb.sprak.granssnitt.text")}
|
||||
</p>
|
||||
</Block>
|
||||
<Block rubrik="Procedure text" beteckning="Never falls back silently">
|
||||
<Block rubrik={t("webb.sprak.metodik.rubrik")} beteckning={t("webb.sprak.metodik.beteckning")}>
|
||||
<p className="text-[14px] leading-[22px]" style={{ color: FARG.graphite }}>
|
||||
Instructions for work on a vehicle. Here an unreviewed translation is worse than a foreign
|
||||
language one — because English <em>looks</em> foreign, while a bad translation looks like an
|
||||
instruction. It is shown in English and marked, with the language named.
|
||||
{t("webb.sprak.metodik.text")}
|
||||
</p>
|
||||
</Block>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 max-w-[760px] border-l-2 pl-6 text-[15px] leading-[24px]"
|
||||
style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
|
||||
The method itself is never translated. Phase names and status words are ALVA’s structure and
|
||||
read identically in every country, so an auditor can read a Romanian and a German case record
|
||||
without knowing which language the workshop works in.
|
||||
{t("webb.sprak.invariant")}
|
||||
</p>
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Redovisningen ---- */}
|
||||
<Sektion etikett="Coverage" rubrik="What is translated, and what is reviewed">
|
||||
<Sektion etikett={t("webb.sprak.tackning.etikett")} rubrik={t("webb.sprak.tackning.rubrik")}>
|
||||
<Tabell
|
||||
kolumner={["Language", "Interface", "Method reviewed by a specialist"]}
|
||||
kolumner={[t("webb.sprak.kolumn.sprak"), t("webb.sprak.kolumn.granssnitt"), t("webb.sprak.kolumn.granskat")]}
|
||||
rader={SPRAKEN.map((s) => [
|
||||
`${s.namn} — ${s.egetNamn}`,
|
||||
`${Math.round((tackning(s.kod) as number) * 100)} %`,
|
||||
s.granskat ? "Yes" : "No — procedure text shown in English",
|
||||
s.granskat ? t("webb.sprak.granskat.ja") : t("webb.sprak.granskat.nej"),
|
||||
])}
|
||||
/>
|
||||
<p className="mt-6 max-w-[680px] text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
Interface coverage is measured, not estimated: a test fails the build if any key is missing from
|
||||
any language. Review status is a statement about people, not about files, and is set by hand.
|
||||
{t("webb.sprak.matt")}
|
||||
</p>
|
||||
{/* Att engelska är källspråket betyder inte att den engelska
|
||||
metodiktexten är fackgranskad. Den är översatt från svenska,
|
||||
@@ -120,22 +124,19 @@ export default function Sprak() {
|
||||
varnar för på de andra nio språken. */}
|
||||
<p className="mt-4 max-w-[680px] border-l-2 pl-6 text-[13px] leading-[20px]"
|
||||
style={{ borderColor: FARG.varning, color: FARG.graphite }}>
|
||||
The English procedure text is a translation of the Swedish source and has not yet been read by a
|
||||
specialist working in the trade. It is held to the same standard as the other nine languages, and
|
||||
the same statement is made about it here rather than quietly excepted because it is the source.
|
||||
{t("webb.sprak.kallsprak")}
|
||||
</p>
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Beviset ---- */}
|
||||
<Sektion etikett="Verification" rubrik="The strings that stop a case">
|
||||
<Sektion etikett={t("webb.sprak.bevis.etikett")} rubrik={t("webb.sprak.bevis.rubrik")}>
|
||||
<p className="mb-8 max-w-[680px] text-[15px] leading-[24px]" style={{ color: FARG.steel }}>
|
||||
These are the sentences that refuse to let a technician close a case. A refusal nobody understands
|
||||
is a refusal with no way through — so they are the right strings to judge a translation by.
|
||||
{t("webb.sprak.bevis.ingress")}
|
||||
</p>
|
||||
|
||||
{/* Rutnät, inte flexradbrytning. Tio språk bryts av flex till nio
|
||||
plus ett ensamt, och den ensamma knappen läser sig som en
|
||||
eftertanke i stället för som ett språk bland tio. Fem kolumner
|
||||
eftertanke i stället för som ett språk bland tio. Fem kolonner
|
||||
och två gör tio jämnt i båda riktningarna. */}
|
||||
<div className="mb-8 grid grid-cols-2 gap-2 sm:grid-cols-5">
|
||||
{SPRAKEN.map((s) => (
|
||||
@@ -162,8 +163,8 @@ export default function Sprak() {
|
||||
<div className="font-mono text-[11px]" style={{ color: FARG.steel }}>
|
||||
{nyckel}
|
||||
</div>
|
||||
<div className="mt-2 text-[15px] leading-[24px]" style={{ color: FARG.graphite }}>
|
||||
{t(nyckel)}
|
||||
<div className="mt-2 text-[15px] leading-[24px]" lang={valt} style={{ color: FARG.graphite }}>
|
||||
{prov(nyckel)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -172,54 +173,48 @@ export default function Sprak() {
|
||||
{/* Varningen står under provet, inte över det. Den som just läst
|
||||
fem meningar på sitt eget språk ska mötas av vad de INTE
|
||||
garanterar — inte varnas i förväg och sedan lugnas av att
|
||||
texten ser bra ut. */}
|
||||
texten ser bra ut. Varningen själv står på PROVSPRÅKET,
|
||||
precis som i drift: den riktar sig till den som skulle
|
||||
arbeta på det språket. */}
|
||||
{varning && (
|
||||
<div className="mt-8 border-l-2 p-6" style={{ borderColor: FARG.varning, background: FARG.white }}>
|
||||
<Etikett>Not reviewed — {post.egetNamn}</Etikett>
|
||||
<p className="mt-2 max-w-[760px] text-[14px] leading-[22px]" style={{ color: FARG.graphite }}>
|
||||
<Etikett>{t("webb.sprak.ej_granskad")} — {post.egetNamn}</Etikett>
|
||||
<p className="mt-2 max-w-[760px] text-[14px] leading-[22px]" lang={valt} style={{ color: FARG.graphite }}>
|
||||
{varning}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!varning && (
|
||||
<div className="mt-8 border-l-2 p-6" style={{ borderColor: FARG.blue, background: FARG.white }}>
|
||||
<Etikett>Reviewed — {post.egetNamn}</Etikett>
|
||||
<Etikett>{t("webb.sprak.granskad")} — {post.egetNamn}</Etikett>
|
||||
<p className="mt-2 max-w-[760px] text-[14px] leading-[22px]" style={{ color: FARG.graphite }}>
|
||||
Procedure text in {post.egetNamn} has been read by a specialist working in the trade. Steps and
|
||||
checks are shown in {post.egetNamn} throughout.
|
||||
{t("webb.sprak.granskad.text", { sprak: post.egetNamn })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Hur språket väljs ---- */}
|
||||
<Sektion etikett="Operation" rubrik="How the language is chosen">
|
||||
<Sektion etikett={t("webb.sprak.val.etikett")} rubrik={t("webb.sprak.val.rubrik")}>
|
||||
<ol className="max-w-[680px]">
|
||||
{[
|
||||
["User preference", "What the individual has selected."],
|
||||
["Organization setting", "The workshop's documentation language."],
|
||||
["Browser language", "The first language the platform recognises."],
|
||||
["English", "The default, and the source."],
|
||||
].map(([rubrik, text], i) => (
|
||||
<li key={rubrik} className="flex gap-6 border-t py-6" style={{ borderColor: FARG.lightSteel }}>
|
||||
{[1, 2, 3, 4].map((steg, i) => (
|
||||
<li key={steg} className="flex gap-6 border-t py-6" style={{ borderColor: FARG.lightSteel }}>
|
||||
<span className="font-mono text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-[15px] font-semibold" style={{ color: FARG.graphite }}>
|
||||
{rubrik}
|
||||
{t(`webb.sprak.val.s${steg}.rubrik`)}
|
||||
</div>
|
||||
<div className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
{text}
|
||||
{t(`webb.sprak.val.s${steg}.text`)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="mt-8 max-w-[680px] text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
The organization ranks above the browser deliberately. A workshop in Germany with a Polish
|
||||
technician needs one documentation language — the case record must not change language depending on
|
||||
who happened to write the line.
|
||||
{t("webb.sprak.val.notering")}
|
||||
</p>
|
||||
</Sektion>
|
||||
</Ram>
|
||||
|
||||
@@ -9,10 +9,16 @@
|
||||
// uppmaning att boka demo. En sida som försöker övertyga läser som
|
||||
// marknadsföring; en sida som redovisar läser som dokumentation. Den
|
||||
// senare är vad en teknisk chef fattar beslut utifrån.
|
||||
//
|
||||
// Texten hämtas ur språkkatalogen (webb.-nycklarna) på besökarens språk.
|
||||
// Ordmärket och utläsningen är ALVA:s struktur och står kvar på engelska
|
||||
// i varje språk — precis som fasnamnen i metodkorten nedan.
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { ALVA, FASER, PLATTFORMSVERSION } from "@/alva/system";
|
||||
import { Block, Etikett, FARG, Knapp, Rubrik, Symbol, Tabell } from "@/alva/komponenter";
|
||||
import { useWebbSprak } from "@/alva/webbsprak";
|
||||
import { oversattare } from "../../../../services/gemensam/sprak/index.mjs";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
function Sektion({ etikett, rubrik, children }: { etikett: string; rubrik?: string; children: React.ReactNode }) {
|
||||
@@ -32,6 +38,9 @@ function Sektion({ etikett, rubrik, children }: { etikett: string; rubrik?: stri
|
||||
}
|
||||
|
||||
export default function Start() {
|
||||
const sprak = useWebbSprak();
|
||||
const t = oversattare(sprak) as (nyckel: string) => string;
|
||||
|
||||
return (
|
||||
<Ram>
|
||||
{/* ---- Hero. En mening. Ingen slogan. ---- */}
|
||||
@@ -44,11 +53,11 @@ export default function Start() {
|
||||
{ALVA.utlast}
|
||||
</p>
|
||||
<p className="mt-2 text-[13px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
{ALVA.position}
|
||||
{t("webb.hero.position")}
|
||||
</p>
|
||||
|
||||
<p className="mt-10 max-w-[620px] text-[18px] leading-[28px]" style={{ color: FARG.graphite }}>
|
||||
{ALVA.definition}
|
||||
{t("webb.hero.definition")}
|
||||
</p>
|
||||
|
||||
{/* Lika breda, inte innehållsbreda. Två knappar bredvid varandra
|
||||
@@ -58,20 +67,20 @@ export default function Start() {
|
||||
fyllning mot kontur, som resten av systemet gör. */}
|
||||
<div className="mt-10 grid max-w-[420px] grid-cols-2 gap-4">
|
||||
<Link to="/alva/ansokan" className="block h-full">
|
||||
<Knapp bred>Request account</Knapp>
|
||||
<Knapp bred>{t("webb.ansok")}</Knapp>
|
||||
</Link>
|
||||
<Link to="/alva/logga-in" className="block h-full">
|
||||
<Knapp bred variant="sekundar">Login</Knapp>
|
||||
<Knapp bred variant="sekundar">{t("webb.loggain")}</Knapp>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---- Metoden ---- */}
|
||||
<Sektion etikett="Methodology" rubrik="The ALVA model">
|
||||
{/* ---- Metoden. Fasnamnen är invarianta; syfte och avgränsning
|
||||
är webbtext och följer besökarens språk. ---- */}
|
||||
<Sektion etikett={t("webb.metod.etikett")} rubrik={t("webb.metod.rubrik")}>
|
||||
<p className="mb-8 max-w-[680px] text-[15px] leading-[24px]" style={{ color: FARG.steel }}>
|
||||
ALVA is a standardized method for systematic analysis, localization, verification and action. Every
|
||||
decision is traceable, every conclusion verifiable, every action reproducible.
|
||||
{t("webb.metod.ingress")}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-px md:grid-cols-4" style={{ background: FARG.lightSteel }}>
|
||||
@@ -89,10 +98,10 @@ export default function Start() {
|
||||
{f.namn}
|
||||
</div>
|
||||
<p className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
|
||||
{f.syfte}
|
||||
{t(`webb.fas.${f.id}.syfte`)}
|
||||
</p>
|
||||
<p className="mt-4 border-t pt-4 text-[12px] leading-[18px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
|
||||
{f.avgransning}
|
||||
{t(`webb.fas.${f.id}.avgransning`)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -100,26 +109,19 @@ export default function Start() {
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Arbetsgång. Numrerad, inte illustrerad. ---- */}
|
||||
<Sektion etikett="Operation" rubrik="How it works">
|
||||
<Sektion etikett={t("webb.drift.etikett")} rubrik={t("webb.drift.rubrik")}>
|
||||
<ol className="max-w-[680px]">
|
||||
{[
|
||||
["Create organization", "Registration is submitted and reviewed."],
|
||||
["Users receive accounts", "Roles are assigned by the organization administrator."],
|
||||
["Guided diagnostic procedures", "Each case follows a defined procedure."],
|
||||
["Verification", "Root cause is confirmed before corrective action."],
|
||||
["Documentation", "The record is generated from the case log."],
|
||||
["Continuous improvement", "Verified outcomes refine subsequent procedures."],
|
||||
].map(([rubrik, text], i) => (
|
||||
<li key={rubrik} className="flex gap-6 border-t py-6" style={{ borderColor: FARG.lightSteel }}>
|
||||
{[1, 2, 3, 4, 5, 6].map((steg, i) => (
|
||||
<li key={steg} className="flex gap-6 border-t py-6" style={{ borderColor: FARG.lightSteel }}>
|
||||
<span className="font-mono text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-[15px] font-semibold" style={{ color: FARG.graphite }}>
|
||||
{rubrik}
|
||||
{t(`webb.drift.s${steg}.rubrik`)}
|
||||
</div>
|
||||
<div className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
{text}
|
||||
{t(`webb.drift.s${steg}.text`)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -128,34 +130,25 @@ export default function Start() {
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Organisationens eget lärande ---- */}
|
||||
<Sektion etikett="Capability" rubrik="Enterprise learning">
|
||||
<Sektion etikett={t("webb.larande.etikett")} rubrik={t("webb.larande.rubrik")}>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-[15px] leading-[24px]" style={{ color: FARG.graphite }}>
|
||||
The platform continuously refines diagnostic procedures using verified operational experience from
|
||||
your own organization.
|
||||
{t("webb.larande.p1")}
|
||||
</p>
|
||||
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
Every completed diagnostic contributes to subsequent guidance. Only verified outcomes are used —
|
||||
a case closed without confirmed root cause contributes nothing, by design.
|
||||
{t("webb.larande.p2")}
|
||||
</p>
|
||||
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
The knowledge base belongs to the organization. It is derived from your cases, your procedures and
|
||||
your documentation, and it is not aggregated across customers.
|
||||
{t("webb.larande.p3")}
|
||||
</p>
|
||||
</div>
|
||||
<Block rubrik="Derived from" beteckning="ALVA-SPEC-004">
|
||||
<Block rubrik={t("webb.larande.block")} beteckning="ALVA-SPEC-004">
|
||||
<ul className="text-[14px] leading-[24px]" style={{ color: FARG.graphite }}>
|
||||
{[
|
||||
"Verified root causes",
|
||||
"Confirmed corrective actions",
|
||||
"Recurring fault categories",
|
||||
"Procedure completion records",
|
||||
"Organization documentation",
|
||||
].map((rad) => (
|
||||
{[1, 2, 3, 4, 5].map((rad) => (
|
||||
<li key={rad} className="flex gap-4">
|
||||
<Symbol ikon="klar" />
|
||||
{rad}
|
||||
{t(`webb.larande.k${rad}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -171,62 +164,47 @@ export default function Start() {
|
||||
|
||||
Nu står frågorna i stället. En verkstadschef känner igen sina
|
||||
egna frågor; ingen känner igen en innehållsförteckning. */}
|
||||
<Sektion etikett="Reporting" rubrik="Quarterly improvement report">
|
||||
<Sektion etikett={t("webb.rapport.etikett")} rubrik={t("webb.rapport.rubrik")}>
|
||||
<p className="mb-8 max-w-[680px] text-[15px] leading-[24px]" style={{ color: FARG.steel }}>
|
||||
Once a quarter, the platform answers six questions about the workshop’s own work. Every
|
||||
answer is derived from the case log — nothing is estimated, and nothing is asked of anyone.
|
||||
{t("webb.rapport.ingress")}
|
||||
</p>
|
||||
<ul className="grid max-w-[760px] gap-4">
|
||||
{[
|
||||
"Which faults are we now diagnosing correctly the first time?",
|
||||
"How often does a suspected cause turn out to be the actual one?",
|
||||
"Which faults keep coming back across the fleet?",
|
||||
"Which procedure steps are being skipped, and why?",
|
||||
"What does the workshop know now that it did not know last quarter?",
|
||||
"Are cases being completed, verified and documented — or only completed?",
|
||||
].map((fraga) => (
|
||||
{[1, 2, 3, 4, 5, 6].map((fraga) => (
|
||||
<li
|
||||
key={fraga}
|
||||
className="border-l-2 pl-6 text-[15px] leading-[24px]"
|
||||
style={{ borderColor: FARG.lightSteel, color: FARG.graphite }}
|
||||
>
|
||||
{fraga}
|
||||
{t(`webb.rapport.f${fraga}`)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Pris. Ingen onlinebetalning. ---- */}
|
||||
<Sektion etikett="Commercial" rubrik="Licensing">
|
||||
<Sektion etikett={t("webb.pris.etikett")} rubrik={t("webb.pris.rubrik")}>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<Tabell
|
||||
kolumner={["Component", "Basis"]}
|
||||
kolumner={[t("webb.pris.komponent"), t("webb.pris.grund")]}
|
||||
rader={[
|
||||
["Platform license", "Per organization, annually"],
|
||||
["User licenses", "Per active user, monthly"],
|
||||
["Enterprise modules", "Optional, per module"],
|
||||
[t("webb.pris.plattform"), t("webb.pris.plattform.grund")],
|
||||
[t("webb.pris.anvandare"), t("webb.pris.anvandare.grund")],
|
||||
[t("webb.pris.moduler"), t("webb.pris.moduler.grund")],
|
||||
]}
|
||||
/>
|
||||
<p className="mt-6 text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
No online payment. No subscription sign-up. Invoicing follows review of the application.
|
||||
{t("webb.pris.betalning")}
|
||||
</p>
|
||||
</div>
|
||||
<Block rubrik="Activation sequence" beteckning="ALVA-PROC-0001">
|
||||
<Block rubrik={t("webb.pris.aktivering")} beteckning="ALVA-PROC-0001">
|
||||
<ol className="text-[14px] leading-[26px]" style={{ color: FARG.graphite }}>
|
||||
{[
|
||||
"Organization submits registration.",
|
||||
"Application is reviewed.",
|
||||
"Invoice is issued.",
|
||||
"Payment is registered.",
|
||||
"Organization is activated.",
|
||||
"Additional users are billed monthly.",
|
||||
].map((rad, i) => (
|
||||
<li key={rad} className="flex gap-4">
|
||||
{[1, 2, 3, 4, 5, 6].map((steg, i) => (
|
||||
<li key={steg} className="flex gap-4">
|
||||
<span className="font-mono text-[12px]" style={{ color: FARG.steel }}>
|
||||
{String(i + 1).padStart(2, "0")}
|
||||
</span>
|
||||
{rad}
|
||||
{t(`webb.pris.a${steg}`)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@@ -235,34 +213,24 @@ export default function Start() {
|
||||
</Sektion>
|
||||
|
||||
{/* ---- Kunskapskällor ---- */}
|
||||
<Sektion etikett="Infrastructure" rubrik="Operational Knowledge Infrastructure">
|
||||
<Sektion etikett={t("webb.kallor.etikett")} rubrik={t("webb.kallor.rubrik")}>
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div>
|
||||
<p className="text-[15px] leading-[24px]" style={{ color: FARG.graphite }}>
|
||||
ALVA uses the organization’s own authorized knowledge sources. The architecture is
|
||||
provider-agnostic: every source implements the same interface, and no provider is assumed.
|
||||
{t("webb.kallor.p1")}
|
||||
</p>
|
||||
<p className="mt-4 text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
A beta installation operates on internal documentation alone. External providers are enabled later
|
||||
through separate connectors, without change to the platform core.
|
||||
{t("webb.kallor.p2")}
|
||||
</p>
|
||||
</div>
|
||||
<Block rubrik="Source resolution order" beteckning="ALVA-SPEC-011">
|
||||
<Block rubrik={t("webb.kallor.block")} beteckning="ALVA-SPEC-011">
|
||||
<ol className="text-[14px] leading-[24px]" style={{ color: FARG.graphite }}>
|
||||
{[
|
||||
"Internal company procedures",
|
||||
"OEM documentation",
|
||||
"Technical bulletins",
|
||||
"Warranty information",
|
||||
"Service manuals",
|
||||
"Historical verified diagnostics",
|
||||
"Organization best practices",
|
||||
].map((rad, i) => (
|
||||
{[1, 2, 3, 4, 5, 6, 7].map((rad, i) => (
|
||||
<li key={rad} className="flex gap-4">
|
||||
<span className="font-mono text-[12px]" style={{ color: FARG.steel }}>
|
||||
{i + 1}
|
||||
</span>
|
||||
{rad}
|
||||
{t(`webb.kallor.k${rad}`)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@@ -277,10 +245,10 @@ export default function Start() {
|
||||
</div>
|
||||
<div className="grid max-w-[420px] grid-cols-2 gap-4">
|
||||
<Link to="/alva/ansokan" className="block h-full">
|
||||
<Knapp bred>Request account</Knapp>
|
||||
<Knapp bred>{t("webb.ansok")}</Knapp>
|
||||
</Link>
|
||||
<Link to="/alva/logga-in" className="block h-full">
|
||||
<Knapp bred variant="sekundar">Login</Knapp>
|
||||
<Knapp bred variant="sekundar">{t("webb.loggain")}</Knapp>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user