feat(manacore/web): localize app names with i18n (5 languages)

Add `apps` locale namespace (de/en/fr/it/es) with localized display
names for all 29 app modules. Workbench card titles, app picker, and
overlay headers now resolve names via $_('apps.{id}') with fallback
to AppDescriptor.name.

Key renames (de): Todo→Aufgaben, Habits→Routinen, Notes→Notizen,
Finance→Finanzen, Places→Orte, Mukke→Musik, Context→Dokumente,
Times→Zeiten, Cards→Karten, Picture→Bilder, Photos→Fotos,
Storage→Ablage, Questions→Recherche, CityCorners→Stadtführer,
Calc→Rechner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 21:39:31 +02:00
parent 8218037841
commit 46dae20fa3
9 changed files with 182 additions and 4 deletions

View file

@ -5,6 +5,7 @@
-->
<script lang="ts">
import { X, CaretUp, CaretDown, ArrowLeft, SpinnerGap } from '@manacore/shared-icons';
import { _ } from 'svelte-i18n';
import { PageShell } from '$lib/components/page-carousel';
import { getApp, getAppByDragType, canDrop, executeDrop } from '$lib/app-registry';
import type { Component } from 'svelte';
@ -40,7 +41,11 @@
}: Props = $props();
let app = $derived(getApp(appId));
let appName = $derived(app?.name ?? appId);
let appName = $derived.by(() => {
const key = `apps.${appId}`;
const translated = $_(key);
return translated !== key ? translated : (app?.name ?? appId);
});
let appColor = $derived(app?.color ?? '#6B7280');
// ── Cross-module drop target ────────────────────────────
@ -152,7 +157,11 @@
params: { [targetAppDesc.paramKey!]: targetId },
component: mod.default,
overlayColor: targetAppDesc.color,
overlayTitle: targetAppDesc.name,
overlayTitle: (() => {
const k = `apps.${targetApp}`;
const t = $_(k);
return t !== k ? t : targetAppDesc.name;
})(),
},
];
});

View file

@ -6,6 +6,12 @@
import { X } from '@manacore/shared-icons';
import { getAllApps } from '$lib/app-registry';
function appName(id: string, fallback: string): string {
const key = `apps.${id}`;
const translated = $_(key);
return translated !== key ? translated : fallback;
}
interface Props {
onSelect: (appId: string) => void;
onClose: () => void;
@ -27,7 +33,7 @@
{#if i > 0}<div class="divider"></div>{/if}
<button class="app-option" onclick={() => onSelect(app.id)}>
<div class="app-dot" style="background-color: {app.color}"></div>
<span class="app-name">{app.name}</span>
<span class="app-name">{appName(app.id, app.name)}</span>
</button>
{/each}

View file

@ -16,6 +16,7 @@ const defaultLocale = 'de';
function registerLocale(lang: SupportedLocale) {
register(lang, async () => {
const [
apps,
common,
nav,
dashboard,
@ -49,6 +50,7 @@ function registerLocale(lang: SupportedLocale) {
guides,
help,
] = await Promise.all([
import(`./locales/apps/${lang}.json`),
import(`./locales/common/${lang}.json`),
import(`./locales/nav/${lang}.json`),
import(`./locales/dashboard/${lang}.json`),
@ -84,6 +86,7 @@ function registerLocale(lang: SupportedLocale) {
]);
return {
apps: apps.default,
common: common.default,
nav: nav.default,
dashboard: dashboard.default,

View file

@ -0,0 +1,31 @@
{
"todo": "Aufgaben",
"calendar": "Kalender",
"contacts": "Kontakte",
"habits": "Routinen",
"notes": "Notizen",
"finance": "Finanzen",
"places": "Orte",
"chat": "Chat",
"context": "Dokumente",
"times": "Zeiten",
"zitare": "Zitare",
"cards": "Karten",
"picture": "Bilder",
"mukke": "Musik",
"photos": "Fotos",
"storage": "Ablage",
"nutriphi": "NutriPhi",
"planta": "Planta",
"presi": "Presi",
"inventar": "Inventar",
"memoro": "Memoro",
"questions": "Recherche",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Stadtführer",
"uload": "uLoad",
"calc": "Rechner",
"automations": "Automationen",
"playground": "Playground"
}

View file

@ -0,0 +1,31 @@
{
"todo": "Tasks",
"calendar": "Calendar",
"contacts": "Contacts",
"habits": "Routines",
"notes": "Notes",
"finance": "Finance",
"places": "Places",
"chat": "Chat",
"context": "Documents",
"times": "Times",
"zitare": "Zitare",
"cards": "Cards",
"picture": "Pictures",
"mukke": "Music",
"photos": "Photos",
"storage": "Storage",
"nutriphi": "NutriPhi",
"planta": "Planta",
"presi": "Presi",
"inventar": "Inventory",
"memoro": "Memoro",
"questions": "Research",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "City Guide",
"uload": "uLoad",
"calc": "Calculator",
"automations": "Automations",
"playground": "Playground"
}

View file

@ -0,0 +1,31 @@
{
"todo": "Tareas",
"calendar": "Calendario",
"contacts": "Contactos",
"habits": "Rutinas",
"notes": "Notas",
"finance": "Finanzas",
"places": "Lugares",
"chat": "Chat",
"context": "Documentos",
"times": "Tiempos",
"zitare": "Zitare",
"cards": "Tarjetas",
"picture": "Imágenes",
"mukke": "Música",
"photos": "Fotos",
"storage": "Almacén",
"nutriphi": "NutriPhi",
"planta": "Planta",
"presi": "Presi",
"inventar": "Inventario",
"memoro": "Memoro",
"questions": "Investigación",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guía urbana",
"uload": "uLoad",
"calc": "Calculadora",
"automations": "Automatizaciones",
"playground": "Playground"
}

View file

@ -0,0 +1,31 @@
{
"todo": "Tâches",
"calendar": "Calendrier",
"contacts": "Contacts",
"habits": "Routines",
"notes": "Notes",
"finance": "Finances",
"places": "Lieux",
"chat": "Chat",
"context": "Documents",
"times": "Temps",
"zitare": "Zitare",
"cards": "Cartes",
"picture": "Images",
"mukke": "Musique",
"photos": "Photos",
"storage": "Stockage",
"nutriphi": "NutriPhi",
"planta": "Planta",
"presi": "Presi",
"inventar": "Inventaire",
"memoro": "Memoro",
"questions": "Recherche",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guide urbain",
"uload": "uLoad",
"calc": "Calculatrice",
"automations": "Automations",
"playground": "Playground"
}

View file

@ -0,0 +1,31 @@
{
"todo": "Attività",
"calendar": "Calendario",
"contacts": "Contatti",
"habits": "Routine",
"notes": "Note",
"finance": "Finanze",
"places": "Luoghi",
"chat": "Chat",
"context": "Documenti",
"times": "Tempi",
"zitare": "Zitare",
"cards": "Schede",
"picture": "Immagini",
"mukke": "Musica",
"photos": "Foto",
"storage": "Archivio",
"nutriphi": "NutriPhi",
"planta": "Planta",
"presi": "Presi",
"inventar": "Inventario",
"memoro": "Memoro",
"questions": "Ricerca",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guida città",
"uload": "uLoad",
"calc": "Calcolatrice",
"automations": "Automazioni",
"playground": "Playground"
}

View file

@ -8,6 +8,7 @@
import { DragPreview } from '@manacore/shared-ui/dnd';
import type { DragType } from '@manacore/shared-ui/dnd';
import { ContextMenu } from '@manacore/shared-ui';
import { _ } from 'svelte-i18n';
import { buildContextMenuItems, createWorkbenchContextMenu } from '$lib/context-menu';
function resolveEntity(type: string, data: Record<string, unknown>) {
@ -101,7 +102,11 @@
maximized: a.maximized,
widthPx: a.widthPx ?? DEFAULT_WIDTH,
heightPx: a.heightPx,
title: entry?.name ?? a.appId,
title: (() => {
const k = `apps.${a.appId}`;
const t = $_(k);
return t !== k ? t : (entry?.name ?? a.appId);
})(),
color: entry?.color ?? '#6B7280',
};
})