feat(workbench): app picker searches English aliases + IT/FR/ES i18n

Die Suche im AppPagePicker matcht jetzt gegen DE + EN + Fallback-Name +
id-Slug, damit englische Begriffe immer greifen — auch wenn die UI in
Deutsch läuft ("cal" → Kalender, "weather" → Wetter).

- AppPagePicker: statische Imports von apps/de.json + apps/en.json,
  neue searchHaystack-Funktion, Filter über Haystack statt nur
  displayName.
- apps/{it,fr,es}.json: +42 Einträge pro Sprache, alle 78 registrierten
  Module sind jetzt nativ übersetzt. Vorher fiel IT/FR/ES für neuere
  Module via fallbackLocale auf Deutsch zurück (z.B. "Wetter" im
  italienischen Menü).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-24 16:00:27 +02:00
parent 63f51799b0
commit ed8ec98572
4 changed files with 149 additions and 8 deletions

View file

@ -12,6 +12,11 @@
import { getAccessibleApps } from '$lib/app-registry';
import type { AppDescriptor } from '$lib/app-registry/types';
import { APP_CATEGORIES, getAppCategory, type AppCategory } from '$lib/app-registry/categories';
import appsDeRaw from '$lib/i18n/locales/apps/de.json';
import appsEnRaw from '$lib/i18n/locales/apps/en.json';
const appsDe = appsDeRaw as Record<string, string>;
const appsEn = appsEnRaw as Record<string, string>;
function appName(id: string, fallback: string): string {
const key = `apps.${id}`;
@ -19,6 +24,15 @@
return translated !== key ? translated : fallback;
}
// Match against DE + EN names, fallback, and id together so English
// aliases stay searchable while the UI is in German ("cal" → Kalender).
function searchHaystack(id: string, fallback: string, displayName: string): string {
return [displayName, fallback, appsDe[id], appsEn[id], id.replace(/-/g, ' ')]
.filter(Boolean)
.join(' ')
.toLowerCase();
}
interface Props {
onSelect: (appId: string) => void;
onClose: () => void;
@ -43,11 +57,14 @@
system: true, // System is collapsed by default — noisy and rarely toggled
});
// Tier-gate first, then drop open apps, then attach display name.
// Tier-gate first, then drop open apps, then attach display name + search haystack.
let available = $derived(
getAccessibleApps(userTier)
.filter((app) => !activeAppIds.includes(app.id))
.map((app) => ({ app, displayName: appName(app.id, app.name) }))
.map((app) => {
const displayName = appName(app.id, app.name);
return { app, displayName, haystack: searchHaystack(app.id, app.name, displayName) };
})
.sort((a, b) => a.displayName.localeCompare(b.displayName, 'de'))
);
@ -56,9 +73,7 @@
let searchResults = $derived(
searchMode
? available.filter(({ displayName }) =>
displayName.toLowerCase().includes(query.trim().toLowerCase())
)
? available.filter(({ haystack }) => haystack.includes(query.trim().toLowerCase()))
: []
);

View file

@ -34,5 +34,47 @@
"who": "Who",
"events": "Eventos",
"automations": "Automatizaciones",
"playground": "Playground"
"playground": "Playground",
"kontext": "Contexto web",
"news": "Noticias",
"news-research": "Investigación de noticias",
"articles": "Artículos",
"research-lab": "Laboratorio de investigación",
"drink": "Bebidas",
"recipes": "Recetas",
"stretch": "Estiramientos",
"mail": "Correo",
"meditate": "Meditación",
"mood": "Estado de ánimo",
"sleep": "Sueño",
"myday": "Mi día",
"activity": "Actividad",
"companion": "Compañero",
"ai-missions": "Misiones IA",
"ai-agents": "Agentes IA",
"ai-workbench": "Workbench IA",
"rituals": "Rituales",
"ai-policy": "Políticas IA",
"ai-insights": "Información IA",
"ai-health": "Salud IA",
"goals": "Metas",
"credits": "Créditos y suscripción",
"spiral": "Mana Spiral",
"settings": "Ajustes",
"themes": "Temas",
"profile": "Perfil",
"admin": "Admin",
"complexity": "Complejidad",
"api-keys": "Claves API",
"wishes": "Deseos",
"help": "Ayuda",
"wetter": "Tiempo",
"feedback": "Comentarios",
"wardrobe": "Armario",
"library": "Biblioteca",
"spaces": "Espacios",
"website": "Sitio web",
"quiz": "Quiz",
"guides": "Guías",
"comic": "Cómic"
}

View file

@ -34,5 +34,47 @@
"who": "Who",
"events": "Événements",
"automations": "Automations",
"playground": "Playground"
"playground": "Playground",
"kontext": "Contexte web",
"news": "Actualités",
"news-research": "Recherche d'actus",
"articles": "Articles",
"research-lab": "Laboratoire de recherche",
"drink": "Boissons",
"recipes": "Recettes",
"stretch": "Étirements",
"mail": "Courrier",
"meditate": "Méditation",
"mood": "Humeur",
"sleep": "Sommeil",
"myday": "Ma journée",
"activity": "Activité",
"companion": "Compagnon",
"ai-missions": "Missions IA",
"ai-agents": "Agents IA",
"ai-workbench": "Workbench IA",
"rituals": "Rituels",
"ai-policy": "Règles IA",
"ai-insights": "Analyses IA",
"ai-health": "Santé IA",
"goals": "Objectifs",
"credits": "Crédits et abonnement",
"spiral": "Mana Spirale",
"settings": "Paramètres",
"themes": "Thèmes",
"profile": "Profil",
"admin": "Admin",
"complexity": "Complexité",
"api-keys": "Clés API",
"wishes": "Souhaits",
"help": "Aide",
"wetter": "Météo",
"feedback": "Retours",
"wardrobe": "Garde-robe",
"library": "Bibliothèque",
"spaces": "Espaces",
"website": "Site web",
"quiz": "Quiz",
"guides": "Guides",
"comic": "Bande dessinée"
}

View file

@ -34,5 +34,47 @@
"who": "Who",
"events": "Eventi",
"automations": "Automazioni",
"playground": "Playground"
"playground": "Playground",
"kontext": "Contesto Web",
"news": "News",
"news-research": "Ricerca News",
"articles": "Articoli",
"research-lab": "Laboratorio di ricerca",
"drink": "Bevande",
"recipes": "Ricette",
"stretch": "Stretching",
"mail": "Posta",
"meditate": "Meditazione",
"mood": "Umore",
"sleep": "Sonno",
"myday": "La mia giornata",
"activity": "Attività",
"companion": "Compagno",
"ai-missions": "Missioni IA",
"ai-agents": "Agenti IA",
"ai-workbench": "Workbench IA",
"rituals": "Rituali",
"ai-policy": "Regole IA",
"ai-insights": "Insight IA",
"ai-health": "Salute IA",
"goals": "Obiettivi",
"credits": "Crediti & Abbonamento",
"spiral": "Mana Spiral",
"settings": "Impostazioni",
"themes": "Temi",
"profile": "Profilo",
"admin": "Admin",
"complexity": "Complessità",
"api-keys": "Chiavi API",
"wishes": "Desideri",
"help": "Aiuto",
"wetter": "Meteo",
"feedback": "Feedback",
"wardrobe": "Guardaroba",
"library": "Biblioteca",
"spaces": "Spazi",
"website": "Sito web",
"quiz": "Quiz",
"guides": "Guide",
"comic": "Fumetto"
}