Task 10: alle sichtbaren Texte von account/, editor/, card/ und der Seite „Passwort neu setzen“ (Texte, aria-label, title, Meldungen,
Rückfragen) gehen durch t()/msg(); en.ts bekommt 87 Einträge.
- account/: Konto-Menü, Login-Dialog (Titel, Reiter, Felder, Meldungen) und errors.ts (errorMessage übersetzt beim Zurückgeben).
validatePassword nutzt {min}/{max}. Der Login-Dialog wird nur einmal gebaut: die Beschriftung des Schließen-Knopfs wird bei jedem
Öffnen in der aktuellen Sprache gesetzt. LOGIN_NEEDED und DEFAULT_CARD_NAME bleiben deutsche Konstanten (msg), angezeigt werden sie
mit t(); der Standardname einer neuen Karte folgt damit der Sprache („My card“).
- editor/: Vorlagen-, Options- und Vorschauspalte, Bild-Upload, Speichern, Download, Rückfragen. Konstanten (upload.ts, download.ts,
editor-page.ts) bleiben deutsche Strings per msg(); validateImageFile, pngSizeWarning und das Speichern („Speichern fehlgeschlagen –
{reason}“) liefern den Text schon übersetzt, Fehler tragen weiter die deutsche Konstante (Vergleiche mit READ_MESSAGE/PIXELS_MESSAGE
gehen weiter) und werden beim Anzeigen mit t(error.message) übersetzt. Zoom „100 %“ / „100%“ und „Bearbeitet: {name}“ mit Platzhaltern.
- card/: Bezeichnungen der Marken (msg) und die Meldungen des Renderers (msg, Anzeige im Editor über t()).
- pages/reset-page.ts, account/login-dialog.ts: Die Meldung „Die Passwörter stimmen nicht überein.“ ist nicht mehr der Schlüssel zum
Fokus: es zählt die Bedingung selbst, nicht der Vergleich mit dem (nun übersetzten) Text.
- i18n/coverage.test.ts: Heuristik für deutsche Text-Literale außerhalb von t()/msg() (Umlaut/ß, mindestens zwei Wörter, einzelnes
großgeschriebenes Wort; gelesen wird der Syntaxbaum) für account/, editor/, card/ und pages/reset-page.ts; Ausnahmen mit Grund, jede
muss noch zutreffen; Scanner mit eigenen Tests. Die Liste der Bereiche ist eine Konstante für Task 11.
- i18n/english-ui.test.ts (neu): Stichproben auf Englisch (Konto-Menü, Login-Dialog, Passwort-Seite, Editor inkl. Fehlerwege) und
„kein deutscher Wörterbuchtext auf der englischen Seite“.
- test-setup.ts: Wer Module mit vi.resetModules() frisch lädt, bekam eine neue Kopie von i18n.ts mit der Browsersprache (happy-dom: en-US);
der Browser meldet im Test jetzt Deutsch, damit auch diese Tests deutsch beginnen. main.test.ts: nach dem Wechsel auf Englisch heißt
der Knopf des Konto-Menüs „Sign in“.
- Tests: Vitest 53 Dateien, 1779 Tests grün, tsc und Build sauber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
// Logo-Register: SVG-Dateien der AirCard-IOS-Marken (Logo oben links), Einfärben und Laden als Bild für den Canvas.
|
||
// Reine Daten plus ein kleiner Browser-Lader; die Größe der Logos bestimmt allein die viewBox.
|
||
// Die Logos unten rechts kommen von der Community (logos/logo-asset.ts), hier steht keine fremde Marke.
|
||
|
||
import { msg } from '../i18n/i18n';
|
||
import { TONE_COLOR, type BrandId, type DetailsTone } from './config';
|
||
|
||
import aircardsSymbol from '../assets/marks/aircards-symbol.svg?raw';
|
||
import aircardsText from '../assets/marks/aircards-text.svg?raw';
|
||
|
||
export type MarkGroup = 'Logo';
|
||
|
||
export interface MarkDef {
|
||
id: Exclude<BrandId, 'none'>;
|
||
/**
|
||
* Gruppenname; zusammen mit dem Label der Text für `aria-label`/`title` („Logo – AirCard-IOS · Symbol“).
|
||
* Beide sind deutsch markiert (`msg`) und werden erst beim Anzeigen mit `t()` übersetzt.
|
||
*/
|
||
group: MarkGroup;
|
||
label: string;
|
||
svg: string;
|
||
/** true: enthält `currentColor` und folgt der Details-Farbe. */
|
||
tint: boolean;
|
||
/** Höchsthöhe auf der Karte in px (Breite folgt aus dem Seitenverhältnis der viewBox). */
|
||
maxH: number;
|
||
}
|
||
|
||
type MarkEntry = Omit<MarkDef, 'id'>;
|
||
|
||
const BRAND_TABLE: Record<Exclude<BrandId, 'none'>, MarkEntry> = {
|
||
'aircards-symbol': { group: msg('Logo'), label: msg('AirCard-IOS · Symbol'), svg: aircardsSymbol, tint: true, maxH: 100 },
|
||
'aircards-text': { group: msg('Logo'), label: msg('AirCard-IOS · Text'), svg: aircardsText, tint: true, maxH: 100 },
|
||
};
|
||
|
||
export const BRAND_MARKS: readonly MarkDef[] = (
|
||
Object.keys(BRAND_TABLE) as Exclude<BrandId, 'none'>[]
|
||
).map((id) => ({ id, ...BRAND_TABLE[id] }));
|
||
|
||
export function getBrandMark(id: Exclude<BrandId, 'none'>): MarkDef {
|
||
return BRAND_MARKS.find((m) => m.id === id) ?? BRAND_MARKS[0];
|
||
}
|
||
|
||
/** Breite durch Höhe aus dem Wert einer viewBox („x y Breite Höhe“, mit Leerraum oder Kommas); null, wenn er nicht brauchbar ist. */
|
||
export function viewBoxAspect(value: string): number | null {
|
||
const parts = value.trim().split(/[\s,]+/).map(Number);
|
||
const [w, h] = [parts[2], parts[3]];
|
||
return parts.length === 4 && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 ? w / h : null;
|
||
}
|
||
|
||
/** Breite durch Höhe aus der viewBox; 1, wenn keine brauchbare viewBox da ist. */
|
||
export function svgAspect(svg: string): number {
|
||
const hit = /viewBox\s*=\s*"([^"]+)"/.exec(svg);
|
||
return (hit && viewBoxAspect(hit[1])) ?? 1;
|
||
}
|
||
|
||
/** Ersetzt jedes `currentColor` durch die Details-Farbe. */
|
||
export function tintSvg(svg: string, tone: DetailsTone): string {
|
||
return svg.replaceAll('currentColor', TONE_COLOR[tone]);
|
||
}
|
||
|
||
/** Gibt dem Wurzelelement eine eigene Größe (viermal maxH hoch), damit alle Browser scharf auf den Canvas zeichnen. */
|
||
function withIntrinsicSize(svg: string, maxH: number): string {
|
||
const h = maxH * 4;
|
||
const w = Math.round(h * svgAspect(svg) * 100) / 100;
|
||
return svg.replace('<svg', `<svg width="${w}" height="${h}"`);
|
||
}
|
||
|
||
const imageCache = new Map<string, Promise<HTMLImageElement>>();
|
||
|
||
/** Lädt ein Logo (eingefärbt) als Bild für `drawImage`. Je (id, tone) wird nur einmal geladen. */
|
||
export function loadMarkImage(def: MarkDef, tone: DetailsTone): Promise<HTMLImageElement> {
|
||
const key = `${def.id}:${tone}`;
|
||
let pending = imageCache.get(key);
|
||
if (!pending) {
|
||
const source = withIntrinsicSize(tintSvg(def.svg, tone), def.maxH);
|
||
pending = new Promise<HTMLImageElement>((resolve, reject) => {
|
||
const url = URL.createObjectURL(new Blob([source], { type: 'image/svg+xml' }));
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
URL.revokeObjectURL(url);
|
||
resolve(img);
|
||
};
|
||
img.onerror = () => {
|
||
URL.revokeObjectURL(url);
|
||
reject(new Error(`Logo konnte nicht geladen werden: ${def.id}`));
|
||
};
|
||
img.src = url;
|
||
});
|
||
pending.catch(() => imageCache.delete(key));
|
||
imageCache.set(key, pending);
|
||
}
|
||
return pending;
|
||
}
|