chore(mana): citycorners + food + wardrobe aus unified-App entfernen

Citycorners-Reste vom vorherigen Sprint mit committet. food → Nutriphi,
wardrobe → Werdrobe sind als Standalone-Apps live; die mana.how-unified-
App trägt die Modul-Surfaces nicht mehr.

Gelöscht / abgebaut:
- Module: apps/mana/.../modules/{food,wardrobe} + Routen + Locales
- Landing-Apps: apps/{food,citycorners}/ Top-Level
- Backend: apps/api/src/modules/{food,wardrobe} + MCP-Tools log_meal /
  nutrition_summary, picture-routes verifyMediaOwnership-Allowlist
- shared-branding: APP_BRANDING, APP_ICONS, MANA_APPS, Logos, Onboarding
- shared-ai, mana-tool-registry, credits, shared-types/spaces,
  shared-utils/analytics, spiral-db/MANA_APP_INDEX, website-blocks
- Cross-Module: Body-CalorieWeightChart, Comic-CharacterPicker-Wardrobe,
  website-Embed wardrobe.outfits, DaySnapshot.nutrition, FoodEventType,
  MealLogged/Meal*-Streaks/Goals/Companion/Trigger, AI-Agent-Policy,
  GoalEditor MealLogged, MyDay/RitualRunner/Rules nutrition-Refs,
  Crypto-Registry meals/wardrobeGarments/wardrobeOutfits
- Generic: PlaceCategory 'food' (places + geocoding + Locales),
  spaces.ts 'food'/'wardrobe' Modul-IDs
- Infrastruktur: cloudflared, docker-compose CORS, nginx-Landing,
  prometheus-Probe, load-tests, package.json dev-Scripts,
  generate-env, mac-mini/build-landings, dependabot

Dexie v62 Migration:
- droppt meals, goals, foodFavorites, mealTags, wardrobeGarments,
  wardrobeOutfits Tabellen
- entfernt wardrobeOutfitId / wardrobeGarmentId aus images-Index
- Upgrade-Callback strippt die toten FK-Properties aus alten image-Rows

Test/Doku:
- module-registry.test.ts: Snapshot refresht auf aktuellen Stand mit
  56 Modulen (vorher 32, statisch eingefroren pre-refactor). Plus
  LEGACY_TABLES-Exclusion für nicht-mehr-registrierte Tabellen aus
  cards/citycorners/moodlit/rituals/wishes/who.
- streaks.test.ts: MealLogged-Test in TaskCompleted-Test umgebaut
- apps/mana/CLAUDE.md: food-Refs in AI-Tool-Tabelle und
  AiProposalInbox-Liste entfernt
- validate-i18n-keys.mjs + validate-no-recursive-turbo.mjs:
  existsSync-Guard, damit die Skripte mit gestaged-aber-rm'ten Dateien
  klarkommen

mana-web svelte-check 0 errors / 7436 files, betroffene Tests grün
(streaks, dashboard, module-registry), validate:pg-schema,
validate:turbo, validate:i18n-parity grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-18 12:47:33 +02:00
parent 932bd98d84
commit ae04c9e194
260 changed files with 234 additions and 21506 deletions

View file

@ -97,15 +97,6 @@ updates:
- "docker"
- "automated"
- package-ecosystem: "docker"
directory: "/apps/food/apps/backend"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "docker"
- "automated"
- package-ecosystem: "docker"
directory: "/apps/news/apps/api"
schedule:

View file

@ -31,13 +31,10 @@ import { chatRoutes } from './modules/chat/routes';
import { notesRoutes } from './modules/notes/routes';
import { pictureRoutes } from './modules/picture/routes';
import { profileRoutes } from './modules/profile/routes';
import { wardrobeRoutes } from './modules/wardrobe/routes';
import { storageRoutes } from './modules/storage/routes';
import { todoRoutes } from './modules/todo/routes';
import { plantsRoutes } from './modules/plants/routes';
import { foodRoutes } from './modules/food/routes';
import { guidesRoutes } from './modules/guides/routes';
import { moodlitRoutes } from './modules/moodlit/routes';
import { newsRoutes } from './modules/news/routes';
import { newsResearchRoutes } from './modules/news-research/routes';
import { articlesRoutes } from './modules/articles/routes';
@ -100,11 +97,10 @@ app.use('/api/*', authMiddleware());
// to `beta`+ so that unauthenticated guest fallbacks (tier='public'
// from a missing claim) can't hit paid infrastructure.
// Pure CRUD modules (calendar, contacts, music, storage, todo, news,
// presi, moodlit) rely on authMiddleware alone — users access only
// presi) rely on authMiddleware alone — users access only
// their own records.
const RESOURCE_MODULES = [
'chat',
'food',
'guides',
'kontext',
'news-research',
@ -135,13 +131,10 @@ app.route('/api/v1/chat', chatRoutes);
app.route('/api/v1/notes', notesRoutes);
app.route('/api/v1/picture', pictureRoutes);
app.route('/api/v1/profile', profileRoutes);
app.route('/api/v1/wardrobe', wardrobeRoutes);
app.route('/api/v1/storage', storageRoutes);
app.route('/api/v1/todo', todoRoutes);
app.route('/api/v1/plants', plantsRoutes);
app.route('/api/v1/food', foodRoutes);
app.route('/api/v1/guides', guidesRoutes);
app.route('/api/v1/moodlit', moodlitRoutes);
app.route('/api/v1/news', newsRoutes);
app.route('/api/v1/news-research', newsResearchRoutes);
app.route('/api/v1/articles', articlesRoutes);

View file

@ -91,11 +91,10 @@ export async function getMediaBufferAsPng(
* doesn't land in the owned set the caller turns that into an HTTP
* response.
*
* Accepts a single app string or an array. The Wardrobe try-on flow
* (plan docs/plans/wardrobe-module.md M4) passes `['me', 'wardrobe']`
* in one call face-ref and body-ref live under `me`, garments live
* under `wardrobe`, both legitimate inputs for the same `/v1/images/
* edits` POST.
* Accepts a single app string or an array. Comic character-ref flows
* pass `['me', 'comic']` in one call when both face/body portraits and
* comic-specific anchors are legitimate inputs for the same
* `/v1/images/edits` POST.
*
* One `list()` round-trip per app. For N apps this is N calls, each
* capped at 500 rows far beyond the product's intended per-app shape

View file

@ -529,44 +529,6 @@ register('undo_drink', async (_args, userId) => {
return ok(`Letzter Drink-Eintrag (${last.name}) rückgängig gemacht.`);
});
// ── Food tools ────────────────────────────────────────────────
register('log_meal', async (args, userId) => {
const mealId = crypto.randomUUID();
const now = nowIso();
const today = now.split('T')[0];
const data = {
id: mealId,
userId,
mealType: args.mealType as string,
description: args.description as string,
calories: (args.calories as number) ?? null,
protein: (args.protein as number) ?? null,
date: today,
createdAt: now,
updatedAt: now,
};
await writeRecord(userId, 'food', 'meals', mealId, 'insert', data, fieldTs(Object.keys(data)));
return ok(`${args.mealType}: "${args.description}" geloggt.`, { id: mealId });
});
register('nutrition_summary', async (_args, userId) => {
const records = await readLatestRecords(userId, 'food', 'meals');
const today = new Date().toISOString().split('T')[0];
const todayMeals = records.filter((r) => r.date === today);
let totalCal = 0,
totalProtein = 0;
for (const m of todayMeals) {
totalCal += (m.calories as number) ?? 0;
totalProtein += (m.protein as number) ?? 0;
}
return ok(`Heute: ${todayMeals.length} Mahlzeiten, ${totalCal} kcal, ${totalProtein}g Protein`, {
meals: todayMeals.length,
calories: totalCal,
protein: totalProtein,
});
});
// ── Journal tools ─────────────────────────────────────────────
register('create_journal_entry', async (args, userId) => {

View file

@ -1,220 +0,0 @@
/**
* Food module Meal analysis (Gemini Vision via mana-llm) + recommendations.
*
* CRUD for meals, goals, favorites is handled by mana-sync. This module
* owns the server-only operations: photo upload to mana-media, structured
* AI analysis using the Vercel AI SDK (`generateObject`) against the
* shared Zod schema in @mana/shared-types, and a small rule-based
* recommendation engine.
*
* Why generateObject + Zod instead of raw fetch?
* - Runtime validation of the AI response if Gemini drifts on a
* field, we throw at the boundary instead of corrupting downstream
* state. The frontend never sees malformed data.
* - Provider-portable structured outputs: the AI SDK translates one
* Zod schema into OpenAI strict json_schema / Anthropic tool-use /
* Gemini response_schema depending on which backend mana-llm routes
* to. We don't have to know which.
* - Single source of truth: the same MealAnalysisSchema is consumed
* by the unified web app via `z.infer<typeof MealAnalysisSchema>`,
* so changes here propagate end-to-end without manual sync.
*/
import { Hono } from 'hono';
import { generateObject } from 'ai';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import {
AI_SCHEMA_VERSION,
MealAnalysisSchema,
type AiResponseEnvelope,
type MealAnalysis,
} from '@mana/shared-types';
import { logger, type AuthVariables } from '@mana/shared-hono';
import { MANA_LLM } from '@mana/shared-ai';
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
// mana-llm resolves this alias to a healthy vision model (chain in
// services/mana-llm/aliases.yaml). To swap the chain, edit the YAML
// and SIGHUP — no service redeploy here.
const VISION_MODEL = MANA_LLM.VISION;
const llm = createOpenAICompatible({
name: 'mana-llm',
// mana-llm exposes /v1/chat/completions (see services/mana-llm/CLAUDE.md +
// src/main.py:125). The AI SDK's openai-compatible adapter appends
// /chat/completions to baseURL, so baseURL ends in /v1.
baseURL: `${LLM_URL}/v1`,
// Tell the AI SDK that mana-llm honours OpenAI-style strict
// json_schema response_format. Without this, generateObject() falls
// back to a tool-call mode that Ollama-backed models don't support
// reliably and the response fails to validate against the Zod schema.
// mana-llm's Ollama provider translates response_format → Ollama's
// native `format` field (services/mana-llm/src/providers/ollama.py)
// so this is honoured end-to-end.
supportsStructuredOutputs: true,
});
const ANALYSIS_PROMPT = `Du bist ein Ernährungsexperte. Analysiere die Mahlzeit und gib strukturierte Nährwertdaten zurück. Schätze realistische Portionsgrößen und Kalorien. Antworte auf Deutsch.`;
/**
* Provider hints attached to the system message. Forward-compat:
*
* - anthropic.cacheControl: ephemeral system-prompt caching. NO-OP today
* because (a) we route to Gemini via mana-llm and (b) the prompt is
* ~50 tokens well under Anthropic's 1024-token cache minimum. Becomes
* active automatically when mana-llm routes to Claude AND the prompt
* grows (e.g. once we attach per-user dietary preferences as system
* context, which would push us past the threshold).
*
* Kept here so the day we flip the backend, we don't have to revisit
* every route to enable caching it just starts working.
*/
const SYSTEM_CACHE_HINT = {
anthropic: { cacheControl: { type: 'ephemeral' as const } },
};
/** Wrap a validated AI object in the standard wire-format envelope. */
function envelope(data: MealAnalysis): AiResponseEnvelope<MealAnalysis> {
return { schemaVersion: AI_SCHEMA_VERSION, data };
}
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Photo Upload (server-only: S3 storage via mana-media) ───
routes.post('/photos/upload', async (c) => {
const userId = c.get('userId');
const formData = await c.req.formData();
const file = formData.get('file') as File | null;
if (!file) return c.json({ error: 'No file provided' }, 400);
if (file.size > 10 * 1024 * 1024) return c.json({ error: 'File too large (max 10MB)' }, 400);
try {
const { uploadImageToMedia } = await import('../../lib/media');
const buffer = await file.arrayBuffer();
const result = await uploadImageToMedia(buffer, file.name, { app: 'food', userId });
return c.json(
{
mediaId: result.id,
publicUrl: result.urls.original,
thumbnailUrl: result.urls.thumbnail || result.urls.original,
storagePath: result.id,
},
201
);
} catch (err) {
logger.error('food.upload_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Upload failed' }, 500);
}
});
// ─── Photo Analysis (Gemini Vision on uploaded URL) ──────────
routes.post('/analysis/photo', async (c) => {
const { photoUrl } = await c.req.json();
if (!photoUrl) return c.json({ error: 'photoUrl required' }, 400);
try {
const { object } = await generateObject({
model: llm(VISION_MODEL),
schema: MealAnalysisSchema,
messages: [
{
role: 'system',
content: ANALYSIS_PROMPT,
providerOptions: SYSTEM_CACHE_HINT,
},
{
role: 'user',
content: [
{ type: 'text', text: 'Analysiere diese Mahlzeit.' },
{ type: 'image', image: new URL(photoUrl) },
],
},
],
temperature: 0.3,
});
return c.json(envelope(object));
} catch (err) {
logger.error('food.photo_analysis_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Analysis failed' }, 500);
}
});
// ─── Text Analysis (Gemini on a free-text meal description) ──
routes.post('/analysis/text', async (c) => {
const { description } = await c.req.json();
if (!description) return c.json({ error: 'description required' }, 400);
try {
const { object } = await generateObject({
model: llm(VISION_MODEL),
schema: MealAnalysisSchema,
messages: [
{
role: 'system',
content: ANALYSIS_PROMPT,
providerOptions: SYSTEM_CACHE_HINT,
},
{
role: 'user',
content: `Analysiere diese Mahlzeit: ${description}`,
},
],
temperature: 0.3,
});
return c.json(envelope(object));
} catch (err) {
logger.error('food.text_analysis_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Analysis failed' }, 500);
}
});
// ─── Recommendations (server-only: rule engine) ──────────────
routes.post('/recommendations/generate', async (c) => {
const { dailyNutrition } = await c.req.json();
const hints: Array<{ type: string; priority: string; message: string; nutrient?: string }> = [];
if (dailyNutrition) {
if (dailyNutrition.protein < 25) {
hints.push({
type: 'hint',
priority: 'medium',
message:
'Deine Proteinzufuhr ist niedrig. Versuche Hülsenfrüchte, Eier oder Joghurt einzubauen.',
nutrient: 'protein',
});
}
if (dailyNutrition.fiber < 10) {
hints.push({
type: 'hint',
priority: 'medium',
message: 'Mehr Ballaststoffe! Vollkornprodukte, Gemüse und Obst helfen.',
nutrient: 'fiber',
});
}
if (dailyNutrition.sugar > 50) {
hints.push({
type: 'hint',
priority: 'high',
message:
'Dein Zuckerkonsum ist hoch. Achte auf versteckten Zucker in Getränken und Fertigprodukten.',
nutrient: 'sugar',
});
}
}
return c.json({ recommendations: hints });
});
export { routes as foodRoutes };

View file

@ -1,41 +0,0 @@
/**
* Moodlit module Preset moods library
* Ported from apps/moodlit/apps/server
*
* Local-first for user moods/sequences.
* This module serves the default preset library.
*/
import { Hono } from 'hono';
const DEFAULT_MOODS = [
{ id: 'fire', name: 'Fire', colors: ['#ff6b35', '#f72585', '#ff006e'], animation: 'flicker' },
{ id: 'breath', name: 'Breath', colors: ['#4361ee', '#3a0ca3', '#7209b7'], animation: 'pulse' },
{
id: 'northern-lights',
name: 'Northern Lights',
colors: ['#06d6a0', '#118ab2', '#073b4c'],
animation: 'aurora',
},
{ id: 'thunder', name: 'Thunder', colors: ['#14213d', '#fca311', '#e5e5e5'], animation: 'flash' },
{
id: 'sunset',
name: 'Sunset',
colors: ['#ff6b6b', '#feca57', '#ff9ff3'],
animation: 'gradient',
},
{ id: 'ocean', name: 'Ocean', colors: ['#0077b6', '#00b4d8', '#90e0ef'], animation: 'wave' },
{ id: 'forest', name: 'Forest', colors: ['#2d6a4f', '#40916c', '#52b788'], animation: 'sway' },
{
id: 'lavender',
name: 'Lavender',
colors: ['#7b2cbf', '#9d4edd', '#c77dff'],
animation: 'pulse',
},
];
const routes = new Hono();
routes.get('/presets', (c) => c.json(DEFAULT_MOODS));
export { routes as moodlitRoutes };

View file

@ -254,11 +254,8 @@ routes.post('/generate', async (c) => {
// image input natively. Replicate/local fallback is a later milestone.
// OpenAI gpt-image-1 / gpt-image-2 accept up to 16 reference images per
// edit call. We clamp at 8 to cover the Wardrobe try-on workflow — one
// face-ref + one body-ref + up to six garment photos (top/bottom/shoes/
// outerwear + two accessories) — while keeping credit exposure and
// upload payload size predictable. Pre-wardrobe the cap was 4; bumped
// in docs/plans/wardrobe-module.md M1.
// edit call. We clamp at 8 to keep credit exposure and upload payload
// size predictable.
const MAX_REFERENCE_IMAGES = 8;
routes.post('/generate-with-reference', async (c) => {
@ -318,18 +315,14 @@ routes.post('/generate-with-reference', async (c) => {
}
// Ownership check before we spend credits or burn OpenAI quota.
// References span three upload tags today:
// - `me` — face/body portraits from the profile module
// - `wardrobe` — garment photos (M4 try-on flow)
// - `comic` — comic-specific anchor / backdrop uploads
// (slot reserved for M6+; no writer lands in
// this app today, M1 character refs come from
// me + wardrobe only).
// References span two upload tags today:
// - `me` — face/body portraits from the profile module
// - `comic` — comic-specific anchor / backdrop uploads
// Anything outside these apps is treated as not-owned regardless of
// mana-media's own view.
try {
const { verifyMediaOwnership } = await import('../../lib/media');
await verifyMediaOwnership(userId, refIds, ['me', 'wardrobe', 'comic']);
await verifyMediaOwnership(userId, refIds, ['me', 'comic']);
} catch (err) {
const e = err as Error & { status?: number; missing?: string[] };
if (e.status === 404) {

View file

@ -1,55 +0,0 @@
/**
* Wardrobe module server endpoints.
*
* Thin wrapper around mana-media for garment photo uploads. Plan:
* docs/plans/wardrobe-module.md M1. No logic beyond tagging uploads
* as `app='wardrobe'` so a later `GET /api/v1/media?app=wardrobe&...`
* query can enumerate a user's garment pool without scanning every
* media reference.
*
* Try-on generation does NOT live here it reuses the Picture
* module's POST /api/v1/picture/generate-with-reference endpoint
* with MAX_REFERENCE_IMAGES bumped to 8 so face + body + garments
* fit into one call.
*/
import { Hono } from 'hono';
import type { AuthVariables } from '@mana/shared-hono';
const routes = new Hono<{ Variables: AuthVariables }>();
// Same 10MB cap as the other photo-upload endpoints (profile me-images,
// picture uploads). Phone-camera PNG/HEIC routinely comes in under 6MB.
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
routes.post('/garments/upload', async (c) => {
const userId = c.get('userId');
const formData = await c.req.formData();
const file = formData.get('file') as File | null;
if (!file) return c.json({ error: 'No file' }, 400);
if (file.size > MAX_UPLOAD_BYTES) return c.json({ error: 'Max 10MB' }, 400);
try {
const { uploadImageToMedia } = await import('../../lib/media');
const buffer = await file.arrayBuffer();
const result = await uploadImageToMedia(buffer, file.name, {
app: 'wardrobe',
userId,
});
return c.json(
{
mediaId: result.id,
storagePath: result.id,
publicUrl: result.urls.original,
thumbnailUrl: result.urls.thumbnail,
},
201
);
} catch (_err) {
return c.json({ error: 'Upload failed' }, 500);
}
});
export { routes as wardrobeRoutes };

View file

@ -1,126 +0,0 @@
# CityCorners
Open platform for city guides worldwide — users create cities and add locations, growing the platform organically from the community.
## Live URLs
| Service | URL |
|---------|-----|
| **Web App** | https://citycorners.mana.how |
| **Landing** | https://citycorners-landing.pages.dev |
## Architecture
```
apps/citycorners/
├── apps/
│ ├── landing/ # Astro static site (Tailwind, Cloudflare Pages)
│ └── web/ # SvelteKit web app (port 5196 dev, 5022 prod)
└── CLAUDE.md
```
### Tech Stack
- **Data Layer:** Local-first via @mana/local-store (Dexie.js/IndexedDB)
- **Sync:** mana-sync (Go, WebSocket) for server synchronization
- **Web:** SvelteKit 2, Svelte 5 runes, Tailwind 4, OpenStreetMap embeds, svelte-i18n (DE/EN), PWA
- **Landing:** Astro 5, Tailwind 3, static site generation
- **Auth:** mana-auth (JWT, guest mode supported)
## Development
```bash
# Full stack (auth + web)
pnpm dev:citycorners:full
# Individual apps
pnpm dev:citycorners:landing
pnpm dev:citycorners:web
```
## Data Model (Local-First)
Three IndexedDB collections managed by `@mana/local-store`:
### Cities
- **id** (string, PK)
- **name** (string) — city/village/town name
- **slug** (string, indexed) — URL-friendly name
- **country** (string, indexed)
- **state** (string, optional) — state/region
- **description** (string, optional)
- **latitude** (number) — center coordinates
- **longitude** (number)
- **imageUrl** (string, optional)
- **createdBy** (string, optional) — user ID
### Locations
- **id** (string, PK)
- **cityId** (string, indexed, FK → cities)
- **name** (string, indexed)
- **category** (enum, indexed: sight/restaurant/shop/museum/cafe/bar/park/beach/hotel/event_venue/viewpoint)
- **description** (string, optional)
- **address** (string, optional)
- **latitude/longitude** (number, optional)
- **imageUrl** (string, optional)
- **timeline** (JSON array of {year, event}, optional)
### Favorites
- **id** (string, PK)
- **locationId** (string, indexed, FK → locations)
## Web App Routes
| Route | Description |
|-------|-------------|
| `/` | City discovery — search & browse cities |
| `/add-city` | Create a new city (auth required) |
| `/cities/:slug` | City home — location grid with category filters |
| `/cities/:slug/map` | OpenStreetMap with location list |
| `/cities/:slug/add` | Add a location to city (auth required) |
| `/cities/:slug/locations/:id` | Location detail with map, timeline, nearby |
| `/cities/:slug/locations/:id/edit` | Edit location (creator only) |
| `/favorites` | User's saved locations |
| `/settings` | Theme mode/variant, account, about |
| `/login`, `/register` | Auth via shared-auth-ui |
| `/offline` | PWA offline fallback |
## Features
- **Multi-City Platform:** Users create cities/villages and add locations within them
- **Local-First:** All CRUD via IndexedDB, works offline, syncs to server
- **Guest Mode:** Browse with seed data (Konstanz, Zürich, Berlin)
- **PWA:** Installable, offline fallback, service worker caching
- **i18n:** German + English, language switcher
- **Context-Aware Navigation:** Nav items change based on city context
- **Categories:** 11 location types with color-coded markers
- **Favorites:** Heart button on cards, auth-gated
- **Geocoding:** Auto-coordinates from city/address names (Nominatim)
- **Slug Generation:** Auto-generated URL-safe slugs with umlaut handling
## Categories
| DB Value | Label (DE) | Label (EN) | Marker Color |
|----------|------------|------------|------------|
| `sight` | Sehenswürdigkeit | Sight | Blue |
| `restaurant` | Restaurant | Restaurant | Red |
| `shop` | Laden | Shop | Green |
| `museum` | Museum | Museum | Purple |
| `cafe` | Café | Café | Amber |
| `bar` | Bar | Bar | Orange |
| `park` | Park | Park | Emerald |
| `beach` | Strandbad | Beach | Cyan |
| `hotel` | Hotel | Hotel | Indigo |
| `event_venue` | Veranstaltungsort | Event Venue | Pink |
| `viewpoint` | Aussichtspunkt | Viewpoint | Sky |
## Docker
- **Web:** `apps/citycorners/apps/web/Dockerfile` (multi-stage, port 5022 prod)
- **docker-compose.macmini.yml:** Web service with health check
## Environment Variables
| Variable | Used by | Description |
|----------|---------|-------------|
| `PUBLIC_MANA_AUTH_URL` | Web | Auth service URL (client) |
| `PUBLIC_SYNC_SERVER_URL` | Web | mana-sync WebSocket URL |

View file

@ -1,26 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
astro_dev.log
server.log
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

View file

@ -1,46 +0,0 @@
# Astro Starter Kit: Basics
```sh
npm create astro@latest -- --template basics
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src
│   ├── assets
│   │   └── astro.svg
│   ├── components
│   │   └── Welcome.astro
│   ├── layouts
│   │   └── Layout.astro
│   └── pages
│   └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

View file

@ -1,8 +0,0 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://citycorners.mana.how',
integrations: [tailwind(), sitemap()],
});

View file

@ -1,22 +0,0 @@
{
"name": "@citycorners/landing",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/tailwind": "^6.0.0",
"astro": "^5.16.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.0.0"
}
}

View file

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View file

@ -1,25 +0,0 @@
---
interface Props {
title?: string;
}
const { title = 'CityCorners Entdecke Städte weltweit' } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta
name="description"
content="CityCorners Die offene Plattform für Stadtführer. Entdecke Orte weltweit, geteilt von der Community."
/>
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body class="min-h-screen bg-gray-50 text-gray-900 antialiased">
<slot />
</body>
</html>

View file

@ -1,157 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
const APP_URL = 'https://citycorners.mana.how';
const exampleCities = [
{
name: 'Konstanz',
country: 'Deutschland',
description: 'Universitätsstadt am Bodensee mit mittelalterlicher Altstadt',
slug: 'konstanz',
},
{
name: 'Zürich',
country: 'Schweiz',
description: 'Größte Stadt der Schweiz am Zürichsee',
slug: 'zuerich',
},
{
name: 'Berlin',
country: 'Deutschland',
description: 'Hauptstadt mit vielfältiger Kultur und Geschichte',
slug: 'berlin',
},
];
---
<Layout>
<!-- Hero -->
<header class="bg-gradient-to-br from-blue-600 to-blue-800 py-20 text-white">
<div class="mx-auto max-w-4xl px-6 text-center">
<h1 class="text-4xl font-bold sm:text-5xl lg:text-6xl">CityCorners</h1>
<p class="mt-4 text-xl text-blue-100">Entdecke Städte weltweit</p>
<p class="mt-2 text-blue-200">
Von der Community für die Community — teile deine Lieblingsorte
</p>
<div class="mt-8 flex flex-col items-center gap-3 sm:flex-row sm:justify-center">
<a
href={APP_URL}
class="rounded-lg bg-white px-8 py-3 text-lg font-semibold text-blue-700 shadow-lg transition-all hover:bg-blue-50 hover:shadow-xl"
>
App öffnen
</a>
<a
href={`${APP_URL}/add-city`}
class="rounded-lg border-2 border-white/30 px-8 py-3 text-lg font-semibold text-white transition-all hover:border-white/60 hover:bg-white/10"
>
Stadt hinzufügen
</a>
</div>
</div>
</header>
<!-- How it works -->
<section class="mx-auto max-w-5xl px-6 py-16">
<h2 class="mb-10 text-center text-3xl font-bold text-gray-900">So funktioniert's</h2>
<div class="grid gap-8 sm:grid-cols-3">
<div class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-blue-100 text-3xl"
>
🏙️
</div>
<h3 class="text-lg font-semibold">Stadt anlegen</h3>
<p class="mt-2 text-gray-600">
Lege deine Stadt, dein Dorf oder deinen Lieblingsort an — egal wo auf der Welt.
</p>
</div>
<div class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-green-100 text-3xl"
>
📍
</div>
<h3 class="text-lg font-semibold">Orte hinzufügen</h3>
<p class="mt-2 text-gray-600">
Teile Restaurants, Sehenswürdigkeiten, Cafés, Parks und mehr mit der Community.
</p>
</div>
<div class="text-center">
<div
class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-100 text-3xl"
>
🗺️
</div>
<h3 class="text-lg font-semibold">Entdecken</h3>
<p class="mt-2 text-gray-600">
Erkunde Städte auf der Karte, filtere nach Kategorien und speichere Favoriten.
</p>
</div>
</div>
</section>
<!-- Example cities -->
<section class="bg-white py-16">
<div class="mx-auto max-w-5xl px-6">
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">Beispielstädte</h2>
<div class="grid gap-6 sm:grid-cols-3">
{
exampleCities.map((city) => (
<a
href={`${APP_URL}/cities/${city.slug}`}
class="group rounded-xl border border-gray-200 bg-gray-50 p-6 transition-all hover:border-blue-300 hover:shadow-md"
>
<h3 class="text-xl font-semibold text-gray-900 group-hover:text-blue-600">
{city.name}
</h3>
<p class="mt-1 text-sm text-gray-500">{city.country}</p>
<p class="mt-2 text-gray-600">{city.description}</p>
</a>
))
}
</div>
</div>
</section>
<!-- Features -->
<section class="mx-auto max-w-5xl px-6 py-16">
<h2 class="mb-8 text-center text-3xl font-bold text-gray-900">Features</h2>
<div class="grid gap-6 sm:grid-cols-2">
<div class="rounded-xl border border-gray-200 p-5">
<h3 class="font-semibold text-gray-900">Offline verfügbar</h3>
<p class="mt-1 text-sm text-gray-600">
Funktioniert auch ohne Internet — Daten werden lokal gespeichert und automatisch
synchronisiert.
</p>
</div>
<div class="rounded-xl border border-gray-200 p-5">
<h3 class="font-semibold text-gray-900">11 Kategorien</h3>
<p class="mt-1 text-sm text-gray-600">
Restaurants, Cafés, Museen, Parks, Hotels, Bars, Sehenswürdigkeiten und mehr.
</p>
</div>
<div class="rounded-xl border border-gray-200 p-5">
<h3 class="font-semibold text-gray-900">Interaktive Karte</h3>
<p class="mt-1 text-sm text-gray-600">
Farbcodierte Marker auf OpenStreetMap mit Standortbestimmung.
</p>
</div>
<div class="rounded-xl border border-gray-200 p-5">
<h3 class="font-semibold text-gray-900">Mehrsprachig</h3>
<p class="mt-1 text-sm text-gray-600">Deutsch und Englisch mit einfachem Sprachwechsel.</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-gray-200 bg-white py-8">
<div class="mx-auto max-w-6xl px-6 text-center text-sm text-gray-500">
<p>CityCorners Die offene Plattform für Stadtführer</p>
<p class="mt-1">
Teil des <a href="https://mana.how" class="text-blue-600 hover:underline">Mana</a>{' '}
Ökosystems
</p>
</div>
</footer>
</Layout>

View file

@ -1,13 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
primary: '#2563eb',
'primary-dark': '#1d4ed8',
},
},
},
plugins: [],
};

View file

@ -1,5 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

View file

@ -1,6 +0,0 @@
# Cloudflare Pages configuration for CityCorners Landing
# Deployed via GitHub Actions (Direct Upload)
name = "citycorners-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"

View file

@ -1,17 +0,0 @@
# Food — consolidated into the unified Mana app
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/food/apps/backend/` and `apps/food/apps/web/`
directories have been removed. Active code now lives in:
- **Backend compute routes**: [`apps/api/src/modules/food/routes.ts`](../api/src/modules/food/routes.ts) (Gemini meal-photo analysis + text analysis + recommendations)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/food/`](../mana/apps/web/src/lib/modules/food/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/food/`](../mana/apps/web/src/routes/(app)/food/)
- **Landing page** (still standalone): [`apps/food/apps/landing/`](apps/landing/)
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
The previous standalone "Food Project Guide" was deleted in the
audit cleanup of 2026-04-09 — it had been inaccurate since the
consolidation. Pre-consolidation reference is in git history.

View file

@ -1,7 +0,0 @@
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
export default defineConfig({
integrations: [tailwind()],
site: 'https://food.mana.how',
});

View file

@ -1,34 +0,0 @@
{
"name": "@food/landing",
"version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev --port 4323",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"type-check": "astro check",
"format": "prettier --write .",
"clean": "rm -rf dist .astro node_modules"
},
"dependencies": {
"@astrojs/check": "^0.9.0",
"@mana/shared-landing-ui": "workspace:*",
"astro": "^5.16.0",
"typescript": "^5.9.2"
},
"devDependencies": {
"@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.18",
"@types/node": "^20.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^1.0.0",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^3.4.0"
}
}

View file

@ -1,58 +0,0 @@
---
import Analytics from '@mana/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
description?: string;
}
const { title, description = 'Food - KI-gestützte Ernährungsanalyse per Foto' } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<!-- Theme Color -->
<meta name="theme-color" content="#22C55E" />
<!-- Umami Analytics -->
{
import.meta.env.PUBLIC_UMAMI_WEBSITE_ID && (
<script
defer
src="https://stats.mana.how/script.js"
data-website-id={import.meta.env.PUBLIC_UMAMI_WEBSITE_ID}
/>
)
}
</head>
<body class="bg-[#0F1F0F] text-gray-100 antialiased">
<slot />
<Analytics />
</body>
</html>
<style is:global>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
html {
font-family: 'Inter', system-ui, sans-serif;
scroll-behavior: smooth;
}
body {
min-height: 100vh;
}
</style>

View file

@ -1,270 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
const features = [
{
icon: '📸',
title: 'Foto-Analyse',
description: 'Mach einfach ein Foto von deinem Essen und lass die KI die Nährwerte berechnen.',
},
{
icon: '🥗',
title: 'Vollständige Nährwerte',
description: 'Kalorien, Makros, Vitamine und Mineralstoffe auf einen Blick.',
},
{
icon: '🎯',
title: 'Persönliche Ziele',
description: 'Setze deine Tagesziele und verfolge deinen Fortschritt in Echtzeit.',
},
{
icon: '🤖',
title: 'KI-Coaching',
description: 'Erhalte personalisierte Empfehlungen basierend auf deinem Ernährungsverlauf.',
},
{
icon: '⭐',
title: 'Favoriten',
description: 'Speichere häufige Mahlzeiten und füge sie mit einem Klick hinzu.',
},
{
icon: '🔒',
title: 'Maximaler Datenschutz',
description: 'Deine Fotos werden nie gespeichert. Nur die Analyseergebnisse bleiben.',
},
];
const steps = [
{
number: '1',
title: 'Foto machen',
description: 'Fotografiere deine Mahlzeit mit der Kamera oder wähle ein bestehendes Bild.',
},
{
number: '2',
title: 'KI analysiert',
description: 'Unsere KI erkennt die Lebensmittel und berechnet alle Nährwerte in Sekunden.',
},
{
number: '3',
title: 'Insights erhalten',
description: 'Sieh deine Tagesbilanz, verfolge Trends und erhalte personalisierte Tipps.',
},
];
const faqs = [
{
question: 'Wie genau ist die KI-Analyse?',
answer:
'Unsere KI erreicht eine Genauigkeit von 85-95% bei der Erkennung von Lebensmitteln. Bei komplexen Gerichten zeigen wir dir einen Konfidenz-Score an.',
},
{
question: 'Was passiert mit meinen Fotos?',
answer:
'Maximaler Datenschutz: Deine Fotos werden nach der Analyse sofort gelöscht und niemals auf unseren Servern gespeichert. Nur die Nährwertdaten werden gesichert.',
},
{
question: 'Kann ich auch ohne Foto tracken?',
answer:
'Ja! Du kannst Mahlzeiten auch per Text eingeben. Die KI schätzt dann die Nährwerte basierend auf deiner Beschreibung.',
},
{
question: 'Funktioniert die App mit allen Gerichten?',
answer:
'Food erkennt die meisten Gerichte weltweit, von klassischer deutscher Küche bis zu asiatischen Spezialitäten. Bei unbekannten Gerichten kannst du manuell nachhelfen.',
},
{
question: 'Wie funktioniert das Credit-System?',
answer:
'Jede Foto-Analyse kostet 5 Credits, Text-Analysen 2 Credits. Du erhältst täglich kostenlose Credits, oder du nutzt Mana Premium für unbegrenzte Analysen.',
},
{
question: 'Gibt es eine kostenlose Version?',
answer:
'Ja! Du kannst Food kostenlos nutzen mit täglich 3 Foto-Analysen. Für mehr Analysen und Premium-Features gibt es Mana Credits.',
},
];
---
<Layout title="Food - Ernährung verstehen per Foto">
<!-- Navigation -->
<nav
class="fixed top-0 left-0 right-0 z-50 bg-[#0F1F0F]/90 backdrop-blur border-b border-green-900/30"
>
<div class="container mx-auto px-4 max-w-6xl">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<span class="text-white font-bold">N</span>
</div>
<span class="font-semibold text-white">Food</span>
</div>
<a
href="https://food.mana.how"
class="px-4 py-2 bg-primary hover:bg-primary-hover text-white font-medium rounded-lg transition-colors"
>
Jetzt starten
</a>
</div>
</div>
</nav>
<main>
<!-- Hero Section -->
<section class="pt-32 pb-20 px-4">
<div class="container mx-auto max-w-4xl text-center">
<!-- Trust Badges -->
<div class="flex flex-wrap justify-center gap-3 mb-8">
<span
class="px-3 py-1 bg-green-900/30 border border-green-800/50 rounded-full text-sm text-green-300"
>
🔒 Datenschutz-First
</span>
<span
class="px-3 py-1 bg-green-900/30 border border-green-800/50 rounded-full text-sm text-green-300"
>
🤖 Powered by Gemini AI
</span>
<span
class="px-3 py-1 bg-green-900/30 border border-green-800/50 rounded-full text-sm text-green-300"
>
✨ Kostenlos starten
</span>
</div>
<h1 class="text-4xl md:text-6xl font-bold text-white mb-6 leading-tight">
Fotografiere dein Essen.
<span class="text-primary">Verstehe deinen Körper.</span>
</h1>
<p class="text-xl text-gray-300 mb-10 max-w-2xl mx-auto">
Food analysiert deine Mahlzeiten per Foto und liefert dir sofort alle Nährwerte. Mit
KI-Coaching erreichst du deine Gesundheitsziele.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="https://food.mana.how"
class="px-8 py-4 bg-primary hover:bg-primary-hover text-white font-semibold rounded-xl transition-colors text-lg"
>
Kostenlos starten
</a>
<a
href="#features"
class="px-8 py-4 bg-white/10 hover:bg-white/20 text-white font-semibold rounded-xl transition-colors text-lg"
>
Mehr erfahren
</a>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-20 px-4 bg-[#1A2F1A]">
<div class="container mx-auto max-w-6xl">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Alles was du brauchst</h2>
<p class="text-gray-300 max-w-2xl mx-auto">
Food macht Ernährungstracking so einfach wie ein Foto.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{
features.map((feature) => (
<div class="p-6 bg-[#0F1F0F] border border-green-900/30 rounded-2xl hover:border-primary/50 transition-colors">
<div class="text-4xl mb-4">{feature.icon}</div>
<h3 class="text-xl font-semibold text-white mb-2">{feature.title}</h3>
<p class="text-gray-400">{feature.description}</p>
</div>
))
}
</div>
</div>
</section>
<!-- How it Works -->
<section class="py-20 px-4">
<div class="container mx-auto max-w-4xl">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">So einfach geht's</h2>
<p class="text-gray-300">In 3 Schritten zu besserer Ernährung</p>
</div>
<div class="space-y-8">
{
steps.map((step, index) => (
<div class="flex items-start gap-6">
<div class="flex-shrink-0 w-12 h-12 rounded-full bg-primary flex items-center justify-center text-white font-bold text-xl">
{step.number}
</div>
<div>
<h3 class="text-xl font-semibold text-white mb-2">{step.title}</h3>
<p class="text-gray-400">{step.description}</p>
</div>
</div>
))
}
</div>
</div>
</section>
<!-- FAQ Section -->
<section class="py-20 px-4 bg-[#1A2F1A]">
<div class="container mx-auto max-w-3xl">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Häufige Fragen</h2>
</div>
<div class="space-y-4">
{
faqs.map((faq) => (
<details class="group bg-[#0F1F0F] border border-green-900/30 rounded-xl">
<summary class="flex items-center justify-between p-5 cursor-pointer list-none">
<span class="font-medium text-white">{faq.question}</span>
<span class="text-primary group-open:rotate-180 transition-transform">▼</span>
</summary>
<p class="px-5 pb-5 text-gray-400">{faq.answer}</p>
</details>
))
}
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-20 px-4">
<div class="container mx-auto max-w-4xl text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-6">Starte jetzt mit Food</h2>
<p class="text-xl text-gray-300 mb-10 max-w-2xl mx-auto">
Kostenlos. Ohne Kreditkarte. Sofort loslegen.
</p>
<a
href="https://food.mana.how"
class="inline-block px-10 py-4 bg-primary hover:bg-primary-hover text-white font-semibold rounded-xl transition-colors text-lg"
>
Jetzt kostenlos registrieren
</a>
</div>
</section>
</main>
<!-- Footer -->
<footer class="py-10 px-4 border-t border-green-900/30">
<div class="container mx-auto max-w-6xl">
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded bg-primary flex items-center justify-center">
<span class="text-white font-bold text-xs">N</span>
</div>
<span class="text-sm text-gray-400">Food by Mana</span>
</div>
<div class="flex gap-6 text-sm text-gray-400">
<a href="/privacy" class="hover:text-white transition-colors">Datenschutz</a>
<a href="/terms" class="hover:text-white transition-colors">AGB</a>
<a href="/imprint" class="hover:text-white transition-colors">Impressum</a>
</div>
</div>
</div>
</footer>
</Layout>

View file

@ -1,18 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#22C55E',
hover: '#16A34A',
light: '#86EFAC',
},
secondary: '#F97316',
accent: '#14B8A6',
},
},
},
plugins: [require('@tailwindcss/typography')],
};

View file

@ -1,10 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"]
}
}
}

View file

@ -1,3 +0,0 @@
name = "food-landing"
compatibility_date = "2024-12-01"
pages_build_output_dir = "dist"

View file

@ -1,24 +0,0 @@
{
"name": "food",
"version": "0.2.0",
"private": true,
"description": "Food - AI-powered nutrition tracking with photo analysis",
"scripts": {
"dev": "pnpm run --filter=@food/* --parallel dev",
"dev:server": "pnpm --filter @food/server dev",
"dev:web": "pnpm --filter @food/web dev",
"dev:landing": "pnpm --filter @food/landing dev",
"db:push": "pnpm --filter @food/server db:push",
"db:studio": "pnpm --filter @food/server db:studio",
"db:seed": "pnpm --filter @food/server db:seed",
"test": "pnpm --filter @food/server test && pnpm --filter @food/shared test && pnpm --filter @food/web test",
"test:backend": "pnpm --filter @food/server test",
"test:web": "pnpm --filter @food/web test",
"test:shared": "pnpm --filter @food/shared test",
"test:cov": "pnpm --filter @food/server test:cov"
},
"devDependencies": {
"typescript": "^5.9.3"
},
"packageManager": "pnpm@9.15.0"
}

View file

@ -1,23 +0,0 @@
{
"name": "@food/shared",
"version": "0.2.0",
"type": "commonjs",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./types": "./src/types/index.ts",
"./utils": "./src/utils/index.ts",
"./constants": "./src/constants/index.ts"
},
"scripts": {
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "~5.9.2",
"vitest": "^4.1.2"
},
"dependencies": {}
}

View file

@ -1,124 +0,0 @@
// Default daily recommended values (based on 2000 kcal diet)
export const DEFAULT_DAILY_VALUES = {
calories: 2000,
protein: 50, // grams
carbohydrates: 275, // grams
fat: 78, // grams
fiber: 28, // grams
sugar: 50, // grams (max)
saturatedFat: 20, // grams (max)
// Vitamins
vitaminA: 900, // µg RAE
vitaminB1: 1.2, // mg
vitaminB2: 1.3, // mg
vitaminB3: 16, // mg
vitaminB5: 5, // mg
vitaminB6: 1.7, // mg
vitaminB7: 30, // µg
vitaminB9: 400, // µg
vitaminB12: 2.4, // µg
vitaminC: 90, // mg
vitaminD: 20, // µg
vitaminE: 15, // mg
vitaminK: 120, // µg
// Minerals
calcium: 1000, // mg
iron: 18, // mg
magnesium: 420, // mg
phosphorus: 700, // mg
potassium: 4700, // mg
sodium: 2300, // mg (max)
zinc: 11, // mg
copper: 0.9, // mg
manganese: 2.3, // mg
selenium: 55, // µg
} as const;
// Meal type labels
export const MEAL_TYPE_LABELS = {
breakfast: {
de: 'Frühstück',
en: 'Breakfast',
},
lunch: {
de: 'Mittagessen',
en: 'Lunch',
},
dinner: {
de: 'Abendessen',
en: 'Dinner',
},
snack: {
de: 'Snack',
en: 'Snack',
},
} as const;
// Nutrient categories for UI grouping
export const NUTRIENT_CATEGORIES = {
macros: ['calories', 'protein', 'carbohydrates', 'fat', 'fiber', 'sugar'],
vitamins: [
'vitaminA',
'vitaminB1',
'vitaminB2',
'vitaminB3',
'vitaminB5',
'vitaminB6',
'vitaminB7',
'vitaminB9',
'vitaminB12',
'vitaminC',
'vitaminD',
'vitaminE',
'vitaminK',
],
minerals: [
'calcium',
'iron',
'magnesium',
'phosphorus',
'potassium',
'sodium',
'zinc',
'copper',
'manganese',
'selenium',
],
} as const;
// Nutrient display info
export const NUTRIENT_INFO = {
calories: { label: 'Kalorien', unit: 'kcal', color: '#F59E0B' },
protein: { label: 'Protein', unit: 'g', color: '#EF4444' },
carbohydrates: { label: 'Kohlenhydrate', unit: 'g', color: '#3B82F6' },
fat: { label: 'Fett', unit: 'g', color: '#8B5CF6' },
fiber: { label: 'Ballaststoffe', unit: 'g', color: '#10B981' },
sugar: { label: 'Zucker', unit: 'g', color: '#EC4899' },
vitaminA: { label: 'Vitamin A', unit: 'µg', color: '#F97316' },
vitaminC: { label: 'Vitamin C', unit: 'mg', color: '#FBBF24' },
vitaminD: { label: 'Vitamin D', unit: 'µg', color: '#A3E635' },
calcium: { label: 'Calcium', unit: 'mg', color: '#E5E7EB' },
iron: { label: 'Eisen', unit: 'mg', color: '#78716C' },
magnesium: { label: 'Magnesium', unit: 'mg', color: '#06B6D4' },
} as const;
// Credit costs per action
export const CREDIT_COSTS = {
photoAnalysis: 5,
textAnalysis: 2,
aiCoaching: 10,
} as const;
// Theme colors
export const FOOD_COLORS = {
primary: '#22C55E', // Green 500
primaryHover: '#16A34A', // Green 600
primaryLight: '#86EFAC', // Green 300
secondary: '#F97316', // Orange 500
accent: '#14B8A6', // Teal 500
background: '#0F1F0F', // Dark green tinted
backgroundCard: '#1A2F1A',
textPrimary: '#F0FDF4', // Green 50
textSecondary: '#BBF7D0', // Green 200
border: '#22543D', // Green 800
} as const;

View file

@ -1,8 +0,0 @@
// Types
export * from './types';
// Constants
export * from './constants';
// Utils
export * from './utils';

View file

@ -1,185 +0,0 @@
// User Goals
export interface UserGoals {
id: string;
userId: string;
dailyCalories: number;
dailyProtein?: number | null; // in grams
dailyCarbs?: number | null;
dailyFat?: number | null;
dailyFiber?: number | null;
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserGoalsDto {
dailyCalories: number;
dailyProtein?: number;
dailyCarbs?: number;
dailyFat?: number;
dailyFiber?: number;
}
// Meal Types
export type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
export type InputType = 'photo' | 'text';
// Meal
export interface Meal {
id: string;
userId: string;
date: Date;
mealType: MealType;
inputType: InputType;
description: string; // AI-generated description of the meal
portionSize?: string; // e.g., "small", "medium", "large" or grams
confidence: number; // AI confidence score 0-1
createdAt: Date;
}
export interface CreateMealDto {
mealType: MealType;
inputType: InputType;
description?: string; // For text input
imageBase64?: string; // For photo input
portionSize?: string;
}
// Nutrition Data
export interface MealNutrition {
id: string;
mealId: string;
// Macros
calories: number;
protein: number;
carbohydrates: number;
fat: number;
fiber: number;
sugar: number;
saturatedFat?: number | null;
unsaturatedFat?: number | null;
// Vitamins (in mg or µg as appropriate)
vitaminA?: number | null; // µg RAE
vitaminB1?: number | null; // mg (Thiamin)
vitaminB2?: number | null; // mg (Riboflavin)
vitaminB3?: number | null; // mg (Niacin)
vitaminB5?: number | null; // mg (Pantothenic acid)
vitaminB6?: number | null; // mg
vitaminB7?: number | null; // µg (Biotin)
vitaminB9?: number | null; // µg (Folate)
vitaminB12?: number | null; // µg
vitaminC?: number | null; // mg
vitaminD?: number | null; // µg
vitaminE?: number | null; // mg
vitaminK?: number | null; // µg
// Minerals (in mg)
calcium?: number | null;
iron?: number | null;
magnesium?: number | null;
phosphorus?: number | null;
potassium?: number | null;
sodium?: number | null;
zinc?: number | null;
copper?: number | null;
manganese?: number | null;
selenium?: number | null; // µg
// Water
water?: number | null; // ml
}
// Favorite Meals
export interface FavoriteMeal {
id: string;
userId: string;
name: string;
description: string;
mealType: MealType;
nutrition: Omit<MealNutrition, 'id' | 'mealId'>;
usageCount: number;
createdAt: Date;
updatedAt: Date;
}
export interface CreateFavoriteMealDto {
name: string;
mealId?: string; // Create from existing meal
description?: string;
mealType?: MealType;
}
// Daily Summary
export interface DailySummary {
date: Date;
meals: Meal[];
totalNutrition: Omit<MealNutrition, 'id' | 'mealId'>;
goals?: UserGoals;
progress: NutritionProgress;
}
export interface NutritionProgress {
calories: { current: number; target: number; percentage: number };
protein?: { current: number; target: number; percentage: number };
carbs?: { current: number; target: number; percentage: number };
fat?: { current: number; target: number; percentage: number };
}
// Recommendations
export type RecommendationType = 'hint' | 'coaching';
export type RecommendationPriority = 'low' | 'medium' | 'high';
export interface Recommendation {
id: string;
userId: string;
date: Date;
type: RecommendationType;
priority: RecommendationPriority;
message: string;
nutrient?: string; // e.g., 'protein', 'vitaminC'
actionable?: string; // e.g., "Add more leafy greens"
dismissed: boolean;
createdAt: Date;
}
// Weekly Stats
export interface WeeklyStats {
startDate: Date;
endDate: Date;
days: DailyStats[];
averages: {
calories: number;
protein: number;
carbs: number;
fat: number;
};
trends: {
caloriesTrend: 'up' | 'down' | 'stable';
proteinTrend: 'up' | 'down' | 'stable';
};
}
export interface DailyStats {
date: Date;
totalCalories: number;
totalProtein: number;
totalCarbs: number;
totalFat: number;
mealCount: number;
goalsMet: boolean;
}
// AI Analysis Response
export interface AIAnalysisResult {
foods: DetectedFood[];
totalNutrition: Omit<MealNutrition, 'id' | 'mealId'>;
description: string;
confidence: number;
warnings?: string[]; // e.g., "Could not identify one item"
suggestions?: string[]; // e.g., "Consider adding more vegetables"
}
export interface DetectedFood {
name: string;
quantity: string; // e.g., "150g", "1 cup"
calories: number;
confidence: number;
source?: 'usda' | 'openfoodfacts' | 'ai_estimate';
}

View file

@ -1,174 +0,0 @@
import { DEFAULT_DAILY_VALUES, NUTRIENT_INFO } from '../constants';
import type { MealNutrition, NutritionProgress, UserGoals } from '../types';
/**
* Calculate nutrition progress towards daily goals
*/
export function calculateProgress(
totalNutrition: Partial<MealNutrition>,
goals?: UserGoals
): NutritionProgress {
const targetCalories = goals?.dailyCalories ?? DEFAULT_DAILY_VALUES.calories;
const targetProtein = goals?.dailyProtein ?? DEFAULT_DAILY_VALUES.protein;
const targetCarbs = goals?.dailyCarbs ?? DEFAULT_DAILY_VALUES.carbohydrates;
const targetFat = goals?.dailyFat ?? DEFAULT_DAILY_VALUES.fat;
return {
calories: {
current: totalNutrition.calories ?? 0,
target: targetCalories,
percentage: Math.min(
100,
Math.round(((totalNutrition.calories ?? 0) / targetCalories) * 100)
),
},
protein: {
current: totalNutrition.protein ?? 0,
target: targetProtein,
percentage: Math.min(100, Math.round(((totalNutrition.protein ?? 0) / targetProtein) * 100)),
},
carbs: {
current: totalNutrition.carbohydrates ?? 0,
target: targetCarbs,
percentage: Math.min(
100,
Math.round(((totalNutrition.carbohydrates ?? 0) / targetCarbs) * 100)
),
},
fat: {
current: totalNutrition.fat ?? 0,
target: targetFat,
percentage: Math.min(100, Math.round(((totalNutrition.fat ?? 0) / targetFat) * 100)),
},
};
}
/**
* Sum up nutrition from multiple meals
*/
export function sumNutrition(
meals: Array<{ nutrition?: Partial<MealNutrition> | null }>
): Partial<MealNutrition> {
const sum = {
calories: 0,
protein: 0,
carbohydrates: 0,
fat: 0,
fiber: 0,
sugar: 0,
};
for (const meal of meals) {
if (!meal.nutrition) continue;
const n = meal.nutrition;
if (typeof n.calories === 'number') sum.calories += n.calories;
if (typeof n.protein === 'number') sum.protein += n.protein;
if (typeof n.carbohydrates === 'number') sum.carbohydrates += n.carbohydrates;
if (typeof n.fat === 'number') sum.fat += n.fat;
if (typeof n.fiber === 'number') sum.fiber += n.fiber;
if (typeof n.sugar === 'number') sum.sugar += n.sugar;
}
return sum;
}
/**
* Format nutrient value with unit
*/
export function formatNutrient(
nutrient: keyof typeof NUTRIENT_INFO,
value: number | undefined
): string {
if (value === undefined) return '-';
const info = NUTRIENT_INFO[nutrient];
if (!info) return `${value}`;
if (nutrient === 'calories') {
return `${Math.round(value)} ${info.unit}`;
}
return `${value.toFixed(1)} ${info.unit}`;
}
/**
* Get color for progress percentage
*/
export function getProgressColor(percentage: number): string {
if (percentage < 50) return '#EF4444'; // Red
if (percentage < 80) return '#F59E0B'; // Orange
if (percentage <= 100) return '#22C55E'; // Green
return '#EF4444'; // Red (over target)
}
/**
* Detect deficiencies based on daily values
*/
export function detectDeficiencies(
totalNutrition: Partial<MealNutrition>
): Array<{ nutrient: string; percentage: number; label: string }> {
const deficiencies: Array<{ nutrient: string; percentage: number; label: string }> = [];
const checks = [
{ key: 'protein', threshold: 0.5 },
{ key: 'fiber', threshold: 0.5 },
{ key: 'vitaminC', threshold: 0.5 },
{ key: 'vitaminD', threshold: 0.5 },
{ key: 'iron', threshold: 0.5 },
{ key: 'calcium', threshold: 0.5 },
] as const;
for (const check of checks) {
const value = totalNutrition[check.key as keyof typeof totalNutrition];
const dailyValue = DEFAULT_DAILY_VALUES[check.key as keyof typeof DEFAULT_DAILY_VALUES];
if (
typeof value === 'number' &&
typeof dailyValue === 'number' &&
value < dailyValue * check.threshold
) {
const info = NUTRIENT_INFO[check.key as keyof typeof NUTRIENT_INFO];
deficiencies.push({
nutrient: check.key,
percentage: Math.round((value / dailyValue) * 100),
label: info?.label ?? check.key,
});
}
}
return deficiencies;
}
/**
* Get meal type based on current time
*/
export function suggestMealType(): 'breakfast' | 'lunch' | 'dinner' | 'snack' {
const hour = new Date().getHours();
if (hour >= 5 && hour < 11) return 'breakfast';
if (hour >= 11 && hour < 14) return 'lunch';
if (hour >= 17 && hour < 21) return 'dinner';
return 'snack';
}
/**
* Format date for display
*/
export function formatDateForDisplay(date: Date, locale = 'de-DE'): string {
return new Intl.DateTimeFormat(locale, {
weekday: 'long',
day: 'numeric',
month: 'long',
}).format(date);
}
/**
* Check if date is today
*/
export function isToday(date: Date): boolean {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
}

View file

@ -1,189 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
calculateProgress,
sumNutrition,
formatNutrient,
getProgressColor,
detectDeficiencies,
suggestMealType,
formatDateForDisplay,
isToday,
} from './index';
describe('Shared Utils', () => {
describe('calculateProgress', () => {
it('should calculate progress with default values', () => {
const nutrition = { calories: 1000, protein: 25, carbohydrates: 137, fat: 39 };
const progress = calculateProgress(nutrition);
expect(progress.calories.current).toBe(1000);
expect(progress.calories.target).toBe(2000);
expect(progress.calories.percentage).toBe(50);
});
it('should use custom goals', () => {
const nutrition = { calories: 1500, protein: 75 };
const goals = {
dailyCalories: 3000,
dailyProtein: 150,
dailyCarbs: 300,
dailyFat: 100,
} as any;
const progress = calculateProgress(nutrition, goals);
expect(progress.calories.target).toBe(3000);
expect(progress.calories.percentage).toBe(50);
});
it('should cap percentage at 100', () => {
const nutrition = { calories: 3000 };
const progress = calculateProgress(nutrition);
expect(progress.calories.percentage).toBe(100);
});
it('should handle missing values', () => {
const progress = calculateProgress({});
expect(progress.calories.current).toBe(0);
expect(progress.calories.percentage).toBe(0);
});
});
describe('sumNutrition', () => {
it('should sum multiple meals', () => {
const meals = [
{ nutrition: { calories: 500, protein: 20, carbohydrates: 60, fat: 15 } },
{ nutrition: { calories: 300, protein: 15, carbohydrates: 40, fat: 10 } },
];
const sum = sumNutrition(meals);
expect(sum.calories).toBe(800);
expect(sum.protein).toBe(35);
expect(sum.carbohydrates).toBe(100);
expect(sum.fat).toBe(25);
});
it('should handle null nutrition', () => {
const meals = [{ nutrition: { calories: 500 } }, { nutrition: null }];
const sum = sumNutrition(meals);
expect(sum.calories).toBe(500);
});
it('should handle empty array', () => {
const sum = sumNutrition([]);
expect(sum.calories).toBe(0);
});
});
describe('formatNutrient', () => {
it('should format calories', () => {
expect(formatNutrient('calories', 1234.5)).toBe('1235 kcal');
});
it('should format protein', () => {
expect(formatNutrient('protein', 25.5)).toBe('25.5 g');
});
it('should return dash for undefined', () => {
expect(formatNutrient('calories', undefined)).toBe('-');
});
});
describe('getProgressColor', () => {
it('should return red for low percentage', () => {
expect(getProgressColor(30)).toBe('#EF4444');
});
it('should return orange for medium percentage', () => {
expect(getProgressColor(60)).toBe('#F59E0B');
});
it('should return green for high percentage', () => {
expect(getProgressColor(90)).toBe('#22C55E');
});
it('should return red for over 100%', () => {
expect(getProgressColor(120)).toBe('#EF4444');
});
});
describe('detectDeficiencies', () => {
it('should detect low protein', () => {
const nutrition = { protein: 10 }; // 20% of 50g target
const deficiencies = detectDeficiencies(nutrition);
expect(deficiencies).toContainEqual(expect.objectContaining({ nutrient: 'protein' }));
});
it('should not detect deficiency when above threshold', () => {
const nutrition = { protein: 30 }; // 60% of target
const deficiencies = detectDeficiencies(nutrition);
expect(deficiencies.find((d) => d.nutrient === 'protein')).toBeUndefined();
});
});
describe('suggestMealType', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should suggest breakfast in the morning', () => {
vi.setSystemTime(new Date('2024-01-15T08:00:00'));
expect(suggestMealType()).toBe('breakfast');
});
it('should suggest lunch at noon', () => {
vi.setSystemTime(new Date('2024-01-15T12:00:00'));
expect(suggestMealType()).toBe('lunch');
});
it('should suggest dinner in the evening', () => {
vi.setSystemTime(new Date('2024-01-15T19:00:00'));
expect(suggestMealType()).toBe('dinner');
});
it('should suggest snack at other times', () => {
vi.setSystemTime(new Date('2024-01-15T15:00:00'));
expect(suggestMealType()).toBe('snack');
});
});
describe('formatDateForDisplay', () => {
it('should format date in German', () => {
const date = new Date('2024-01-15');
const formatted = formatDateForDisplay(date, 'de-DE');
expect(formatted).toContain('15');
expect(formatted).toContain('Januar');
});
});
describe('isToday', () => {
it('should return true for today', () => {
expect(isToday(new Date())).toBe(true);
});
it('should return false for yesterday', () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
expect(isToday(yesterday)).toBe(false);
});
it('should return false for same day different year', () => {
const lastYear = new Date();
lastYear.setFullYear(lastYear.getFullYear() - 1);
expect(isToday(lastYear)).toBe(false);
});
});
});

View file

@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["ES2021"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,9 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['src/**/*.{test,spec}.{js,ts}'],
environment: 'node',
globals: true,
},
});

View file

@ -252,7 +252,7 @@ The companion is a **second actor** that works alongside the human in every modu
- **Actor attribution** — every event, record, and sync row carries `{ kind, principalId, displayName }` (+ mission/iteration/rationale for AI). `principalId` is the userId / agentId / `system:<source>` sentinel; `displayName` is cached at write time so rename doesn't rewrite history. Factories in `@mana/shared-ai/src/actor.ts`; runtime ambient context in `src/lib/data/events/actor.ts`.
- **Agents** — named AI personas that own Missions. `/ai-agents` module for CRUD (policy editor, memory, budget, concurrency). Default "Mana" agent auto-bootstrapped on first login; legacy missions backfilled. `data/ai/agents/{store,queries,bootstrap}.ts`.
- **AI policy** — per-tool `auto | propose | deny`. Lives on the agent (`agent.policy`). Proposable tool names come from `@mana/shared-ai`'s `AI_PROPOSABLE_TOOL_NAMES`; the mana-ai service runs a boot-time drift guard against the same list. Resolution in `src/lib/data/ai/policy.ts`; executor loads `agent.policy` for every AI write.
- **Proposal inbox** — drop `<AiProposalInbox module="…" />` into any module page to render pending proposals inline with approve / freitext-reject buttons. Cards show the owning agent's name + avatar chip. Wired in `/todo`, `/calendar`, `/places`, `/drink`, `/food`, `/news`, `/notes`. The mission-detail view also embeds a **cross-module inbox** (`<AiProposalInbox missionId={id} />`): shows all pending proposals for that mission across all modules with a module-badge per card, so the user can review and approve without navigating to individual module pages.
- **Proposal inbox** — drop `<AiProposalInbox module="…" />` into any module page to render pending proposals inline with approve / freitext-reject buttons. Cards show the owning agent's name + avatar chip. Wired in `/todo`, `/calendar`, `/places`, `/drink`, `/news`, `/notes`. The mission-detail view also embeds a **cross-module inbox** (`<AiProposalInbox missionId={id} />`): shows all pending proposals for that mission across all modules with a module-badge per card, so the user can review and approve without navigating to individual module pages.
- **Reasoning loop** — the foreground Runner chains up to 5 planner calls per iteration. Read-only tools (`list_notes`, `get_task_stats`, etc.) execute inline as auto-policy, their outputs are fed back as synthetic `ResolvedInput`s for the next planner call. The loop exits when a propose-policy tool is staged (human must approve), the planner returns 0 steps, or the budget exhausts. This enables "read → reason → act" missions like *"list all notes and tag them"* in a single run. Code: `data/ai/missions/runner.ts` reasoning loop.
- **Missions** — long-lived autonomous work items at `/ai-missions` with concept + objective + linked inputs + cadence + **owning agent** (AgentPicker in the create flow). Both the foreground tick AND the server-side `mana-ai` service produce plans under the agent's identity; `data/ai/missions/server-iteration-staging.ts` translates server-source iterations into local Proposals on sync.
- **Input picker**`<MissionInputPicker>` sources candidates from the `input-index` registry (notes / kontext / goals / tasks / calendar). The Runner resolves via the parallel `input-resolvers` registry. Encrypted tables (notes, tasks, …) decrypt client-side only.
@ -272,7 +272,6 @@ Agents interact with the app through tools — each one either auto (executes si
| notes | `create_note`, `update_note`, `append_to_note`, `add_tag_to_note` | `list_notes` |
| places | `create_place`, `visit_place` | `get_places`, `get_current_location` |
| drink | `undo_drink` | `get_drink_progress`, `log_drink` |
| food | — | `nutrition_summary`, `log_meal` |
| news | `save_news_article` | — |
| news-research | `research_news` | — |
| articles | `save_article`, `archive_article`, `tag_article`, `add_article_highlight`, `import_articles_from_urls` (auto) | `list_articles` |

View file

@ -20,7 +20,7 @@ import { setSecurityHeaders } from '@mana/shared-utils/security-headers';
* - Glitchtip DSN client-side error reporting
*
* Per-app HTTP backends (todo-api, calendar-api, contacts-api, chat-api,
* storage-api, cards-api, mukke-api, food-api, picture-api, presi-api,
* storage-api, cards-api, mukke-api, picture-api, presi-api,
* quotes-api, clock-api, context-api) were removed in the pre-launch
* ghost-API cleanup every product module now talks to mana-sync directly.
*/
@ -152,18 +152,15 @@ const APP_SUBDOMAINS = new Set([
'cards',
'storage',
'presi',
'food',
'photos',
'music',
'picture',
'calc',
'citycorners',
'inventory',
'times',
'uload',
'memoro',
'questions',
'moodlit',
]);
export const handle: Handle = async ({ event, resolve }) => {

View file

@ -26,7 +26,6 @@ import {
MusicNotes,
Camera,
HardDrives,
ForkKnife,
Plant,
Presentation,
Package,
@ -34,7 +33,6 @@ import {
NumberCircleOne,
Binoculars,
ArrowsInCardinal,
SunHorizon,
Buildings,
DownloadSimple,
Calculator,
@ -76,7 +74,6 @@ import {
Flask,
Exam,
Globe,
CoatHanger,
NotePencil,
FilmStrip,
Hourglass,
@ -103,10 +100,10 @@ import {
// Knowledge: chat · kontext · cards · quiz · guides ·
// news · news-research · research-lab · articles ·
// library · writing · comic · presi
// Body & life: body · food · meditate · stretch · period ·
// Body & life: body · meditate · stretch · period ·
// dreams · firsts · lasts · habits · recipes
// Places & ev.: places · citycorners · events · who
// Creative: picture · music · photos · wardrobe · moodlit
// Places & ev.: places · events · who
// Creative: picture · music · photos
// Tools: memoro · uload · calc · plants · inventory ·
// storage · skilltree · questions
// Long-tail: quotes · automations · companion · wetter ·
@ -641,27 +638,6 @@ registerApp({
},
});
registerApp({
id: 'food',
name: 'Food',
color: '#22C55E',
icon: ForkKnife,
views: {
list: { load: () => import('$lib/modules/food/ListView.svelte') },
},
contextMenuActions: [
{
id: 'new-meal',
label: 'Neue Mahlzeit',
icon: Plus,
action: () =>
window.dispatchEvent(
new CustomEvent('mana:quick-action', { detail: { app: 'food', action: 'new' } })
),
},
],
});
registerApp({
id: 'plants',
name: 'Plants',
@ -728,27 +704,6 @@ registerApp({
},
});
registerApp({
id: 'moodlit',
name: 'Moodlit',
color: '#F97316',
icon: SunHorizon,
views: {
list: { load: () => import('$lib/modules/moodlit/ListView.svelte') },
},
});
registerApp({
id: 'citycorners',
name: 'CityCorners',
color: '#14B8A6',
icon: Buildings,
views: {
list: { load: () => import('$lib/modules/citycorners/ListView.svelte') },
detail: { load: () => import('$lib/modules/citycorners/views/DetailView.svelte') },
},
});
registerApp({
id: 'uload',
name: 'uLoad',
@ -1334,19 +1289,6 @@ registerApp({
},
});
registerApp({
id: 'wardrobe',
name: 'Kleiderschrank',
color: '#e11d48',
icon: CoatHanger,
views: {
// Detail routes (/wardrobe/garment/[id], /wardrobe/outfit/[id],
// /wardrobe/compose/[[outfitId]]) live as SvelteKit routes; the
// workbench only needs the list view for the tab-switcher root.
list: { load: () => import('$lib/modules/wardrobe/ListView.svelte') },
},
});
registerApp({
id: 'library',
name: 'Bibliothek',

View file

@ -68,13 +68,11 @@ export const APP_CATEGORY_MAP: Record<string, AppCategory> = {
meditate: 'life',
rituals: 'life',
journal: 'life',
food: 'life',
recipes: 'life',
plants: 'life',
finance: 'life',
contacts: 'life',
places: 'life',
citycorners: 'life',
news: 'life',
wetter: 'life',
inventory: 'life',
@ -100,7 +98,6 @@ export const APP_CATEGORY_MAP: Record<string, AppCategory> = {
picture: 'creative',
photos: 'creative',
presi: 'creative',
moodlit: 'creative',
cards: 'creative',
skilltree: 'creative',
guides: 'creative',
@ -109,7 +106,6 @@ export const APP_CATEGORY_MAP: Record<string, AppCategory> = {
library: 'creative',
playground: 'creative',
quiz: 'creative',
wardrobe: 'creative',
// System — settings, admin, meta
settings: 'system',

View file

@ -267,19 +267,6 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
'Dateinamen sind verschlüsselt, auch im Speicher nicht im Klartext sichtbar',
],
},
food: {
description: 'Mahlzeiten tracken mit AI-Unterstützung. Nährwerte werden automatisch erkannt.',
features: [
'Mahlzeiten per Text beschreiben',
'Automatische Nährwertanalyse durch AI',
'Tagesübersicht mit Kalorien & Makros',
'AI-Tools: Mahlzeiten loggen, Tages-Zusammenfassung',
],
tips: [
'Beschreibe Mahlzeiten natürlich: "2 Scheiben Vollkornbrot mit Käse und Tomate"',
'Im Chat: "Was habe ich heute gegessen?"',
],
},
plants: {
description:
'Pflanzen katalogisieren — Pflege-Notizen, Standort, Bewässerung und Bodentyp. Ideal für Hobbygärtner.',
@ -357,30 +344,6 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
],
tips: ['Definiere Kategorien nach Lebensbereichen für eine gute Übersicht'],
},
moodlit: {
description:
'Stimmungslicht und Ambient-Szenen für Fokus und Entspannung. Verwandle deinen Bildschirm in eine Lichtquelle.',
features: [
'Verschiedene Licht-Szenen & Farbverläufe',
'Timer-Funktion für zeitlich begrenzte Sessions',
'Farbwechsel & Animationen',
'Vollbild-Modus',
],
tips: ['Kombiniere Moodlit mit Meditate für eine immersive Meditationssession'],
},
citycorners: {
description:
'Interessante Ecken in deiner Stadt entdecken und festhalten. Ein persönlicher Stadtführer.',
features: [
'Orte mit Fotos & Beschreibung',
'Kategorien (Café, Street Art, Architektur, ...)',
'Standort & Adresse',
'Entdeckungs-Feed',
],
tips: [
'Halte Orte fest wenn du sie entdeckst — später erinnerst du dich nicht mehr an die Adresse',
],
},
uload: {
description:
'Quick-Upload — Dateien schnell hochladen und teilbare Links erstellen. Ideal zum schnellen Teilen.',
@ -980,23 +943,6 @@ export const MODULE_HELP: Record<string, ModuleHelp> = {
'Ab ~8 Panels pro Story wird Character-Konsistenz spürbar schwerer (gpt-image-2-Limit).',
],
},
wardrobe: {
description:
'Dein digitaler Kleiderschrank — fotografiere Kleidungsstücke und Accessoires, komponiere Outfits, und probiere sie mit KI an dir selbst an. Pro Space ein eigener Schrank: was im Family-Space liegt, taucht im Brand-Space nicht auf.',
features: [
'Kleidung nach Kategorien (Oberteile, Hosen, Kleider, Jacken, Schuhe, Accessoires …)',
'Outfits aus mehreren Stücken komponieren und als Set anprobieren',
'Solo-Try-On pro Einzelstück — Accessoire-Modus (Brille, Schmuck, Hut) rendert nur das Gesicht und spart Credits',
'Referenzbilder aus „Meine Bilder" (Gesicht + optional Ganzkörper) werden automatisch genutzt',
'MCP-Tools: listGarments / listOutfits / createOutfit / tryOn für Agents',
],
tips: [
'Aktive Kategorie oben bestimmt den Typ für neue Uploads — erst die Kategorie wählen, dann die Datei droppen.',
'Die Upload-Zone oben akzeptiert Drag-&-Drop direkt aus dem Finder.',
'Frontal-Fotos mit hellem Hintergrund liefern die besten Try-On-Ergebnisse.',
'Ohne Gesichtsbild kannst du kein Try-On starten — der Banner oben hilft beim Upload in einem Schritt.',
],
},
'research-lab': {
description:
'Web-Research-Anbieter Seite-an-Seite vergleichen: gleiche Query an bis zu fünf Provider parallel, Antworten + Latenz + Kosten nebeneinander. Alle Runs werden serverseitig persistiert für spätere Auswertung.',

View file

@ -88,22 +88,6 @@ export const GOAL_TEMPLATES: GoalTemplate[] = [
metric: { source: 'event_count', eventType: 'TaskCompleted' },
target: { value: 5, period: 'day', comparison: 'gte' },
},
{
id: 'tpl-meals-daily',
title: 'Alle Mahlzeiten tracken',
description: 'Mindestens 3 Mahlzeiten pro Tag erfassen',
moduleId: 'food',
metric: { source: 'event_count', eventType: 'MealLogged' },
target: { value: 3, period: 'day', comparison: 'gte' },
},
{
id: 'tpl-calories-daily',
title: 'Kalorien-Ziel einhalten',
description: 'Maximal 2000 kcal pro Tag',
moduleId: 'food',
metric: { source: 'event_sum', eventType: 'MealLogged', sumField: 'calories' },
target: { value: 2000, period: 'day', comparison: 'lte' },
},
{
id: 'tpl-places-weekly',
title: 'Neue Orte entdecken',

View file

@ -206,10 +206,6 @@ export async function extractAllPatterns(): Promise<void> {
// Calendar patterns
extractDayOfWeekPattern('CalendarEventCreated', 'Termine erstellt', 'calendar'),
// Food patterns
extractTimePreference('MealLogged', 'Mahlzeiten geloggt', 'food'),
extractFrequencyPattern('MealLogged', 'Mahlzeiten', 'food'),
// Places patterns
extractDayOfWeekPattern('PlaceVisited', 'Orte besucht', 'places'),
]);

View file

@ -106,48 +106,10 @@ export const overdueTasksRule: PulseRule = {
},
};
export const mealReminderRule: PulseRule = {
id: 'meal-reminder',
name: 'Mahlzeit-Erinnerung',
trigger: { kind: 'schedule', hours: [12, 19] },
check(ctx) {
const { meals, calories } = ctx.day.nutrition;
// Lunch check at 12
if (ctx.hour === 12 && meals < 1) {
return {
id: `meal-lunch-${ctx.day.date}`,
type: 'meal_reminder',
title: 'Mittagessen tracken',
body: 'Noch keine Mahlzeit heute erfasst.',
priority: 'low',
actionLabel: 'Mahlzeit loggen',
actionRoute: '/food',
};
}
// Dinner check at 19
if (ctx.hour === 19 && meals < 2) {
return {
id: `meal-dinner-${ctx.day.date}`,
type: 'meal_reminder',
title: 'Abendessen tracken',
body: `Erst ${meals} Mahlzeit(en) heute (${calories.actual} kcal).`,
priority: 'low',
actionLabel: 'Mahlzeit loggen',
actionRoute: '/food',
};
}
return null;
},
};
/** All built-in rules */
export const DEFAULT_RULES: PulseRule[] = [
waterReminderRule,
streakWarningRule,
morningSummaryRule,
overdueTasksRule,
mealReminderRule,
];

View file

@ -26,7 +26,6 @@ import PresiDecksWidget from './widgets/PresiDecksWidget.svelte';
// Phase 4: Unified app widgets (direct Dexie queries, internal routing)
import RecentContactsWidget from '$lib/modules/core/widgets/RecentContactsWidget.svelte';
import ActiveTimerWidget from '$lib/modules/core/widgets/ActiveTimerWidget.svelte';
import NutritionProgressWidget from '$lib/modules/core/widgets/NutritionProgressWidget.svelte';
import PlantWateringWidget from '$lib/modules/core/widgets/PlantWateringWidget.svelte';
import PeriodWidget from '$lib/modules/core/widgets/PeriodWidget.svelte';
import NewsUnreadWidget from '$lib/modules/news/widgets/NewsUnreadWidget.svelte';
@ -55,7 +54,6 @@ export const widgetComponents: Record<WidgetType, Component> = {
'music-library': MusicLibraryWidget,
'presi-decks': PresiDecksWidget,
'active-timer': ActiveTimerWidget,
'nutrition-progress': NutritionProgressWidget,
'plant-watering': PlantWateringWidget,
'day-timeline': DayTimelineWidget,
'activity-feed': ActivityFeedWidget,

View file

@ -498,45 +498,6 @@ export const appConfigs: Record<string, AppConfig> = {
dashboardRoute: '/',
website: 'https://storage.mana.how',
},
moodlit: {
name: 'moodlit',
displayName: 'Moodlit',
tagline: 'Ambient Lighting & Moods',
description:
'Erstelle beruhigende Lichtstimmungen mit animierten Farbverläufen für entspannte Atmosphäre.',
logoEmoji: '🌈',
primaryColor: '#8B5CF6',
accentColor: '#A78BFA',
features: [
{
icon: '🌈',
title: 'Farbverläufe',
description: 'Animierte Ambient-Beleuchtung',
color: '#8B5CF6',
},
{
icon: '🎨',
title: 'Themes',
description: 'Vordefinierte Stimmungen',
color: '#EC4899',
},
{
icon: '✨',
title: 'Animationen',
description: 'Sanfte, beruhigende Bewegungen',
color: '#F59E0B',
},
{
icon: '🌙',
title: 'Nachtmodus',
description: 'Perfekt zum Einschlafen',
color: '#6366F1',
},
],
dashboardRoute: '/',
website: 'https://moodlit.mana.how',
},
};
/**
@ -631,6 +592,6 @@ export function getAppsByCategory(): {
appConfigs.contacts,
appConfigs.finance,
],
utility: [appConfigs.clock, appConfigs.quotes, appConfigs.storage, appConfigs.moodlit],
utility: [appConfigs.clock, appConfigs.quotes, appConfigs.storage],
};
}

View file

@ -90,7 +90,6 @@ import type {
} from '../../modules/broadcasts/types';
import type { LocalArticle, LocalHighlight } from '../../modules/articles/types';
import type { LocalMeImage } from '../../modules/profile/types';
import type { LocalWardrobeGarment, LocalWardrobeOutfit } from '../../modules/wardrobe/types';
import type {
LocalDraft,
LocalDraftVersion,
@ -194,29 +193,6 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
periods: { enabled: true, fields: ['notes'] },
periodDayLogs: { enabled: true, fields: ['notes', 'mood'] },
// ─── Food ────────────────────────────────────────────
// LocalMeal user-typed / AI-generated content → encrypted:
// - description, portionSize: free-text, same sensitivity tier
// - foods: AI-identified food items (array of {name, quantity,
// calories}). aes.ts JSON-stringifies before wrap, so an array
// value works the same as a string. The food names are user
// content ("Currywurst Pommes mittel") and deserve the same
// protection as `description`.
// Plaintext (intentional):
// - mealType / inputType / date / createdAt: structural, used for
// filtering and the daily-summary aggregations + calorie-progress
// widget. Encrypting would force decrypt-then-aggregate on every
// liveQuery refresh.
// - nutrition (object of numbers): same — calorie totals are summed
// in pure $derived helpers; encrypting them would defeat the
// local-first reactive layer.
// - photoMediaId / photoUrl / photoThumbnailUrl: opaque pointers to
// mana-media; the URL alone is not PII (anyone with the URL
// already has the bytes), and CAS-deduped media IDs leak no user
// content. Same rationale plants uses for plantPhotos.
// - confidence (float 0-1): pure metadata about the AI run.
meals: { enabled: true, fields: ['description', 'portionSize', 'foods'] },
// ─── Plants ──────────────────────────────────────────────
// `name` is NOT in the schema index for plants (only isActive +
// healthStatus), so encrypting it is safe. LocalPlant uses
@ -582,33 +558,6 @@ export const ENCRYPTION_REGISTRY: Record<string, EncryptionConfig> = {
// lives in MinIO behind owner-RLS, not in Dexie.
meImages: entry<LocalMeImage>(['label', 'tags']),
// ─── Wardrobe (garments + outfits) ───────────────────────
// docs/plans/wardrobe-module.md M1. Two space-scoped tables.
//
// Garments: user-typed clothing metadata is the sensitive surface —
// brand names leak purchasing patterns, notes leak preferences,
// tags leak categorization intent. `category` stays plaintext
// because it's the Category-Tabs filter index; `mediaIds`, dates,
// and counters are structural.
wardrobeGarments: entry<LocalWardrobeGarment>([
'name',
'brand',
'color',
'size',
'material',
'tags',
'notes',
]),
// Outfits: name + description + tags are user-authored. Occasion
// stays plaintext (closed enum, small cardinality — useful to
// filter on without decrypt). `garmentIds` is an array of FKs,
// plaintext by the standard "IDs are plaintext" rule. `lastTryOn`
// is a structural pointer + prompt; the prompt itself isn't
// secret (OpenAI already saw it) but lands inside the encrypted
// JSON-stringified blob via the `season` array-path anyway — keep
// it plaintext and revisit if prompts later carry personal data.
wardrobeOutfits: entry<LocalWardrobeOutfit>(['name', 'description', 'tags']),
// ─── Comic (stories + inline panel metadata) ─────────────
// docs/plans/comic-module.md M1. Single space-scoped table.
//

View file

@ -141,45 +141,6 @@ export interface DrinkEntryUndonePayload {
export type DrinkEventType = 'DrinkLogged' | 'DrinkEntryDeleted' | 'DrinkEntryUndone';
// ── Food ────────────────────────────────────────
export interface MealLoggedPayload {
mealId: string;
mealType: string;
inputType: string;
description: string;
calories?: number;
protein?: number;
date: string;
}
export interface MealFromPhotoLoggedPayload {
mealId: string;
mealType: string;
photoMediaId: string;
confidence: number;
calories?: number;
}
export interface MealDeletedPayload {
mealId: string;
mealType: string;
}
export interface NutritionGoalSetPayload {
goalId: string;
dailyCalories: number;
dailyProtein?: number;
dailyCarbs?: number;
dailyFat?: number;
}
export type FoodEventType =
| 'MealLogged'
| 'MealFromPhotoLogged'
| 'MealDeleted'
| 'NutritionGoalSet';
// ── Places ──────────────────────────────────────────
export interface PlaceCreatedPayload {
@ -661,7 +622,6 @@ export type ManaEventType =
| TodoEventType
| CalendarEventType
| DrinkEventType
| FoodEventType
| PlacesEventType
| HabitsEventType
| JournalEventType
@ -717,11 +677,6 @@ export type ManaEvent =
| DomainEvent<'DrinkLogged', DrinkLoggedPayload>
| DomainEvent<'DrinkEntryDeleted', DrinkEntryDeletedPayload>
| DomainEvent<'DrinkEntryUndone', DrinkEntryUndonePayload>
// Food
| DomainEvent<'MealLogged', MealLoggedPayload>
| DomainEvent<'MealFromPhotoLogged', MealFromPhotoLoggedPayload>
| DomainEvent<'MealDeleted', MealDeletedPayload>
| DomainEvent<'NutritionGoalSet', NutritionGoalSetPayload>
// Places
| DomainEvent<'PlaceCreated', PlaceCreatedPayload>
| DomainEvent<'PlaceDeleted', PlaceDeletedPayload>

View file

@ -6,10 +6,13 @@
* the matching index in `database.ts` (or vice versa), one of these tests
* fails loudly instead of letting sync silently drop the table.
*
* The "snapshot" tests pin the *exact* registry shape that existed before
* the refactor. Any intentional change to a module's tables / sync names
* should update both the module config AND the corresponding entry below
* in the same commit this makes such changes visible in code review.
* The "snapshot" tests pin the *exact* registry shape that exists today.
* Any intentional change to a module's tables / sync names should update
* both the module config AND the corresponding entry below in the same
* commit this makes such changes visible in code review.
*
* Last full snapshot refresh: 2026-05-18 (food + wardrobe module retirement;
* citycorners + cards modules already retired before).
*/
import 'fake-indexeddb/auto';
@ -35,17 +38,73 @@ import {
import { db } from './database';
// ─── Internal Dexie tables that are intentionally NOT in SYNC_APP_MAP ───
// These hold local-only state (sync metadata, retry queues, activity log)
// that must never leave the device.
// These hold local-only state (sync metadata, retry queues, activity log,
// AI debug capture, BYOK key material, …) that must never leave the device.
const INTERNAL_TABLES = new Set([
'_pendingChanges',
'_syncMeta',
'_eventsTombstones',
'_activity',
'_events',
'_memory',
'_streakState',
'_aiDebugLog',
'_byokKeys',
'_clientIdentity',
'_nudgeOutcomes',
'_serverIterationExecutions',
// Local-only AI Workbench staging; approvals run the underlying tool
// which writes via its module's sync path — proposals themselves never
// leave the device.
'pendingProposals',
// Local-only news feed cache.
'newsCachedFeed',
]);
// ─── Dexie tables that survive in the schema for backwards-compat with
// existing user databases, but whose owning module has been retired and
// is no longer expected to register them. These rows are stranded until
// a future Dexie version() call drops them explicitly. Tracked here so
// the "every Dexie table is registered" guard doesn't break on legacy
// schema history.
const LEGACY_TABLES = new Set([
// Cards → wordeck.com (2026-05-17 rebrand)
'cardDecks',
'cards',
'deckTags',
'cardReviews',
'cardStudyBlocks',
// CityCorners → seepuls.mana.how (2026-05 retired)
'cities',
'ccLocations',
'ccFavorites',
'ccLocationTags',
// Moodlit → mood module split (legacy tables still in Dexie history)
'moods',
'sequences',
'moodTags',
// Companion module — surfaces still live but tables predate the
// per-module registry refactor; tracked in the agents/missions registry
// today.
'companionConversations',
'companionGoals',
'companionMessages',
// Rituals — local-only state for the AI Workbench ritual runner; not
// yet promoted into a module config but writes happen via the workbench
// pathway.
'rituals',
'ritualSteps',
'ritualLogs',
// Wishes — module surface exists, registry adoption pending.
'wishesItems',
'wishesLists',
'wishesPriceChecks',
// Who — module surface exists, registry adoption pending.
'whoGames',
'whoMessages',
// User-level legacy table from the v40 tag-preset migration; lives
// outside the module-registry by design (cross-module shared shape).
'userTagPresets',
]);
describe('module-registry — structural invariants', () => {
@ -114,10 +173,11 @@ describe('module-registry — Dexie schema alignment', () => {
}
});
it('every Dexie table is either internal or registered with an appId', () => {
it('every Dexie table is either internal, legacy, or registered with an appId', () => {
const registered = new Set(Object.keys(TABLE_TO_APP));
for (const t of db.tables) {
if (INTERNAL_TABLES.has(t.name)) continue;
if (LEGACY_TABLES.has(t.name)) continue;
expect(
registered.has(t.name),
`Dexie table "${t.name}" is not registered in any module.config.ts — sync will silently skip it`
@ -128,29 +188,29 @@ describe('module-registry — Dexie schema alignment', () => {
// ─── Snapshot of the registry shape ───────────────────────────────
//
// This is the exact set of (appId → tables) and (unified → sync) mappings
// that the legacy hardcoded blocks in database.ts had pre-refactor. If you
// intentionally change a module's sync surface, update the matching entry
// here in the same commit so the change is reviewable.
// Exact (appId → tables) and (unified → sync) shape of the current registry.
// If you intentionally change a module's sync surface, update the matching
// entry here in the same commit so the change is reviewable.
describe('module-registry — pre-refactor snapshot', () => {
it('SYNC_APP_MAP matches the legacy hardcoded shape', () => {
describe('module-registry — snapshot', () => {
it('SYNC_APP_MAP matches the current registry shape', () => {
expect(SYNC_APP_MAP).toEqual({
mana: ['userSettings', 'dashboardConfigs', 'automations'],
mana: ['userSettings', 'dashboardConfigs', 'workbenchScenes', 'automations'],
tags: ['globalTags', 'tagGroups'],
links: ['manaLinks'],
timeblocks: ['timeBlocks', 'timeBlockTags'],
todo: ['tasks', 'todoProjects', 'taskLabels', 'reminders', 'boardViews'],
calendar: ['calendars', 'events', 'eventTags'],
contacts: ['contacts', 'contactTags'],
chat: ['conversations', 'messages', 'chatTemplates', 'conversationTags'],
picture: ['images', 'boards', 'boardItems', 'imageTags'],
cards: ['cardDecks', 'cards', 'deckTags'],
quotes: ['quotesFavorites', 'quotesLists', 'quotesListTags'],
quotes: ['quotesFavorites', 'quotesLists', 'quotesListTags', 'customQuotes'],
music: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers', 'songTags'],
storage: ['files', 'storageFolders', 'fileTags'],
presi: ['presiDecks', 'slides', 'presiDeckTags'],
inventory: ['invCollections', 'invItems', 'invLocations', 'invCategories', 'invItemTags'],
photos: ['albums', 'albumItems', 'photoFavorites', 'photoMediaTags'],
skilltree: ['skills', 'activities', 'achievements', 'skillTags'],
citycorners: ['cities', 'ccLocations', 'ccFavorites', 'ccLocationTags'],
times: [
'timeClients',
'timeProjects',
@ -163,33 +223,77 @@ describe('module-registry — pre-refactor snapshot', () => {
'entryTags',
],
questions: ['qCollections', 'questions', 'answers', 'questionTags'],
food: ['meals', 'goals', 'foodFavorites', 'mealTags'],
plants: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs', 'plantTags'],
uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'],
calc: ['calculations', 'savedFormulas'],
moodlit: ['moods', 'sequences', 'moodTags'],
memoro: ['memos', 'memories', 'memoTags', 'memoroSpaces', 'spaceMembers', 'memoSpaces'],
guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs', 'guideTags'],
habits: ['habits', 'habitLogs'],
notes: ['notes', 'noteTags'],
journal: ['journalEntries'],
dreams: ['dreams', 'dreamSymbols', 'dreamTags'],
period: ['periods', 'periodDayLogs', 'periodSymptoms'],
events: ['socialEvents', 'eventGuests', 'eventInvitations', 'eventItems'],
finance: ['transactions', 'financeCategories', 'budgets'],
places: ['places', 'locationLogs', 'placeTags'],
tags: ['globalTags', 'tagGroups'],
links: ['manaLinks'],
timeblocks: ['timeBlocks', 'timeBlockTags'],
playground: ['playgroundSnippets', 'playgroundConversations', 'playgroundMessages'],
news: ['newsArticles', 'newsCategories', 'newsPreferences', 'newsReactions'],
body: [
'bodyExercises',
'bodyRoutines',
'bodyWorkouts',
'bodySets',
'bodyMeasurements',
'bodyChecks',
'bodyPhases',
],
firsts: ['firsts'],
lasts: ['lasts', 'lastsCooldown'],
drink: ['drinkEntries', 'drinkPresets'],
recipes: ['recipes'],
stretch: [
'stretchExercises',
'stretchRoutines',
'stretchSessions',
'stretchAssessments',
'stretchReminders',
],
mail: ['mailDrafts'],
meditate: ['meditatePresets', 'meditateSessions', 'meditateSettings'],
sleep: ['sleepEntries', 'sleepHygieneLogs', 'sleepHygieneChecks', 'sleepSettings'],
mood: ['moodEntries', 'moodSettings'],
quiz: ['quizzes', 'quizQuestions', 'quizAttempts'],
profile: ['userContext', 'meImages'],
library: ['libraryEntries'],
articles: [
'articles',
'articleHighlights',
'articleTags',
'articleImportJobs',
'articleImportItems',
'articleExtractPickup',
],
invoices: ['invoices', 'invoiceClients', 'invoiceSettings'],
broadcasts: ['broadcastCampaigns', 'broadcastTemplates', 'broadcastSettings'],
wetter: ['wetterLocations', 'wetterSettings'],
website: ['websites', 'websitePages', 'websiteBlocks'],
writing: ['writingDrafts', 'writingDraftVersions', 'writingGenerations', 'writingStyles'],
comic: ['comicStories', 'comicCharacters'],
augur: ['augurEntries'],
forms: ['forms', 'formResponses'],
ai: ['aiMissions', 'agents', 'agentKontextDocs'],
});
});
it('TABLE_TO_SYNC_NAME matches the legacy hardcoded shape', () => {
it('TABLE_TO_SYNC_NAME matches the current registry shape', () => {
expect(TABLE_TO_SYNC_NAME).toEqual({
globalTags: 'tags',
manaLinks: 'links',
todoProjects: 'projects',
chatTemplates: 'templates',
cardDecks: 'decks',
quotesFavorites: 'favorites',
quotesLists: 'lists',
customQuotes: 'custom-quotes',
mukkePlaylists: 'playlists',
mukkeProjects: 'projects',
storageFolders: 'folders',
@ -200,8 +304,6 @@ describe('module-registry — pre-refactor snapshot', () => {
invCategories: 'categories',
photoFavorites: 'favorites',
photoMediaTags: 'photoTags',
ccLocations: 'locations',
ccFavorites: 'favorites',
timeClients: 'clients',
timeProjects: 'projects',
timeTemplates: 'templates',
@ -210,18 +312,27 @@ describe('module-registry — pre-refactor snapshot', () => {
timeCountdownTimers: 'countdownTimers',
timeWorldClocks: 'worldClocks',
qCollections: 'collections',
foodFavorites: 'favorites',
memoroSpaces: 'spaces',
uloadTags: 'tags',
uloadFolders: 'folders',
memoroSpaces: 'spaces',
guideCollections: 'collections',
financeCategories: 'categories',
socialEvents: 'events',
globalTags: 'tags',
// `tagGroups` is intentionally absent — it has no rename in the registry
// (the legacy hardcoded block had a redundant tagGroups→tagGroups entry
// which was a no-op; toSyncName() returns the same value either way).
manaLinks: 'links',
financeCategories: 'categories',
playgroundSnippets: 'snippets',
playgroundConversations: 'conversations',
playgroundMessages: 'messages',
newsArticles: 'articles',
newsCategories: 'categories',
newsPreferences: 'preferences',
newsReactions: 'reactions',
quizQuestions: 'questions',
quizAttempts: 'attempts',
articleHighlights: 'highlights',
articleImportJobs: 'importJobs',
articleImportItems: 'importItems',
articleExtractPickup: 'extractPickup',
wetterLocations: 'locations',
wetterSettings: 'settings',
});
});
});

View file

@ -63,14 +63,11 @@ import { presiModuleConfig } from '$lib/modules/presi/module.config';
import { inventoryModuleConfig } from '$lib/modules/inventory/module.config';
import { photosModuleConfig } from '$lib/modules/photos/module.config';
import { skilltreeModuleConfig } from '$lib/modules/skilltree/module.config';
import { citycornersModuleConfig } from '$lib/modules/citycorners/module.config';
import { timesModuleConfig } from '$lib/modules/times/module.config';
import { questionsModuleConfig } from '$lib/modules/questions/module.config';
import { foodModuleConfig } from '$lib/modules/food/module.config';
import { plantsModuleConfig } from '$lib/modules/plants/module.config';
import { uloadModuleConfig } from '$lib/modules/uload/module.config';
import { calcModuleConfig } from '$lib/modules/calc/module.config';
import { moodlitModuleConfig } from '$lib/modules/moodlit/module.config';
import { memoroModuleConfig } from '$lib/modules/memoro/module.config';
import { guidesModuleConfig } from '$lib/modules/guides/module.config';
import { habitsModuleConfig } from '$lib/modules/habits/module.config';
@ -101,7 +98,6 @@ import { invoicesModuleConfig } from '$lib/modules/invoices/module.config';
import { broadcastModuleConfig } from '$lib/modules/broadcasts/module.config';
import { wetterModuleConfig } from '$lib/modules/wetter/module.config';
import { websiteModuleConfig } from '$lib/modules/website/module.config';
import { wardrobeModuleConfig } from '$lib/modules/wardrobe/module.config';
import { writingModuleConfig } from '$lib/modules/writing/module.config';
import { comicModuleConfig } from '$lib/modules/comic/module.config';
import { augurModuleConfig } from '$lib/modules/augur/module.config';
@ -125,14 +121,11 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [
inventoryModuleConfig,
photosModuleConfig,
skilltreeModuleConfig,
citycornersModuleConfig,
timesModuleConfig,
questionsModuleConfig,
foodModuleConfig,
plantsModuleConfig,
uloadModuleConfig,
calcModuleConfig,
moodlitModuleConfig,
memoroModuleConfig,
guidesModuleConfig,
habitsModuleConfig,
@ -163,7 +156,6 @@ export const MODULE_CONFIGS: readonly ModuleConfig[] = [
broadcastModuleConfig,
wetterModuleConfig,
websiteModuleConfig,
wardrobeModuleConfig,
writingModuleConfig,
comicModuleConfig,
augurModuleConfig,

View file

@ -139,18 +139,6 @@ const TABLES: TableConfig[] = [
return recipesStore.setVisibility(id, next);
},
},
{
module: 'wardrobe',
collection: 'wardrobeOutfits',
moduleLabel: 'Wardrobe (Outfits)',
encrypted: true,
title: (r) => asString(r.name),
href: () => '/wardrobe',
setVisibility: async (id, next) => {
const { wardrobeOutfitsStore } = await import('$lib/modules/wardrobe/stores/outfits.svelte');
return wardrobeOutfitsStore.setVisibility(id, next);
},
},
{
module: 'comic',
collection: 'comicStories',

View file

@ -75,14 +75,6 @@ export function generateContextDocument(
lines.push(`- Kaffee: ${day.drinks.coffee.count}x (${day.drinks.coffee.ml}ml)`);
}
// Nutrition
lines.push(
`- Ernaehrung: ${day.nutrition.meals} Mahlzeiten, ${day.nutrition.calories.actual} / ${day.nutrition.calories.goal} kcal (${day.nutrition.calories.percent}%)`
);
if (day.nutrition.protein) {
lines.push(` - Protein: ${day.nutrition.protein.actual}g / ${day.nutrition.protein.goal}g`);
}
// Places
if (day.places.visitedToday > 0) {
lines.push(`- ${day.places.visitedToday} Orte besucht`);

View file

@ -61,18 +61,6 @@ const METRICS: MetricDef[] = [
label: 'Kaffee (Tassen)',
extract: (days) => countByType(days, 'DrinkLogged', (p) => p.drinkType === 'coffee'),
},
{
id: 'food:calories',
module: 'food',
label: 'Kalorien',
extract: (days) => sumByTypeField(days, 'MealLogged', 'calories'),
},
{
id: 'food:meals',
module: 'food',
label: 'Mahlzeiten',
extract: (days) => countByType(days, 'MealLogged'),
},
{
id: 'calendar:events',
module: 'calendar',

View file

@ -14,12 +14,10 @@ import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '../database';
import { decryptRecords } from '../crypto';
import { DEFAULT_DAILY_GOAL_ML } from '$lib/modules/drink/types';
import { DEFAULT_DAILY_VALUES } from '$lib/modules/food/constants';
import { trackingStore } from '$lib/modules/places/stores/tracking.svelte';
import type { LocalTask } from '$lib/modules/todo/types';
import type { LocalEvent } from '$lib/modules/calendar/types';
import type { LocalDrinkEntry } from '$lib/modules/drink/types';
import type { LocalMeal, LocalGoal as NutriGoal } from '$lib/modules/food/types';
import type { LocalPlace } from '$lib/modules/places/types';
import type { LocalTimeBlock } from '../time-blocks/types';
import type { DaySnapshot, TaskSummary, EventSummary } from './types';
@ -38,11 +36,6 @@ function emptySnapshot(date: string): DaySnapshot {
coffee: { ml: 0, count: 0 },
total: { ml: 0, count: 0 },
},
nutrition: {
meals: 0,
calories: { actual: 0, goal: DEFAULT_DAILY_VALUES.calories, percent: 0 },
protein: null,
},
places: { visitedToday: 0, tracking: false },
};
}
@ -53,8 +46,8 @@ async function buildSnapshot(): Promise<DaySnapshot> {
const todayStart = `${today}T00:00:00`;
const todayEnd = `${today}T23:59:59`;
// ── Parallel queries — all 5 modules at once ────
const [allTasks, blocks, allDrinks, allMeals, foodGoals, allPlaces] = await Promise.all([
// ── Parallel queries — all modules at once ──────
const [allTasks, blocks, allDrinks, allPlaces] = await Promise.all([
db.table<LocalTask>('tasks').toArray(),
db
.table<LocalTimeBlock>('timeBlocks')
@ -62,8 +55,6 @@ async function buildSnapshot(): Promise<DaySnapshot> {
.between(todayStart, todayEnd + '\uffff')
.toArray(),
db.table<LocalDrinkEntry>('drinkEntries').toArray(),
db.table<LocalMeal>('meals').toArray(),
db.table<NutriGoal>('goals').toArray(),
db.table<LocalPlace>('places').toArray(),
]);
@ -73,13 +64,11 @@ async function buildSnapshot(): Promise<DaySnapshot> {
(b) => !b.deletedAt && b.type === 'event' && b.sourceModule === 'calendar'
);
const todayDrinks = allDrinks.filter((d) => !d.deletedAt && d.date === today);
const todayMeals = allMeals.filter((m) => !m.deletedAt && m.date === today);
const [decryptedTasks, decryptedBlocks, decryptedDrinks, decryptedMeals] = await Promise.all([
const [decryptedTasks, decryptedBlocks, decryptedDrinks] = await Promise.all([
decryptRecords<LocalTask>('tasks', activeTasks),
decryptRecords<LocalTimeBlock>('timeBlocks', eventBlocks),
decryptRecords<LocalDrinkEntry>('drinkEntries', todayDrinks),
decryptRecords<LocalMeal>('meals', todayMeals),
]);
// ── Tasks ───────────────────────────────────────
@ -126,20 +115,6 @@ async function buildSnapshot(): Promise<DaySnapshot> {
}
}
// ── Nutrition ───────────────────────────────────
let totalCalories = 0;
let totalProtein = 0;
for (const m of decryptedMeals) {
const n = m.nutrition as { calories?: number; protein?: number } | null;
if (n) {
totalCalories += n.calories ?? 0;
totalProtein += n.protein ?? 0;
}
}
const activeGoal = foodGoals.find((g) => !g.deletedAt);
const calorieGoal = activeGoal?.dailyCalories ?? DEFAULT_DAILY_VALUES.calories;
const proteinGoal = activeGoal?.dailyProtein;
// ── Places ──────────────────────────────────────
const visitedToday = allPlaces.filter(
(p) => !p.deletedAt && p.lastVisitedAt && (p.lastVisitedAt as string).startsWith(today)
@ -167,15 +142,6 @@ async function buildSnapshot(): Promise<DaySnapshot> {
coffee: { ml: coffeeMl, count: coffeeCount },
total: { ml: totalMl, count: totalCount },
},
nutrition: {
meals: decryptedMeals.length,
calories: {
actual: Math.round(totalCalories),
goal: calorieGoal,
percent: Math.min(Math.round((totalCalories / calorieGoal) * 100), 100),
},
protein: proteinGoal ? { actual: Math.round(totalProtein), goal: proteinGoal } : null,
},
places: {
visitedToday,
tracking: trackingStore.isTracking,

View file

@ -131,7 +131,7 @@ describe('Streak Tracker', () => {
expect(state.currentStreak).toBe(1);
});
it('tracks multiple streak types independently', async () => {
it('tracks task streak on task completed event', async () => {
startStreakTracker();
eventBus.emit({
@ -147,30 +147,9 @@ describe('Streak Tracker', () => {
actor: USER_ACTOR,
},
});
eventBus.emit({
type: 'MealLogged',
payload: {
mealId: '1',
mealType: 'lunch',
inputType: 'text',
description: 'Pasta',
date: todayStr(),
},
meta: {
id: '2',
timestamp: new Date().toISOString(),
appId: 'food',
collection: 'meals',
recordId: '2',
userId: 'u1',
actor: USER_ACTOR,
},
});
await flush();
const tasks = await db.table(TABLE).get('streak-tasks-completed');
const meals = await db.table(TABLE).get('streak-meals-logged');
expect(tasks?.currentStreak).toBe(1);
expect(meals?.currentStreak).toBe(1);
});
});

View file

@ -72,12 +72,6 @@ const STREAK_DEFS: StreakDef[] = [
label: 'Tasks erledigt',
triggerEvents: ['TaskCompleted'],
},
{
id: 'streak-meals-logged',
moduleId: 'food',
label: 'Mahlzeiten getrackt',
triggerEvents: ['MealLogged', 'MealFromPhotoLogged'],
},
{
id: 'streak-workout',
moduleId: 'body',

View file

@ -47,12 +47,6 @@ export interface DaySnapshot {
total: { ml: number; count: number };
};
nutrition: {
meals: number;
calories: { actual: number; goal: number; percent: number };
protein: { actual: number; goal: number } | null;
};
places: {
visitedToday: number;
tracking: boolean;

View file

@ -120,7 +120,7 @@ export function scopedForModule<T, PK>(
* space as the second step.
*
* const recent = await scopedAnd(
* db.table<LocalMeal>('meals').where('date').aboveOrEqual(since),
* db.table<LocalTask>('tasks').where('updatedAt').aboveOrEqual(since),
* ).toArray();
*
* The wrapper accepts any Collection so the caller can freely build

View file

@ -18,7 +18,6 @@ import { HABITS_GUEST_SEED } from '$lib/modules/habits/collections';
import { BODY_GUEST_SEED } from '$lib/modules/body/collections';
import { JOURNAL_GUEST_SEED } from '$lib/modules/journal/collections';
import { DREAMS_GUEST_SEED } from '$lib/modules/dreams/collections';
import { MOODLIT_GUEST_SEED } from '$lib/modules/moodlit/collections';
import { CONTACTS_GUEST_SEED } from '$lib/modules/contacts/collections';
import { CALENDAR_GUEST_SEED } from '$lib/modules/calendar/collections';
import { CHAT_GUEST_SEED } from '$lib/modules/chat/collections';
@ -58,7 +57,6 @@ register(HABITS_GUEST_SEED);
register(BODY_GUEST_SEED);
register(JOURNAL_GUEST_SEED);
register(DREAMS_GUEST_SEED);
register(MOODLIT_GUEST_SEED);
register(CONTACTS_GUEST_SEED);
register(CALENDAR_GUEST_SEED);
register(CHAT_GUEST_SEED);

View file

@ -46,7 +46,6 @@ export type TimeBlockSourceModule =
| 'places'
| 'cards'
| 'music'
| 'moodlit'
| 'presi';
// ─── Local Record Types (Dexie) ──────────────────────────

View file

@ -7,7 +7,6 @@ import { registerTools } from './registry';
import { todoTools } from '$lib/modules/todo/tools';
import { calendarTools } from '$lib/modules/calendar/tools';
import { drinkTools } from '$lib/modules/drink/tools';
import { foodTools } from '$lib/modules/food/tools';
import { placesTools } from '$lib/modules/places/tools';
import { habitsTools } from '$lib/modules/habits/tools';
import { journalTools } from '$lib/modules/journal/tools';
@ -58,7 +57,6 @@ export function initTools(): void {
registerTools(todoTools);
registerTools(calendarTools);
registerTools(drinkTools);
registerTools(foodTools);
registerTools(placesTools);
registerTools(habitsTools);
registerTools(journalTools);

View file

@ -14,7 +14,7 @@
* places just happens to be the first consumer.
*/
export type PlaceCategory = 'home' | 'work' | 'food' | 'shopping' | 'transit' | 'leisure' | 'other';
export type PlaceCategory = 'home' | 'work' | 'shopping' | 'transit' | 'leisure' | 'other';
/**
* Where to send geocoding requests:

View file

@ -6,8 +6,6 @@
"maerchenzauber_long_desc": "Erstelle personalisierte Kindergeschichten mit KI-generierten Illustrationen.",
"cards_desc": "KI Lernkarten",
"cards_long_desc": "Erstelle und lerne mit smarten Lernkarten und KI-gestützter Wiederholung.",
"moodlit_desc": "Stimmungslicht-Steuerung",
"moodlit_long_desc": "Steuere deine smarten Lichter basierend auf deiner Stimmung und Aktivität.",
"mana_desc": "Zentrale Verwaltung",
"mana_long_desc": "Verwalte alle deine Mana-Apps und Einstellungen an einem Ort.",
"status_published": "Verfügbar",

View file

@ -6,8 +6,6 @@
"maerchenzauber_long_desc": "Create personalized children's stories with AI-generated illustrations.",
"cards_desc": "AI Flashcards",
"cards_long_desc": "Create and study with smart flashcards and AI-powered spaced repetition.",
"moodlit_desc": "Mood light control",
"moodlit_long_desc": "Control your smart lights based on your mood and activity.",
"mana_desc": "Central management",
"mana_long_desc": "Manage all your Mana apps and settings in one place.",
"status_published": "Available",

View file

@ -6,8 +6,6 @@
"maerchenzauber_long_desc": "Crea historias personalizadas para niños con ilustraciones generadas por IA.",
"cards_desc": "Flashcards IA",
"cards_long_desc": "Crea y estudia con flashcards inteligentes y repetición espaciada con IA.",
"moodlit_desc": "Control de luces ambientales",
"moodlit_long_desc": "Controla tus luces inteligentes según tu estado de ánimo y actividades.",
"mana_desc": "Gestión central",
"mana_long_desc": "Gestiona todas tus apps Mana y configuraciones en un solo lugar.",
"status_published": "Disponible",

View file

@ -6,8 +6,6 @@
"maerchenzauber_long_desc": "Créez des histoires personnalisées pour enfants avec des illustrations générées par l'IA.",
"cards_desc": "Flashcards IA",
"cards_long_desc": "Créez et étudiez avec des flashcards intelligentes et la répétition espacée assistée par IA.",
"moodlit_desc": "Contrôle d'éclairage ambiant",
"moodlit_long_desc": "Contrôlez vos lumières intelligentes en fonction de votre humeur et de vos activités.",
"mana_desc": "Gestion centrale",
"mana_long_desc": "Gérez toutes vos applications Mana et paramètres en un seul endroit.",
"status_published": "Disponible",

View file

@ -6,8 +6,6 @@
"maerchenzauber_long_desc": "Crea storie personalizzate per bambini con illustrazioni generate dall'AI.",
"cards_desc": "Flashcard AI",
"cards_long_desc": "Crea e studia con flashcard intelligenti e ripetizione spaziata basata su AI.",
"moodlit_desc": "Controllo luci ambientali",
"moodlit_long_desc": "Controlla le tue luci smart in base al tuo umore e alle tue attività.",
"mana_desc": "Gestione centrale",
"mana_long_desc": "Gestisci tutte le tue app Mana e impostazioni in un unico posto.",
"status_published": "Disponibile",

View file

@ -15,15 +15,12 @@
"music": "Musik",
"photos": "Fotos",
"storage": "Speicher",
"food": "Essen",
"plants": "Pflanzen",
"presi": "Presi",
"inventory": "Inventar",
"memoro": "Memoro",
"questions": "Recherche",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Stadtführer",
"uload": "uLoad",
"calc": "Rechner",
"period": "Periode",
@ -70,7 +67,6 @@
"help": "Hilfe",
"wetter": "Wetter",
"feedback": "Feedback",
"wardrobe": "Kleiderschrank",
"library": "Bibliothek",
"spaces": "Bereiche",
"website": "Website",

View file

@ -15,15 +15,12 @@
"music": "Music",
"photos": "Photos",
"storage": "Storage",
"food": "Food",
"plants": "Plants",
"presi": "Presi",
"inventory": "Inventory",
"memoro": "Memoro",
"questions": "Research",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "City Guide",
"uload": "uLoad",
"calc": "Calculator",
"period": "Period",
@ -70,7 +67,6 @@
"help": "Help",
"wetter": "Weather",
"feedback": "Feedback",
"wardrobe": "Wardrobe",
"library": "Library",
"spaces": "Spaces",
"website": "Website",

View file

@ -15,15 +15,12 @@
"music": "Música",
"photos": "Fotos",
"storage": "Almacén",
"food": "Food",
"plants": "Plantas",
"presi": "Presi",
"inventory": "Inventario",
"memoro": "Memoro",
"questions": "Investigación",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guía urbana",
"uload": "uLoad",
"calc": "Calculadora",
"period": "Ciclo",
@ -70,7 +67,6 @@
"help": "Ayuda",
"wetter": "Tiempo",
"feedback": "Comentarios",
"wardrobe": "Armario",
"library": "Biblioteca",
"spaces": "Espacios",
"website": "Sitio web",

View file

@ -15,15 +15,12 @@
"music": "Musique",
"photos": "Photos",
"storage": "Stockage",
"food": "Food",
"plants": "Plantes",
"presi": "Presi",
"inventory": "Inventaire",
"memoro": "Memoro",
"questions": "Recherche",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guide urbain",
"uload": "uLoad",
"calc": "Calculatrice",
"period": "Règles",
@ -70,7 +67,6 @@
"help": "Aide",
"wetter": "Météo",
"feedback": "Retours",
"wardrobe": "Garde-robe",
"library": "Bibliothèque",
"spaces": "Espaces",
"website": "Site web",

View file

@ -15,15 +15,12 @@
"music": "Musica",
"photos": "Foto",
"storage": "Archivio",
"food": "Food",
"plants": "Piante",
"presi": "Presi",
"inventory": "Inventario",
"memoro": "Memoro",
"questions": "Ricerca",
"skilltree": "Skills",
"moodlit": "Moodlit",
"citycorners": "Guida città",
"uload": "uLoad",
"calc": "Calcolatrice",
"period": "Ciclo",
@ -70,7 +67,6 @@
"help": "Aiuto",
"wetter": "Meteo",
"feedback": "Feedback",
"wardrobe": "Guardaroba",
"library": "Biblioteca",
"spaces": "Spazi",
"website": "Sito web",

View file

@ -1,255 +0,0 @@
{
"app": {
"name": "CityCorners",
"tagline": "Entdecke Städte weltweit"
},
"nav": {
"explore": "Entdecken",
"map": "Karte",
"add": "Hinzufügen",
"favorites": "Favoriten",
"settings": "Einstellungen",
"showNav": "Navigation einblenden",
"hideNav": "Navigation ausblenden",
"cities": "Städte"
},
"cities": {
"title": "Städte entdecken",
"subtitle": "Von der Community für die Community",
"search": "Stadt suchen...",
"add": "Stadt hinzufügen",
"empty": "Noch keine Städte. Sei der Erste!",
"locationsCount": "{count} Orte",
"noLocationsYet": "Noch keine Orte",
"contributors": "{count} Beitragende",
"contributorsOne": "1 Beitragender",
"onMap": "{count} auf der Karte",
"recentlyAdded": "Zuletzt hinzugefügt",
"stats": "Statistiken",
"totalCities": "{count} Städte",
"totalLocations": "{count} Orte",
"totalContributors": "{count} Beitragende",
"topCategories": "Top-Kategorien"
},
"cityAdd": {
"title": "Neue Stadt anlegen",
"subtitle": "Füge eine Stadt, ein Dorf oder einen Ort hinzu",
"name": "Name",
"namePlaceholder": "z.B. Konstanz",
"country": "Land",
"countryPlaceholder": "z.B. Deutschland",
"state": "Bundesland / Region (optional)",
"statePlaceholder": "z.B. Baden-Württemberg",
"description": "Beschreibung (optional)",
"descriptionPlaceholder": "Was macht diesen Ort besonders?",
"imageUrl": "Bild-URL (optional)",
"imageUrlPlaceholder": "https://example.com/bild.jpg",
"submit": "Stadt anlegen",
"submitting": "Wird angelegt...",
"loginRequired": "Melde dich an, um Städte anzulegen.",
"error": "Fehler beim Anlegen. Bitte versuche es erneut.",
"geocoding": "Koordinaten werden ermittelt...",
"coordinatesFound": "Koordinaten gefunden",
"slugExists": "Eine Stadt mit diesem Namen existiert bereits."
},
"home": {
"title": "Orte entdecken",
"subtitle": "Sehenswürdigkeiten, Restaurants, Museen und mehr",
"all": "Alle",
"loading": "Laden...",
"noResults": "Keine Orte gefunden.",
"noResultsCategory": "Keine {category} gefunden.",
"addFirst": "Ersten Ort hinzufügen",
"loadMore": "Mehr laden"
},
"categories": {
"sight": "Sehenswürdigkeiten",
"restaurant": "Restaurants",
"shop": "Läden",
"museum": "Museen",
"cafe": "Cafés",
"bar": "Bars",
"park": "Parks",
"beach": "Strandbäder",
"hotel": "Hotels",
"event_venue": "Veranstaltungsorte",
"viewpoint": "Aussichtspunkte"
},
"category": {
"sight": "Sehenswürdigkeit",
"restaurant": "Restaurant",
"shop": "Laden",
"museum": "Museum",
"cafe": "Café",
"bar": "Bar",
"park": "Park",
"beach": "Strandbad",
"hotel": "Hotel",
"event_venue": "Veranstaltungsort",
"viewpoint": "Aussichtspunkt"
},
"detail": {
"history": "Geschichte",
"openInMaps": "OSM",
"showOnMap": "Auf Karte",
"directions": "Route",
"share": "Teilen",
"linkCopied": "Link kopiert!",
"showDetails": "Details",
"back": "Zurück zur Übersicht",
"notFound": "Ort nicht gefunden.",
"edit": "Bearbeiten",
"delete": "Löschen",
"deleteConfirm": "Bist du sicher, dass du diesen Ort löschen möchtest? Das kann nicht rückgängig gemacht werden.",
"confirmDelete": "Endgültig löschen",
"deleting": "Wird gelöscht...",
"cancel": "Abbrechen",
"nearby": "In der Nähe",
"website": "Webseite",
"phone": "Telefon",
"openingHours": "Öffnungszeiten",
"closed": "Geschlossen",
"openNow": "Jetzt geöffnet",
"closedNow": "Geschlossen"
},
"days": {
"mo": "Montag",
"tu": "Dienstag",
"we": "Mittwoch",
"th": "Donnerstag",
"fr": "Freitag",
"sa": "Samstag",
"su": "Sonntag"
},
"gallery": {
"addPhoto": "Foto hinzufügen",
"add": "Hinzufügen",
"addError": "Foto konnte nicht hinzugefügt werden."
},
"favorites": {
"title": "Favoriten",
"subtitle": "Deine gespeicherten Orte",
"empty": "Noch keine Favoriten. Tippe auf das Herz bei einer Location, um sie zu speichern.",
"loginRequired": "Melde dich an, um Favoriten zu speichern.",
"add": "Zu Favoriten hinzufügen",
"remove": "Aus Favoriten entfernen",
"tabFavorites": "Favoriten",
"tabCollections": "Sammlungen"
},
"collections": {
"title": "Sammlungen",
"empty": "Noch keine Sammlungen erstellt.",
"create": "Sammlung erstellen",
"name": "Name",
"namePlaceholder": "z.B. Meine Lieblingsrestaurants",
"description": "Beschreibung (optional)",
"descriptionPlaceholder": "Worum geht es in dieser Sammlung?",
"save": "Speichern",
"cancel": "Abbrechen",
"locations": "{count} Orte",
"noLocations": "Keine Orte in dieser Sammlung.",
"delete": "Sammlung löschen",
"deleteConfirm": "Bist du sicher, dass du diese Sammlung löschen möchtest?",
"back": "Zurück zu Sammlungen"
},
"map": {
"title": "Karte",
"subtitle": "Alle Orte auf der Karte",
"locateMe": "Mein Standort",
"yourLocation": "Du bist hier",
"geolocationNotSupported": "Standortbestimmung wird nicht unterstützt.",
"geolocationError": "Standort konnte nicht ermittelt werden.",
"filterAll": "Alle"
},
"search": {
"placeholder": "Ort suchen...",
"noResults": "Keine Ergebnisse",
"searching": "Suche..."
},
"settings": {
"title": "Einstellungen",
"appearance": "Erscheinungsbild",
"mode": "Modus",
"light": "Hell",
"dark": "Dunkel",
"system": "System",
"colorScheme": "Farbschema",
"account": "Account",
"email": "E-Mail",
"logout": "Abmelden",
"loginPrompt": "Melde dich an, um Favoriten zu speichern und alle Features zu nutzen.",
"login": "Anmelden",
"register": "Registrieren",
"about": "Über CityCorners",
"aboutText": "CityCorners ist eine offene Plattform für Stadtführer weltweit. Entdecke Orte, die von der Community geteilt werden — oder lege selbst eine Stadt an."
},
"auth": {
"loginTitle": "Login - CityCorners",
"registerTitle": "Registrieren - CityCorners"
},
"add": {
"title": "Ort hinzufügen",
"subtitle": "Teile deinen Lieblingsort",
"name": "Name",
"namePlaceholder": "z.B. Café am See",
"category": "Kategorie",
"description": "Beschreibung",
"descriptionPlaceholder": "Was macht diesen Ort besonders?",
"minChars": "Mindestens 10 Zeichen",
"address": "Adresse (optional)",
"addressPlaceholder": "z.B. Seestraße 1",
"searchTitle": "Ort im Web suchen",
"searchSubtitle": "Wir suchen automatisch nach Infos und füllen das Formular vor.",
"searchPlaceholder": "z.B. Café Zeitlos",
"searchButton": "Suchen",
"skipSearch": "Überspringen und manuell eintragen",
"foundSources": "Quellen gefunden:",
"reset": "Zurück",
"submit": "Ort einreichen",
"submitting": "Wird eingereicht...",
"loginRequired": "Melde dich an, um Orte hinzuzufügen.",
"error": "Fehler beim Einreichen. Bitte versuche es erneut.",
"imageUrl": "Bild-URL (optional)",
"imageUrlPlaceholder": "https://example.com/bild.jpg",
"imagePreview": "Bildvorschau",
"imageLoadError": "Bild konnte nicht geladen werden.",
"imageRetry": "Erneut versuchen",
"geocoding": "Koordinaten werden ermittelt...",
"coordinatesFound": "Koordinaten gefunden",
"website": "Webseite (optional)",
"websitePlaceholder": "https://example.com",
"phone": "Telefon (optional)",
"phonePlaceholder": "+49 7531 12345"
},
"edit": {
"title": "Ort bearbeiten",
"subtitle": "Ändere die Details dieses Ortes",
"save": "Änderungen speichern",
"saving": "Wird gespeichert...",
"cancel": "Abbrechen",
"error": "Fehler beim Speichern. Bitte versuche es erneut.",
"loadError": "Ort konnte nicht geladen werden.",
"forbidden": "Du kannst nur deine eigenen Orte bearbeiten."
},
"reviews": {
"title": "Bewertungen",
"write": "Bewertung schreiben",
"yourRating": "Deine Bewertung",
"commentPlaceholder": "Was hat dir gefallen? (optional)",
"submit": "Absenden",
"submitting": "Wird gesendet...",
"loginRequired": "Melde dich an, um eine Bewertung zu schreiben.",
"alreadyReviewed": "Du hast diesen Ort bereits bewertet.",
"deleteConfirm": "Bewertung löschen?",
"delete": "Löschen",
"noReviews": "Noch keine Bewertungen. Sei der Erste!",
"error": "Bewertung konnte nicht gespeichert werden.",
"count": "{count} Bewertungen",
"countOne": "1 Bewertung"
},
"offline": {
"title": "Keine Verbindung",
"message": "Du bist gerade offline. Sobald du wieder eine Internetverbindung hast, kannst du CityCorners weiter nutzen.",
"retry": "Erneut versuchen"
}
}

View file

@ -1,255 +0,0 @@
{
"app": {
"name": "CityCorners",
"tagline": "Discover cities worldwide"
},
"nav": {
"explore": "Explore",
"map": "Map",
"add": "Add",
"favorites": "Favorites",
"settings": "Settings",
"showNav": "Show navigation",
"hideNav": "Hide navigation",
"cities": "Cities"
},
"cities": {
"title": "Discover cities",
"subtitle": "By the community, for the community",
"search": "Search cities...",
"add": "Add a city",
"empty": "No cities yet. Be the first!",
"locationsCount": "{count} places",
"noLocationsYet": "No places yet",
"contributors": "{count} contributors",
"contributorsOne": "1 contributor",
"onMap": "{count} on the map",
"recentlyAdded": "Recently added",
"stats": "Statistics",
"totalCities": "{count} cities",
"totalLocations": "{count} places",
"totalContributors": "{count} contributors",
"topCategories": "Top categories"
},
"cityAdd": {
"title": "Add a new city",
"subtitle": "Add a city, village, or town",
"name": "Name",
"namePlaceholder": "e.g. Berlin",
"country": "Country",
"countryPlaceholder": "e.g. Germany",
"state": "State / Region (optional)",
"statePlaceholder": "e.g. Bavaria",
"description": "Description (optional)",
"descriptionPlaceholder": "What makes this place special?",
"imageUrl": "Image URL (optional)",
"imageUrlPlaceholder": "https://example.com/image.jpg",
"submit": "Add city",
"submitting": "Creating...",
"loginRequired": "Sign in to add cities.",
"error": "Failed to create. Please try again.",
"geocoding": "Finding coordinates...",
"coordinatesFound": "Coordinates found",
"slugExists": "A city with this name already exists."
},
"home": {
"title": "Discover places",
"subtitle": "Sights, restaurants, museums and more",
"all": "All",
"loading": "Loading...",
"noResults": "No places found.",
"noResultsCategory": "No {category} found.",
"addFirst": "Add the first place",
"loadMore": "Load more"
},
"categories": {
"sight": "Sights",
"restaurant": "Restaurants",
"shop": "Shops",
"museum": "Museums",
"cafe": "Cafés",
"bar": "Bars",
"park": "Parks",
"beach": "Beaches",
"hotel": "Hotels",
"event_venue": "Event Venues",
"viewpoint": "Viewpoints"
},
"category": {
"sight": "Sight",
"restaurant": "Restaurant",
"shop": "Shop",
"museum": "Museum",
"cafe": "Café",
"bar": "Bar",
"park": "Park",
"beach": "Beach",
"hotel": "Hotel",
"event_venue": "Event Venue",
"viewpoint": "Viewpoint"
},
"detail": {
"history": "History",
"openInMaps": "OSM",
"showOnMap": "On map",
"directions": "Directions",
"share": "Share",
"linkCopied": "Link copied!",
"showDetails": "Details",
"back": "Back to overview",
"notFound": "Place not found.",
"edit": "Edit",
"delete": "Delete",
"deleteConfirm": "Are you sure you want to delete this place? This cannot be undone.",
"confirmDelete": "Delete permanently",
"deleting": "Deleting...",
"cancel": "Cancel",
"nearby": "Nearby",
"website": "Website",
"phone": "Phone",
"openingHours": "Opening hours",
"closed": "Closed",
"openNow": "Open now",
"closedNow": "Closed"
},
"days": {
"mo": "Monday",
"tu": "Tuesday",
"we": "Wednesday",
"th": "Thursday",
"fr": "Friday",
"sa": "Saturday",
"su": "Sunday"
},
"gallery": {
"addPhoto": "Add photo",
"add": "Add",
"addError": "Could not add photo."
},
"favorites": {
"title": "Favorites",
"subtitle": "Your saved places",
"empty": "No favorites yet. Tap the heart on a location to save it.",
"loginRequired": "Sign in to save favorites.",
"add": "Add to favorites",
"remove": "Remove from favorites",
"tabFavorites": "Favorites",
"tabCollections": "Collections"
},
"collections": {
"title": "Collections",
"empty": "No collections created yet.",
"create": "Create collection",
"name": "Name",
"namePlaceholder": "e.g. My favorite restaurants",
"description": "Description (optional)",
"descriptionPlaceholder": "What is this collection about?",
"save": "Save",
"cancel": "Cancel",
"locations": "{count} places",
"noLocations": "No places in this collection.",
"delete": "Delete collection",
"deleteConfirm": "Are you sure you want to delete this collection?",
"back": "Back to collections"
},
"map": {
"title": "Map",
"subtitle": "All places on the map",
"locateMe": "My location",
"yourLocation": "You are here",
"geolocationNotSupported": "Geolocation is not supported.",
"geolocationError": "Could not determine location.",
"filterAll": "All"
},
"search": {
"placeholder": "Search places...",
"noResults": "No results",
"searching": "Searching..."
},
"settings": {
"title": "Settings",
"appearance": "Appearance",
"mode": "Mode",
"light": "Light",
"dark": "Dark",
"system": "System",
"colorScheme": "Color scheme",
"account": "Account",
"email": "Email",
"logout": "Sign out",
"loginPrompt": "Sign in to save favorites and use all features.",
"login": "Sign in",
"register": "Sign up",
"about": "About CityCorners",
"aboutText": "CityCorners is an open platform for city guides worldwide. Discover places shared by the community — or add your own city."
},
"auth": {
"loginTitle": "Login - CityCorners",
"registerTitle": "Sign up - CityCorners"
},
"add": {
"title": "Add a place",
"subtitle": "Share your favorite spot",
"name": "Name",
"namePlaceholder": "e.g. Lakeside Cafe",
"category": "Category",
"description": "Description",
"descriptionPlaceholder": "What makes this place special?",
"minChars": "At least 10 characters",
"address": "Address (optional)",
"addressPlaceholder": "e.g. Main Street 1",
"searchTitle": "Search for a place online",
"searchSubtitle": "We'll automatically find info and pre-fill the form for you.",
"searchPlaceholder": "e.g. Cafe Zeitlos",
"searchButton": "Search",
"skipSearch": "Skip and enter manually",
"foundSources": "Sources found:",
"reset": "Back",
"submit": "Submit place",
"submitting": "Submitting...",
"loginRequired": "Sign in to add places.",
"error": "Failed to submit. Please try again.",
"imageUrl": "Image URL (optional)",
"imageUrlPlaceholder": "https://example.com/image.jpg",
"imagePreview": "Image preview",
"imageLoadError": "Image could not be loaded.",
"imageRetry": "Retry",
"geocoding": "Finding coordinates...",
"coordinatesFound": "Coordinates found",
"website": "Website (optional)",
"websitePlaceholder": "https://example.com",
"phone": "Phone (optional)",
"phonePlaceholder": "+49 7531 12345"
},
"edit": {
"title": "Edit place",
"subtitle": "Update the details of this place",
"save": "Save changes",
"saving": "Saving...",
"cancel": "Cancel",
"error": "Failed to save. Please try again.",
"loadError": "Could not load place.",
"forbidden": "You can only edit your own places."
},
"reviews": {
"title": "Reviews",
"write": "Write a review",
"yourRating": "Your rating",
"commentPlaceholder": "What did you like? (optional)",
"submit": "Submit",
"submitting": "Submitting...",
"loginRequired": "Sign in to write a review.",
"alreadyReviewed": "You have already reviewed this place.",
"deleteConfirm": "Delete review?",
"delete": "Delete",
"noReviews": "No reviews yet. Be the first!",
"error": "Could not save review.",
"count": "{count} reviews",
"countOne": "1 review"
},
"offline": {
"title": "No connection",
"message": "You are currently offline. You can continue using CityCorners once you have an internet connection again.",
"retry": "Try again"
}
}

View file

@ -1,255 +0,0 @@
{
"app": {
"name": "CityCorners",
"tagline": "Descubre ciudades en todo el mundo"
},
"nav": {
"explore": "Explorar",
"map": "Mapa",
"add": "Agregar",
"favorites": "Favoritos",
"settings": "Ajustes",
"showNav": "Mostrar navegación",
"hideNav": "Ocultar navegación",
"cities": "Ciudades"
},
"cities": {
"title": "Descubrir ciudades",
"subtitle": "De la comunidad, para la comunidad",
"search": "Buscar ciudades...",
"add": "Agregar ciudad",
"empty": "Aún no hay ciudades. ¡Sé el primero!",
"locationsCount": "{count} lugares",
"noLocationsYet": "Aún no hay lugares",
"contributors": "{count} colaboradores",
"contributorsOne": "1 colaborador",
"onMap": "{count} en el mapa",
"recentlyAdded": "Agregados recientemente",
"stats": "Estadísticas",
"totalCities": "{count} ciudades",
"totalLocations": "{count} lugares",
"totalContributors": "{count} colaboradores",
"topCategories": "Categorías principales"
},
"cityAdd": {
"title": "Agregar nueva ciudad",
"subtitle": "Agrega una ciudad, pueblo o localidad",
"name": "Nombre",
"namePlaceholder": "ej. Madrid",
"country": "País",
"countryPlaceholder": "ej. España",
"state": "Comunidad / Región (opcional)",
"statePlaceholder": "ej. Comunidad de Madrid",
"description": "Descripción (opcional)",
"descriptionPlaceholder": "¿Qué hace especial a este lugar?",
"imageUrl": "URL de imagen (opcional)",
"imageUrlPlaceholder": "https://example.com/imagen.jpg",
"submit": "Agregar ciudad",
"submitting": "Creando...",
"loginRequired": "Inicia sesión para agregar ciudades.",
"error": "Error al crear. Inténtalo de nuevo.",
"geocoding": "Buscando coordenadas...",
"coordinatesFound": "Coordenadas encontradas",
"slugExists": "Ya existe una ciudad con este nombre."
},
"home": {
"title": "Descubrir lugares",
"subtitle": "Monumentos, restaurantes, museos y más",
"all": "Todos",
"loading": "Cargando...",
"noResults": "No se encontraron lugares.",
"noResultsCategory": "No se encontraron {category}.",
"addFirst": "Agrega el primer lugar",
"loadMore": "Cargar más"
},
"categories": {
"sight": "Monumentos",
"restaurant": "Restaurantes",
"shop": "Tiendas",
"museum": "Museos",
"cafe": "Cafeterías",
"bar": "Bares",
"park": "Parques",
"beach": "Playas",
"hotel": "Hoteles",
"event_venue": "Salas de eventos",
"viewpoint": "Miradores"
},
"category": {
"sight": "Monumento",
"restaurant": "Restaurante",
"shop": "Tienda",
"museum": "Museo",
"cafe": "Cafetería",
"bar": "Bar",
"park": "Parque",
"beach": "Playa",
"hotel": "Hotel",
"event_venue": "Sala de eventos",
"viewpoint": "Mirador"
},
"detail": {
"history": "Historia",
"openInMaps": "OSM",
"showOnMap": "En el mapa",
"directions": "Cómo llegar",
"share": "Compartir",
"linkCopied": "¡Enlace copiado!",
"showDetails": "Detalles",
"back": "Volver al listado",
"notFound": "Lugar no encontrado.",
"edit": "Editar",
"delete": "Eliminar",
"deleteConfirm": "¿Seguro que quieres eliminar este lugar? No se puede deshacer.",
"confirmDelete": "Eliminar definitivamente",
"deleting": "Eliminando...",
"cancel": "Cancelar",
"nearby": "Cerca",
"website": "Sitio web",
"phone": "Teléfono",
"openingHours": "Horario",
"closed": "Cerrado",
"openNow": "Abierto ahora",
"closedNow": "Cerrado"
},
"days": {
"mo": "Lunes",
"tu": "Martes",
"we": "Miércoles",
"th": "Jueves",
"fr": "Viernes",
"sa": "Sábado",
"su": "Domingo"
},
"gallery": {
"addPhoto": "Agregar foto",
"add": "Agregar",
"addError": "No se pudo agregar la foto."
},
"favorites": {
"title": "Favoritos",
"subtitle": "Tus lugares guardados",
"empty": "Aún no hay favoritos. Toca el corazón en un lugar para guardarlo.",
"loginRequired": "Inicia sesión para guardar favoritos.",
"add": "Agregar a favoritos",
"remove": "Quitar de favoritos",
"tabFavorites": "Favoritos",
"tabCollections": "Colecciones"
},
"collections": {
"title": "Colecciones",
"empty": "Aún no hay colecciones.",
"create": "Crear colección",
"name": "Nombre",
"namePlaceholder": "ej. Mis restaurantes favoritos",
"description": "Descripción (opcional)",
"descriptionPlaceholder": "¿De qué trata esta colección?",
"save": "Guardar",
"cancel": "Cancelar",
"locations": "{count} lugares",
"noLocations": "No hay lugares en esta colección.",
"delete": "Eliminar colección",
"deleteConfirm": "¿Seguro que quieres eliminar esta colección?",
"back": "Volver a colecciones"
},
"map": {
"title": "Mapa",
"subtitle": "Todos los lugares en el mapa",
"locateMe": "Mi ubicación",
"yourLocation": "Estás aquí",
"geolocationNotSupported": "La geolocalización no está disponible.",
"geolocationError": "No se pudo determinar la ubicación.",
"filterAll": "Todos"
},
"search": {
"placeholder": "Buscar lugares...",
"noResults": "Sin resultados",
"searching": "Buscando..."
},
"settings": {
"title": "Ajustes",
"appearance": "Apariencia",
"mode": "Modo",
"light": "Claro",
"dark": "Oscuro",
"system": "Sistema",
"colorScheme": "Esquema de color",
"account": "Cuenta",
"email": "Email",
"logout": "Cerrar sesión",
"loginPrompt": "Inicia sesión para guardar favoritos y usar todas las funciones.",
"login": "Iniciar sesión",
"register": "Registrarse",
"about": "Sobre CityCorners",
"aboutText": "CityCorners es una plataforma abierta para guías de ciudades en todo el mundo. Descubre lugares compartidos por la comunidad o agrega tu propia ciudad."
},
"auth": {
"loginTitle": "Login - CityCorners",
"registerTitle": "Registro - CityCorners"
},
"add": {
"title": "Agregar un lugar",
"subtitle": "Comparte tu sitio favorito",
"name": "Nombre",
"namePlaceholder": "ej. Café del Lago",
"category": "Categoría",
"description": "Descripción",
"descriptionPlaceholder": "¿Qué hace especial a este lugar?",
"minChars": "Mínimo 10 caracteres",
"address": "Dirección (opcional)",
"addressPlaceholder": "ej. Calle Mayor 1",
"searchTitle": "Buscar un lugar en línea",
"searchSubtitle": "Encontraremos la información y rellenaremos el formulario automáticamente.",
"searchPlaceholder": "ej. Café Zeitlos",
"searchButton": "Buscar",
"skipSearch": "Omitir e ingresar manualmente",
"foundSources": "Fuentes encontradas:",
"reset": "Atrás",
"submit": "Enviar lugar",
"submitting": "Enviando...",
"loginRequired": "Inicia sesión para agregar lugares.",
"error": "Error al enviar. Inténtalo de nuevo.",
"imageUrl": "URL de imagen (opcional)",
"imageUrlPlaceholder": "https://example.com/imagen.jpg",
"imagePreview": "Vista previa de imagen",
"imageLoadError": "No se pudo cargar la imagen.",
"imageRetry": "Reintentar",
"geocoding": "Buscando coordenadas...",
"coordinatesFound": "Coordenadas encontradas",
"website": "Sitio web (opcional)",
"websitePlaceholder": "https://example.com",
"phone": "Teléfono (opcional)",
"phonePlaceholder": "+34 91 123 4567"
},
"edit": {
"title": "Editar lugar",
"subtitle": "Actualiza los detalles de este lugar",
"save": "Guardar cambios",
"saving": "Guardando...",
"cancel": "Cancelar",
"error": "Error al guardar. Inténtalo de nuevo.",
"loadError": "No se pudo cargar el lugar.",
"forbidden": "Solo puedes editar tus propios lugares."
},
"reviews": {
"title": "Reseñas",
"write": "Escribir reseña",
"yourRating": "Tu valoración",
"commentPlaceholder": "¿Qué te gustó? (opcional)",
"submit": "Enviar",
"submitting": "Enviando...",
"loginRequired": "Inicia sesión para escribir una reseña.",
"alreadyReviewed": "Ya has opinado sobre este lugar.",
"deleteConfirm": "¿Eliminar reseña?",
"delete": "Eliminar",
"noReviews": "Aún no hay reseñas. ¡Sé el primero!",
"error": "No se pudo guardar la reseña.",
"count": "{count} reseñas",
"countOne": "1 reseña"
},
"offline": {
"title": "Sin conexión",
"message": "Estás sin conexión. Puedes seguir usando CityCorners cuando tengas conexión a internet.",
"retry": "Reintentar"
}
}

View file

@ -1,255 +0,0 @@
{
"app": {
"name": "CityCorners",
"tagline": "Découvrez des villes du monde entier"
},
"nav": {
"explore": "Explorer",
"map": "Carte",
"add": "Ajouter",
"favorites": "Favoris",
"settings": "Paramètres",
"showNav": "Afficher la navigation",
"hideNav": "Masquer la navigation",
"cities": "Villes"
},
"cities": {
"title": "Découvrir des villes",
"subtitle": "Par la communauté, pour la communauté",
"search": "Rechercher des villes...",
"add": "Ajouter une ville",
"empty": "Pas encore de villes. Soyez le premier !",
"locationsCount": "{count} lieux",
"noLocationsYet": "Pas encore de lieux",
"contributors": "{count} contributeurs",
"contributorsOne": "1 contributeur",
"onMap": "{count} sur la carte",
"recentlyAdded": "Ajoutés récemment",
"stats": "Statistiques",
"totalCities": "{count} villes",
"totalLocations": "{count} lieux",
"totalContributors": "{count} contributeurs",
"topCategories": "Catégories principales"
},
"cityAdd": {
"title": "Ajouter une ville",
"subtitle": "Ajoutez une ville, un village ou une localité",
"name": "Nom",
"namePlaceholder": "ex. Paris",
"country": "Pays",
"countryPlaceholder": "ex. France",
"state": "Région / Département (optionnel)",
"statePlaceholder": "ex. Île-de-France",
"description": "Description (optionnel)",
"descriptionPlaceholder": "Qu'est-ce qui rend cet endroit spécial ?",
"imageUrl": "URL de l'image (optionnel)",
"imageUrlPlaceholder": "https://example.com/image.jpg",
"submit": "Ajouter la ville",
"submitting": "Création...",
"loginRequired": "Connectez-vous pour ajouter des villes.",
"error": "Échec de la création. Veuillez réessayer.",
"geocoding": "Recherche des coordonnées...",
"coordinatesFound": "Coordonnées trouvées",
"slugExists": "Une ville avec ce nom existe déjà."
},
"home": {
"title": "Découvrir des lieux",
"subtitle": "Monuments, restaurants, musées et plus",
"all": "Tous",
"loading": "Chargement...",
"noResults": "Aucun lieu trouvé.",
"noResultsCategory": "Aucun {category} trouvé.",
"addFirst": "Ajoutez le premier lieu",
"loadMore": "Charger plus"
},
"categories": {
"sight": "Monuments",
"restaurant": "Restaurants",
"shop": "Boutiques",
"museum": "Musées",
"cafe": "Cafés",
"bar": "Bars",
"park": "Parcs",
"beach": "Plages",
"hotel": "Hôtels",
"event_venue": "Salles d'événements",
"viewpoint": "Points de vue"
},
"category": {
"sight": "Monument",
"restaurant": "Restaurant",
"shop": "Boutique",
"museum": "Musée",
"cafe": "Café",
"bar": "Bar",
"park": "Parc",
"beach": "Plage",
"hotel": "Hôtel",
"event_venue": "Salle d'événements",
"viewpoint": "Point de vue"
},
"detail": {
"history": "Histoire",
"openInMaps": "OSM",
"showOnMap": "Sur la carte",
"directions": "Itinéraire",
"share": "Partager",
"linkCopied": "Lien copié !",
"showDetails": "Détails",
"back": "Retour à la liste",
"notFound": "Lieu introuvable.",
"edit": "Modifier",
"delete": "Supprimer",
"deleteConfirm": "Voulez-vous vraiment supprimer ce lieu ? Cette action est irréversible.",
"confirmDelete": "Supprimer définitivement",
"deleting": "Suppression...",
"cancel": "Annuler",
"nearby": "À proximité",
"website": "Site web",
"phone": "Téléphone",
"openingHours": "Horaires d'ouverture",
"closed": "Fermé",
"openNow": "Ouvert",
"closedNow": "Fermé"
},
"days": {
"mo": "Lundi",
"tu": "Mardi",
"we": "Mercredi",
"th": "Jeudi",
"fr": "Vendredi",
"sa": "Samedi",
"su": "Dimanche"
},
"gallery": {
"addPhoto": "Ajouter une photo",
"add": "Ajouter",
"addError": "Impossible d'ajouter la photo."
},
"favorites": {
"title": "Favoris",
"subtitle": "Vos lieux enregistrés",
"empty": "Pas encore de favoris. Appuyez sur le cœur pour sauvegarder un lieu.",
"loginRequired": "Connectez-vous pour sauvegarder vos favoris.",
"add": "Ajouter aux favoris",
"remove": "Retirer des favoris",
"tabFavorites": "Favoris",
"tabCollections": "Collections"
},
"collections": {
"title": "Collections",
"empty": "Pas encore de collections.",
"create": "Créer une collection",
"name": "Nom",
"namePlaceholder": "ex. Mes restaurants préférés",
"description": "Description (optionnel)",
"descriptionPlaceholder": "De quoi parle cette collection ?",
"save": "Enregistrer",
"cancel": "Annuler",
"locations": "{count} lieux",
"noLocations": "Aucun lieu dans cette collection.",
"delete": "Supprimer la collection",
"deleteConfirm": "Voulez-vous vraiment supprimer cette collection ?",
"back": "Retour aux collections"
},
"map": {
"title": "Carte",
"subtitle": "Tous les lieux sur la carte",
"locateMe": "Ma position",
"yourLocation": "Vous êtes ici",
"geolocationNotSupported": "La géolocalisation n'est pas disponible.",
"geolocationError": "Impossible de déterminer la position.",
"filterAll": "Tous"
},
"search": {
"placeholder": "Rechercher des lieux...",
"noResults": "Aucun résultat",
"searching": "Recherche..."
},
"settings": {
"title": "Paramètres",
"appearance": "Apparence",
"mode": "Mode",
"light": "Clair",
"dark": "Sombre",
"system": "Système",
"colorScheme": "Thème de couleur",
"account": "Compte",
"email": "Email",
"logout": "Déconnexion",
"loginPrompt": "Connectez-vous pour sauvegarder vos favoris et utiliser toutes les fonctions.",
"login": "Connexion",
"register": "Inscription",
"about": "À propos de CityCorners",
"aboutText": "CityCorners est une plateforme ouverte de guides de villes du monde entier. Découvrez des lieux partagés par la communauté ou ajoutez votre propre ville."
},
"auth": {
"loginTitle": "Connexion - CityCorners",
"registerTitle": "Inscription - CityCorners"
},
"add": {
"title": "Ajouter un lieu",
"subtitle": "Partagez votre endroit préféré",
"name": "Nom",
"namePlaceholder": "ex. Café du Lac",
"category": "Catégorie",
"description": "Description",
"descriptionPlaceholder": "Qu'est-ce qui rend cet endroit spécial ?",
"minChars": "Au moins 10 caractères",
"address": "Adresse (optionnel)",
"addressPlaceholder": "ex. 1 Rue Principale",
"searchTitle": "Rechercher un lieu en ligne",
"searchSubtitle": "Nous trouverons les informations et pré-remplirons le formulaire.",
"searchPlaceholder": "ex. Café Zeitlos",
"searchButton": "Rechercher",
"skipSearch": "Passer et saisir manuellement",
"foundSources": "Sources trouvées :",
"reset": "Retour",
"submit": "Soumettre le lieu",
"submitting": "Envoi...",
"loginRequired": "Connectez-vous pour ajouter des lieux.",
"error": "Échec de l'envoi. Veuillez réessayer.",
"imageUrl": "URL de l'image (optionnel)",
"imageUrlPlaceholder": "https://example.com/image.jpg",
"imagePreview": "Aperçu de l'image",
"imageLoadError": "Impossible de charger l'image.",
"imageRetry": "Réessayer",
"geocoding": "Recherche des coordonnées...",
"coordinatesFound": "Coordonnées trouvées",
"website": "Site web (optionnel)",
"websitePlaceholder": "https://example.com",
"phone": "Téléphone (optionnel)",
"phonePlaceholder": "+33 1 23 45 67 89"
},
"edit": {
"title": "Modifier le lieu",
"subtitle": "Mettez à jour les détails de ce lieu",
"save": "Enregistrer",
"saving": "Enregistrement...",
"cancel": "Annuler",
"error": "Échec de la sauvegarde. Veuillez réessayer.",
"loadError": "Impossible de charger le lieu.",
"forbidden": "Vous ne pouvez modifier que vos propres lieux."
},
"reviews": {
"title": "Avis",
"write": "Écrire un avis",
"yourRating": "Votre note",
"commentPlaceholder": "Qu'avez-vous aimé ? (optionnel)",
"submit": "Envoyer",
"submitting": "Envoi...",
"loginRequired": "Connectez-vous pour écrire un avis.",
"alreadyReviewed": "Vous avez déjà donné votre avis sur ce lieu.",
"deleteConfirm": "Supprimer l'avis ?",
"delete": "Supprimer",
"noReviews": "Pas encore d'avis. Soyez le premier !",
"error": "Impossible de sauvegarder l'avis.",
"count": "{count} avis",
"countOne": "1 avis"
},
"offline": {
"title": "Pas de connexion",
"message": "Vous êtes hors ligne. Vous pourrez utiliser CityCorners dès que vous aurez une connexion internet.",
"retry": "Réessayer"
}
}

View file

@ -1,255 +0,0 @@
{
"app": {
"name": "CityCorners",
"tagline": "Scopri città in tutto il mondo"
},
"nav": {
"explore": "Esplora",
"map": "Mappa",
"add": "Aggiungi",
"favorites": "Preferiti",
"settings": "Impostazioni",
"showNav": "Mostra navigazione",
"hideNav": "Nascondi navigazione",
"cities": "Città"
},
"cities": {
"title": "Scopri città",
"subtitle": "Dalla community, per la community",
"search": "Cerca città...",
"add": "Aggiungi città",
"empty": "Ancora nessuna città. Sii il primo!",
"locationsCount": "{count} luoghi",
"noLocationsYet": "Ancora nessun luogo",
"contributors": "{count} collaboratori",
"contributorsOne": "1 collaboratore",
"onMap": "{count} sulla mappa",
"recentlyAdded": "Aggiunti di recente",
"stats": "Statistiche",
"totalCities": "{count} città",
"totalLocations": "{count} luoghi",
"totalContributors": "{count} collaboratori",
"topCategories": "Categorie principali"
},
"cityAdd": {
"title": "Aggiungi nuova città",
"subtitle": "Aggiungi una città, un paese o una località",
"name": "Nome",
"namePlaceholder": "es. Roma",
"country": "Paese",
"countryPlaceholder": "es. Italia",
"state": "Regione / Provincia (opzionale)",
"statePlaceholder": "es. Lazio",
"description": "Descrizione (opzionale)",
"descriptionPlaceholder": "Cosa rende speciale questo posto?",
"imageUrl": "URL immagine (opzionale)",
"imageUrlPlaceholder": "https://example.com/immagine.jpg",
"submit": "Aggiungi città",
"submitting": "Creazione...",
"loginRequired": "Accedi per aggiungere città.",
"error": "Creazione fallita. Riprova.",
"geocoding": "Ricerca coordinate...",
"coordinatesFound": "Coordinate trovate",
"slugExists": "Esiste già una città con questo nome."
},
"home": {
"title": "Scopri luoghi",
"subtitle": "Monumenti, ristoranti, musei e altro",
"all": "Tutti",
"loading": "Caricamento...",
"noResults": "Nessun luogo trovato.",
"noResultsCategory": "Nessun {category} trovato.",
"addFirst": "Aggiungi il primo luogo",
"loadMore": "Carica altri"
},
"categories": {
"sight": "Monumenti",
"restaurant": "Ristoranti",
"shop": "Negozi",
"museum": "Musei",
"cafe": "Caffè",
"bar": "Bar",
"park": "Parchi",
"beach": "Spiagge",
"hotel": "Hotel",
"event_venue": "Sale eventi",
"viewpoint": "Punti panoramici"
},
"category": {
"sight": "Monumento",
"restaurant": "Ristorante",
"shop": "Negozio",
"museum": "Museo",
"cafe": "Caffè",
"bar": "Bar",
"park": "Parco",
"beach": "Spiaggia",
"hotel": "Hotel",
"event_venue": "Sala eventi",
"viewpoint": "Punto panoramico"
},
"detail": {
"history": "Storia",
"openInMaps": "OSM",
"showOnMap": "Sulla mappa",
"directions": "Indicazioni",
"share": "Condividi",
"linkCopied": "Link copiato!",
"showDetails": "Dettagli",
"back": "Torna alla lista",
"notFound": "Luogo non trovato.",
"edit": "Modifica",
"delete": "Elimina",
"deleteConfirm": "Vuoi davvero eliminare questo luogo? Non è possibile annullare.",
"confirmDelete": "Elimina definitivamente",
"deleting": "Eliminazione...",
"cancel": "Annulla",
"nearby": "Nelle vicinanze",
"website": "Sito web",
"phone": "Telefono",
"openingHours": "Orari di apertura",
"closed": "Chiuso",
"openNow": "Aperto ora",
"closedNow": "Chiuso"
},
"days": {
"mo": "Lunedì",
"tu": "Martedì",
"we": "Mercoledì",
"th": "Giovedì",
"fr": "Venerdì",
"sa": "Sabato",
"su": "Domenica"
},
"gallery": {
"addPhoto": "Aggiungi foto",
"add": "Aggiungi",
"addError": "Impossibile aggiungere la foto."
},
"favorites": {
"title": "Preferiti",
"subtitle": "I tuoi luoghi salvati",
"empty": "Ancora nessun preferito. Tocca il cuore su un luogo per salvarlo.",
"loginRequired": "Accedi per salvare i preferiti.",
"add": "Aggiungi ai preferiti",
"remove": "Rimuovi dai preferiti",
"tabFavorites": "Preferiti",
"tabCollections": "Collezioni"
},
"collections": {
"title": "Collezioni",
"empty": "Ancora nessuna collezione.",
"create": "Crea collezione",
"name": "Nome",
"namePlaceholder": "es. I miei ristoranti preferiti",
"description": "Descrizione (opzionale)",
"descriptionPlaceholder": "Di cosa tratta questa collezione?",
"save": "Salva",
"cancel": "Annulla",
"locations": "{count} luoghi",
"noLocations": "Nessun luogo in questa collezione.",
"delete": "Elimina collezione",
"deleteConfirm": "Vuoi davvero eliminare questa collezione?",
"back": "Torna alle collezioni"
},
"map": {
"title": "Mappa",
"subtitle": "Tutti i luoghi sulla mappa",
"locateMe": "La mia posizione",
"yourLocation": "Sei qui",
"geolocationNotSupported": "La geolocalizzazione non è disponibile.",
"geolocationError": "Impossibile determinare la posizione.",
"filterAll": "Tutti"
},
"search": {
"placeholder": "Cerca luoghi...",
"noResults": "Nessun risultato",
"searching": "Ricerca..."
},
"settings": {
"title": "Impostazioni",
"appearance": "Aspetto",
"mode": "Modalità",
"light": "Chiaro",
"dark": "Scuro",
"system": "Sistema",
"colorScheme": "Schema colori",
"account": "Account",
"email": "Email",
"logout": "Esci",
"loginPrompt": "Accedi per salvare i preferiti e usare tutte le funzioni.",
"login": "Accedi",
"register": "Registrati",
"about": "Informazioni su CityCorners",
"aboutText": "CityCorners è una piattaforma aperta per guide di città in tutto il mondo. Scopri luoghi condivisi dalla community o aggiungi la tua città."
},
"auth": {
"loginTitle": "Login - CityCorners",
"registerTitle": "Registrazione - CityCorners"
},
"add": {
"title": "Aggiungi un luogo",
"subtitle": "Condividi il tuo posto preferito",
"name": "Nome",
"namePlaceholder": "es. Caffè del Lago",
"category": "Categoria",
"description": "Descrizione",
"descriptionPlaceholder": "Cosa rende speciale questo posto?",
"minChars": "Almeno 10 caratteri",
"address": "Indirizzo (opzionale)",
"addressPlaceholder": "es. Via Roma 1",
"searchTitle": "Cerca un luogo online",
"searchSubtitle": "Troveremo le informazioni e compileremo il modulo automaticamente.",
"searchPlaceholder": "es. Caffè Zeitlos",
"searchButton": "Cerca",
"skipSearch": "Salta e inserisci manualmente",
"foundSources": "Fonti trovate:",
"reset": "Indietro",
"submit": "Invia luogo",
"submitting": "Invio...",
"loginRequired": "Accedi per aggiungere luoghi.",
"error": "Invio fallito. Riprova.",
"imageUrl": "URL immagine (opzionale)",
"imageUrlPlaceholder": "https://example.com/immagine.jpg",
"imagePreview": "Anteprima immagine",
"imageLoadError": "Impossibile caricare l'immagine.",
"imageRetry": "Riprova",
"geocoding": "Ricerca coordinate...",
"coordinatesFound": "Coordinate trovate",
"website": "Sito web (opzionale)",
"websitePlaceholder": "https://example.com",
"phone": "Telefono (opzionale)",
"phonePlaceholder": "+39 06 1234567"
},
"edit": {
"title": "Modifica luogo",
"subtitle": "Aggiorna i dettagli di questo luogo",
"save": "Salva modifiche",
"saving": "Salvataggio...",
"cancel": "Annulla",
"error": "Salvataggio fallito. Riprova.",
"loadError": "Impossibile caricare il luogo.",
"forbidden": "Puoi modificare solo i tuoi luoghi."
},
"reviews": {
"title": "Recensioni",
"write": "Scrivi una recensione",
"yourRating": "La tua valutazione",
"commentPlaceholder": "Cosa ti è piaciuto? (opzionale)",
"submit": "Invia",
"submitting": "Invio...",
"loginRequired": "Accedi per scrivere una recensione.",
"alreadyReviewed": "Hai già recensito questo luogo.",
"deleteConfirm": "Eliminare la recensione?",
"delete": "Elimina",
"noReviews": "Ancora nessuna recensione. Sii il primo!",
"error": "Impossibile salvare la recensione.",
"count": "{count} recensioni",
"countOne": "1 recensione"
},
"offline": {
"title": "Nessuna connessione",
"message": "Sei offline. Potrai usare CityCorners quando avrai una connessione internet.",
"retry": "Riprova"
}
}

View file

@ -1,127 +0,0 @@
{
"app": {
"name": "Food",
"loading": "Laden...",
"tagline": "Ernährung verstehen"
},
"nav": {
"dashboard": "Dashboard",
"meals": "Mahlzeiten",
"goals": "Ziele",
"favorites": "Favoriten",
"stats": "Statistiken",
"settings": "Einstellungen"
},
"meal": {
"add": "Mahlzeit hinzufügen",
"edit": "Mahlzeit bearbeiten",
"delete": "Mahlzeit löschen",
"photo": "Foto aufnehmen",
"text": "Beschreiben",
"analyzing": "Analysiere...",
"noMeals": "Noch keine Mahlzeiten",
"breakfast": "Frühstück",
"lunch": "Mittagessen",
"dinner": "Abendessen",
"snack": "Snack"
},
"nutrition": {
"calories": "Kalorien",
"protein": "Protein",
"carbs": "Kohlenhydrate",
"fat": "Fett",
"fiber": "Ballaststoffe",
"sugar": "Zucker",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Tagesziele",
"setGoals": "Ziele setzen",
"calories": "Kalorien-Ziel",
"protein": "Protein-Ziel",
"carbs": "Kohlenhydrate-Ziel",
"fat": "Fett-Ziel",
"progress": "Fortschritt"
},
"stats": {
"today": "Heute",
"week": "Diese Woche",
"remaining": "Verbleibend",
"consumed": "Verzehrt",
"average": "Durchschnitt"
},
"favorites": {
"add": "Zu Favoriten",
"remove": "Aus Favoriten entfernen",
"noFavorites": "Keine Favoriten",
"useAgain": "Erneut verwenden"
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"edit": "Bearbeiten",
"add": "Hinzufügen",
"close": "Schließen",
"search": "Suchen",
"error": "Fehler",
"success": "Erfolgreich",
"loading": "Laden..."
},
"errors": {
"loadMeals": "Mahlzeiten konnten nicht geladen werden",
"analyzeFailed": "Analyse fehlgeschlagen",
"saveFailed": "Speichern fehlgeschlagen",
"loadGoals": "Ziele konnten nicht geladen werden"
},
"success": {
"mealAdded": "Mahlzeit hinzugefügt",
"mealDeleted": "Mahlzeit gelöscht",
"goalsSaved": "Ziele gespeichert",
"favoriteAdded": "Zu Favoriten hinzugefügt"
},
"home": {
"page_title_html": "Food - Mana",
"heading_today": "Heute",
"action_history": "Verlauf",
"action_meal": "Mahlzeit",
"section_today_meals": "Heutige Mahlzeiten",
"entries_count": "{n} Einträge",
"empty_no_meals": "Noch keine Mahlzeiten",
"empty_hint": "Trage deine erste Mahlzeit ein.",
"action_add_meal": "Mahlzeit hinzufügen",
"macro_protein": "{n}g Protein",
"macro_carbs": "{n}g Carbs",
"macro_fat": "{n}g Fett",
"link_goals": "Ziele"
},
"detail": {
"page_title_html": "{description} - Food - Mana",
"untitled_fallback": "Mahlzeit",
"back": "Zurück",
"not_found": "Mahlzeit nicht gefunden.",
"lightbox_open_aria": "Bild vergrößern",
"lightbox_close_aria": "Bild schließen",
"section_foods": "Erkannte Bestandteile",
"section_nutrients": "Nährwerte",
"label_meal_type": "Mahlzeittyp",
"label_description": "Beschreibung",
"label_calories_kcal": "Kalorien (kcal)",
"label_protein_g": "Protein (g)",
"label_carbs_g": "Kohlenhydrate (g)",
"label_fat_g": "Fett (g)",
"label_fiber_g": "Ballaststoffe (g)",
"label_sugar_g": "Zucker (g)",
"fiber_with_value": "Ballaststoffe: {n}g",
"sugar_with_value": "Zucker: {n}g",
"action_reanalyze": "🔄 Erneut analysieren",
"action_reanalyzing": "Analysiere…",
"action_saving": "Speichere…",
"confirm_sure": "Sicher?",
"error_description_required": "Beschreibung darf nicht leer sein",
"error_save_failed": "Speichern fehlgeschlagen",
"error_analyze_failed": "KI-Analyse fehlgeschlagen",
"error_delete_failed": "Löschen fehlgeschlagen"
}
}

View file

@ -1,127 +0,0 @@
{
"app": {
"name": "Food",
"loading": "Loading...",
"tagline": "Understand nutrition"
},
"nav": {
"dashboard": "Dashboard",
"meals": "Meals",
"goals": "Goals",
"favorites": "Favorites",
"stats": "Statistics",
"settings": "Settings"
},
"meal": {
"add": "Add meal",
"edit": "Edit meal",
"delete": "Delete meal",
"photo": "Take photo",
"text": "Describe",
"analyzing": "Analyzing...",
"noMeals": "No meals yet",
"breakfast": "Breakfast",
"lunch": "Lunch",
"dinner": "Dinner",
"snack": "Snack"
},
"nutrition": {
"calories": "Calories",
"protein": "Protein",
"carbs": "Carbohydrates",
"fat": "Fat",
"fiber": "Fiber",
"sugar": "Sugar",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Daily goals",
"setGoals": "Set goals",
"calories": "Calorie goal",
"protein": "Protein goal",
"carbs": "Carbohydrate goal",
"fat": "Fat goal",
"progress": "Progress"
},
"stats": {
"today": "Today",
"week": "This week",
"remaining": "Remaining",
"consumed": "Consumed",
"average": "Average"
},
"favorites": {
"add": "Add to favorites",
"remove": "Remove from favorites",
"noFavorites": "No favorites",
"useAgain": "Use again"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"close": "Close",
"search": "Search",
"error": "Error",
"success": "Success",
"loading": "Loading..."
},
"errors": {
"loadMeals": "Failed to load meals",
"analyzeFailed": "Analysis failed",
"saveFailed": "Failed to save",
"loadGoals": "Failed to load goals"
},
"success": {
"mealAdded": "Meal added",
"mealDeleted": "Meal deleted",
"goalsSaved": "Goals saved",
"favoriteAdded": "Added to favorites"
},
"home": {
"page_title_html": "Food - Mana",
"heading_today": "Today",
"action_history": "History",
"action_meal": "Meal",
"section_today_meals": "Today's meals",
"entries_count": "{n} entries",
"empty_no_meals": "No meals yet",
"empty_hint": "Log your first meal.",
"action_add_meal": "Add meal",
"macro_protein": "{n}g protein",
"macro_carbs": "{n}g carbs",
"macro_fat": "{n}g fat",
"link_goals": "Goals"
},
"detail": {
"page_title_html": "{description} - Food - Mana",
"untitled_fallback": "Meal",
"back": "Back",
"not_found": "Meal not found.",
"lightbox_open_aria": "Enlarge image",
"lightbox_close_aria": "Close image",
"section_foods": "Detected items",
"section_nutrients": "Nutrients",
"label_meal_type": "Meal type",
"label_description": "Description",
"label_calories_kcal": "Calories (kcal)",
"label_protein_g": "Protein (g)",
"label_carbs_g": "Carbohydrates (g)",
"label_fat_g": "Fat (g)",
"label_fiber_g": "Fiber (g)",
"label_sugar_g": "Sugar (g)",
"fiber_with_value": "Fiber: {n}g",
"sugar_with_value": "Sugar: {n}g",
"action_reanalyze": "🔄 Re-analyze",
"action_reanalyzing": "Analyzing…",
"action_saving": "Saving…",
"confirm_sure": "Sure?",
"error_description_required": "Description cannot be empty",
"error_save_failed": "Save failed",
"error_analyze_failed": "AI analysis failed",
"error_delete_failed": "Delete failed"
}
}

View file

@ -1,127 +0,0 @@
{
"app": {
"name": "Food",
"loading": "Cargando...",
"tagline": "Entiende la nutrición"
},
"nav": {
"dashboard": "Panel",
"meals": "Comidas",
"goals": "Objetivos",
"favorites": "Favoritos",
"stats": "Estadísticas",
"settings": "Ajustes"
},
"meal": {
"add": "Añadir comida",
"edit": "Editar comida",
"delete": "Eliminar comida",
"photo": "Tomar foto",
"text": "Describir",
"analyzing": "Analizando...",
"noMeals": "Aún no hay comidas",
"breakfast": "Desayuno",
"lunch": "Almuerzo",
"dinner": "Cena",
"snack": "Snack"
},
"nutrition": {
"calories": "Calorías",
"protein": "Proteínas",
"carbs": "Carbohidratos",
"fat": "Grasas",
"fiber": "Fibra",
"sugar": "Azúcar",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Objetivos diarios",
"setGoals": "Establecer objetivos",
"calories": "Objetivo de calorías",
"protein": "Objetivo de proteínas",
"carbs": "Objetivo de carbohidratos",
"fat": "Objetivo de grasas",
"progress": "Progreso"
},
"stats": {
"today": "Hoy",
"week": "Esta semana",
"remaining": "Restante",
"consumed": "Consumido",
"average": "Promedio"
},
"favorites": {
"add": "Añadir a favoritos",
"remove": "Quitar de favoritos",
"noFavorites": "Sin favoritos",
"useAgain": "Usar de nuevo"
},
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"edit": "Editar",
"add": "Añadir",
"close": "Cerrar",
"search": "Buscar",
"error": "Error",
"success": "Éxito",
"loading": "Cargando..."
},
"errors": {
"loadMeals": "No se pudieron cargar las comidas",
"analyzeFailed": "El análisis falló",
"saveFailed": "No se pudo guardar",
"loadGoals": "No se pudieron cargar los objetivos"
},
"success": {
"mealAdded": "Comida añadida",
"mealDeleted": "Comida eliminada",
"goalsSaved": "Objetivos guardados",
"favoriteAdded": "Añadido a favoritos"
},
"home": {
"page_title_html": "Food - Mana",
"heading_today": "Hoy",
"action_history": "Historial",
"action_meal": "Comida",
"section_today_meals": "Comidas de hoy",
"entries_count": "{n} entradas",
"empty_no_meals": "Sin comidas",
"empty_hint": "Registra tu primera comida.",
"action_add_meal": "Añadir comida",
"macro_protein": "{n}g proteína",
"macro_carbs": "{n}g carbs",
"macro_fat": "{n}g grasa",
"link_goals": "Objetivos"
},
"detail": {
"page_title_html": "{description} - Food - Mana",
"untitled_fallback": "Comida",
"back": "Atrás",
"not_found": "Comida no encontrada.",
"lightbox_open_aria": "Ampliar imagen",
"lightbox_close_aria": "Cerrar imagen",
"section_foods": "Componentes detectados",
"section_nutrients": "Nutrientes",
"label_meal_type": "Tipo de comida",
"label_description": "Descripción",
"label_calories_kcal": "Calorías (kcal)",
"label_protein_g": "Proteína (g)",
"label_carbs_g": "Carbohidratos (g)",
"label_fat_g": "Grasa (g)",
"label_fiber_g": "Fibra (g)",
"label_sugar_g": "Azúcar (g)",
"fiber_with_value": "Fibra: {n}g",
"sugar_with_value": "Azúcar: {n}g",
"action_reanalyze": "🔄 Reanalizar",
"action_reanalyzing": "Analizando…",
"action_saving": "Guardando…",
"confirm_sure": "¿Seguro?",
"error_description_required": "La descripción no puede estar vacía",
"error_save_failed": "Error al guardar",
"error_analyze_failed": "Error en el análisis IA",
"error_delete_failed": "Error al eliminar"
}
}

View file

@ -1,127 +0,0 @@
{
"app": {
"name": "Food",
"loading": "Chargement...",
"tagline": "Comprendre la nutrition"
},
"nav": {
"dashboard": "Tableau de bord",
"meals": "Repas",
"goals": "Objectifs",
"favorites": "Favoris",
"stats": "Statistiques",
"settings": "Paramètres"
},
"meal": {
"add": "Ajouter un repas",
"edit": "Modifier le repas",
"delete": "Supprimer le repas",
"photo": "Prendre une photo",
"text": "Décrire",
"analyzing": "Analyse en cours...",
"noMeals": "Pas encore de repas",
"breakfast": "Petit-déjeuner",
"lunch": "Déjeuner",
"dinner": "Dîner",
"snack": "En-cas"
},
"nutrition": {
"calories": "Calories",
"protein": "Protéines",
"carbs": "Glucides",
"fat": "Lipides",
"fiber": "Fibres",
"sugar": "Sucre",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Objectifs quotidiens",
"setGoals": "Définir les objectifs",
"calories": "Objectif calorique",
"protein": "Objectif protéines",
"carbs": "Objectif glucides",
"fat": "Objectif lipides",
"progress": "Progression"
},
"stats": {
"today": "Aujourd'hui",
"week": "Cette semaine",
"remaining": "Restant",
"consumed": "Consommé",
"average": "Moyenne"
},
"favorites": {
"add": "Ajouter aux favoris",
"remove": "Retirer des favoris",
"noFavorites": "Pas de favoris",
"useAgain": "Réutiliser"
},
"common": {
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"add": "Ajouter",
"close": "Fermer",
"search": "Rechercher",
"error": "Erreur",
"success": "Succès",
"loading": "Chargement..."
},
"errors": {
"loadMeals": "Impossible de charger les repas",
"analyzeFailed": "L'analyse a échoué",
"saveFailed": "Impossible d'enregistrer",
"loadGoals": "Impossible de charger les objectifs"
},
"success": {
"mealAdded": "Repas ajouté",
"mealDeleted": "Repas supprimé",
"goalsSaved": "Objectifs enregistrés",
"favoriteAdded": "Ajouté aux favoris"
},
"home": {
"page_title_html": "Food - Mana",
"heading_today": "Aujourd'hui",
"action_history": "Historique",
"action_meal": "Repas",
"section_today_meals": "Repas du jour",
"entries_count": "{n} entrées",
"empty_no_meals": "Pas encore de repas",
"empty_hint": "Enregistre ton premier repas.",
"action_add_meal": "Ajouter un repas",
"macro_protein": "{n}g protéines",
"macro_carbs": "{n}g glucides",
"macro_fat": "{n}g lipides",
"link_goals": "Objectifs"
},
"detail": {
"page_title_html": "{description} - Food - Mana",
"untitled_fallback": "Repas",
"back": "Retour",
"not_found": "Repas introuvable.",
"lightbox_open_aria": "Agrandir l'image",
"lightbox_close_aria": "Fermer l'image",
"section_foods": "Ingrédients détectés",
"section_nutrients": "Nutriments",
"label_meal_type": "Type de repas",
"label_description": "Description",
"label_calories_kcal": "Calories (kcal)",
"label_protein_g": "Protéines (g)",
"label_carbs_g": "Glucides (g)",
"label_fat_g": "Lipides (g)",
"label_fiber_g": "Fibres (g)",
"label_sugar_g": "Sucres (g)",
"fiber_with_value": "Fibres : {n}g",
"sugar_with_value": "Sucres : {n}g",
"action_reanalyze": "🔄 Ré-analyser",
"action_reanalyzing": "Analyse…",
"action_saving": "Enregistrement…",
"confirm_sure": "Sûr ?",
"error_description_required": "La description ne peut pas être vide",
"error_save_failed": "Échec de l'enregistrement",
"error_analyze_failed": "Échec de l'analyse IA",
"error_delete_failed": "Échec de la suppression"
}
}

View file

@ -1,127 +0,0 @@
{
"app": {
"name": "Food",
"loading": "Caricamento...",
"tagline": "Comprendi la nutrizione"
},
"nav": {
"dashboard": "Dashboard",
"meals": "Pasti",
"goals": "Obiettivi",
"favorites": "Preferiti",
"stats": "Statistiche",
"settings": "Impostazioni"
},
"meal": {
"add": "Aggiungi pasto",
"edit": "Modifica pasto",
"delete": "Elimina pasto",
"photo": "Scatta foto",
"text": "Descrivi",
"analyzing": "Analisi in corso...",
"noMeals": "Nessun pasto ancora",
"breakfast": "Colazione",
"lunch": "Pranzo",
"dinner": "Cena",
"snack": "Spuntino"
},
"nutrition": {
"calories": "Calorie",
"protein": "Proteine",
"carbs": "Carboidrati",
"fat": "Grassi",
"fiber": "Fibre",
"sugar": "Zucchero",
"kcal": "kcal",
"grams": "g"
},
"goals": {
"daily": "Obiettivi giornalieri",
"setGoals": "Imposta obiettivi",
"calories": "Obiettivo calorie",
"protein": "Obiettivo proteine",
"carbs": "Obiettivo carboidrati",
"fat": "Obiettivo grassi",
"progress": "Progresso"
},
"stats": {
"today": "Oggi",
"week": "Questa settimana",
"remaining": "Rimanente",
"consumed": "Consumato",
"average": "Media"
},
"favorites": {
"add": "Aggiungi ai preferiti",
"remove": "Rimuovi dai preferiti",
"noFavorites": "Nessun preferito",
"useAgain": "Usa di nuovo"
},
"common": {
"save": "Salva",
"cancel": "Annulla",
"delete": "Elimina",
"edit": "Modifica",
"add": "Aggiungi",
"close": "Chiudi",
"search": "Cerca",
"error": "Errore",
"success": "Successo",
"loading": "Caricamento..."
},
"errors": {
"loadMeals": "Impossibile caricare i pasti",
"analyzeFailed": "Analisi fallita",
"saveFailed": "Salvataggio fallito",
"loadGoals": "Impossibile caricare gli obiettivi"
},
"success": {
"mealAdded": "Pasto aggiunto",
"mealDeleted": "Pasto eliminato",
"goalsSaved": "Obiettivi salvati",
"favoriteAdded": "Aggiunto ai preferiti"
},
"home": {
"page_title_html": "Food - Mana",
"heading_today": "Oggi",
"action_history": "Storico",
"action_meal": "Pasto",
"section_today_meals": "Pasti di oggi",
"entries_count": "{n} voci",
"empty_no_meals": "Nessun pasto",
"empty_hint": "Registra il tuo primo pasto.",
"action_add_meal": "Aggiungi pasto",
"macro_protein": "{n}g proteine",
"macro_carbs": "{n}g carboidrati",
"macro_fat": "{n}g grassi",
"link_goals": "Obiettivi"
},
"detail": {
"page_title_html": "{description} - Food - Mana",
"untitled_fallback": "Pasto",
"back": "Indietro",
"not_found": "Pasto non trovato.",
"lightbox_open_aria": "Ingrandisci immagine",
"lightbox_close_aria": "Chiudi immagine",
"section_foods": "Componenti rilevati",
"section_nutrients": "Nutrienti",
"label_meal_type": "Tipo di pasto",
"label_description": "Descrizione",
"label_calories_kcal": "Calorie (kcal)",
"label_protein_g": "Proteine (g)",
"label_carbs_g": "Carboidrati (g)",
"label_fat_g": "Grassi (g)",
"label_fiber_g": "Fibre (g)",
"label_sugar_g": "Zuccheri (g)",
"fiber_with_value": "Fibre: {n}g",
"sugar_with_value": "Zuccheri: {n}g",
"action_reanalyze": "🔄 Rianalizza",
"action_reanalyzing": "Analisi…",
"action_saving": "Salvataggio…",
"confirm_sure": "Sicuro?",
"error_description_required": "La descrizione non può essere vuota",
"error_save_failed": "Salvataggio non riuscito",
"error_analyze_failed": "Analisi AI non riuscita",
"error_delete_failed": "Eliminazione non riuscita"
}
}

View file

@ -1,91 +0,0 @@
{
"app": {
"name": "Moodlit",
"tagline": "Ambient Lighting & Moods"
},
"nav": {
"home": "Startseite",
"moods": "Moods",
"sequences": "Sequenzen",
"settings": "Einstellungen",
"feedback": "Feedback"
},
"home": {
"title": "Deine Moods",
"subtitle": "Wähle eine Lichtstimmung",
"sequences": "Sequenzen",
"sequencesDescription": "Verkette mehrere Moods zu einer Sequenz",
"favorites": "Favoriten",
"all": "Alle Moods",
"custom": "Eigene Moods"
},
"sequences": {
"title": "Sequenzen",
"subtitle": "Spiele mehrere Moods nacheinander ab",
"moods": "Moods",
"empty": "Noch keine Sequenzen",
"emptyDescription": "Erstelle eine Sequenz, indem du mehrere Moods verkettest."
},
"mood": {
"play": "Abspielen",
"pause": "Pause",
"edit": "Bearbeiten",
"delete": "Löschen",
"addToFavorites": "Zu Favoriten",
"removeFromFavorites": "Aus Favoriten",
"animation": "Animation",
"colors": "Farben",
"startTimer": "Start",
"stopTimer": "Timer stoppen",
"timerRunning": "Timer läuft",
"stop": "Stopp"
},
"settings": {
"title": "Einstellungen",
"animationSpeed": "Animationsgeschwindigkeit",
"slow": "Langsam",
"normal": "Normal",
"fast": "Schnell",
"brightness": "Helligkeit",
"autoTimer": "Auto-Timer",
"autoTimerOff": "Aus",
"autoTimerMinutes": "{minutes} Minuten",
"autoMoodSwitch": "Auto-Mood-Wechsel",
"autoMoodSwitchInterval": "Wechsel-Intervall",
"reset": "Zurücksetzen",
"resetConfirm": "Alle Einstellungen zurücksetzen?"
},
"createMood": {
"title": "Mood erstellen",
"editTitle": "Mood bearbeiten",
"name": "Name",
"namePlaceholder": "Mood-Name eingeben...",
"colors": "Farben",
"addColor": "Farbe hinzufügen",
"animation": "Animationstyp",
"preview": "Vorschau"
},
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"confirm": "Bestätigen",
"loading": "Lädt...",
"error": "Fehler",
"success": "Erfolgreich",
"create": "Erstellen"
},
"moodsPage": {
"page_title_html": "Moods - Moodlit - Mana",
"title": "Moods",
"action_close": "Schliessen",
"action_new_mood": "+ Neues Mood",
"label_name": "Name",
"placeholder_name": "Mein Mood",
"label_animation": "Animation",
"label_colors": "Farben",
"toast_created": "\"{name}\" erstellt",
"toast_default_protected": "Standard-Moods können nicht gelöscht werden",
"toast_deleted": "Gelöscht"
}
}

View file

@ -1,91 +0,0 @@
{
"app": {
"name": "Moodlit",
"tagline": "Ambient Lighting & Moods"
},
"nav": {
"home": "Home",
"moods": "Moods",
"sequences": "Sequences",
"settings": "Settings",
"feedback": "Feedback"
},
"home": {
"title": "Your Moods",
"subtitle": "Choose a lighting mood",
"sequences": "Sequences",
"sequencesDescription": "Chain multiple moods into a sequence",
"favorites": "Favorites",
"all": "All Moods",
"custom": "Custom Moods"
},
"sequences": {
"title": "Sequences",
"subtitle": "Play multiple moods in sequence",
"moods": "moods",
"empty": "No Sequences Yet",
"emptyDescription": "Create a sequence by chaining multiple moods together."
},
"mood": {
"play": "Play",
"pause": "Pause",
"edit": "Edit",
"delete": "Delete",
"addToFavorites": "Add to Favorites",
"removeFromFavorites": "Remove from Favorites",
"animation": "Animation",
"colors": "Colors",
"startTimer": "Start",
"stopTimer": "Stop Timer",
"timerRunning": "Timer running",
"stop": "Stop"
},
"settings": {
"title": "Settings",
"animationSpeed": "Animation Speed",
"slow": "Slow",
"normal": "Normal",
"fast": "Fast",
"brightness": "Brightness",
"autoTimer": "Auto Timer",
"autoTimerOff": "Off",
"autoTimerMinutes": "{minutes} minutes",
"autoMoodSwitch": "Auto Mood Switch",
"autoMoodSwitchInterval": "Switch Interval",
"reset": "Reset",
"resetConfirm": "Reset all settings?"
},
"createMood": {
"title": "Create Mood",
"editTitle": "Edit Mood",
"name": "Name",
"namePlaceholder": "Enter mood name...",
"colors": "Colors",
"addColor": "Add Color",
"animation": "Animation Type",
"preview": "Preview"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"create": "Create"
},
"moodsPage": {
"page_title_html": "Moods - Moodlit - Mana",
"title": "Moods",
"action_close": "Close",
"action_new_mood": "+ New mood",
"label_name": "Name",
"placeholder_name": "My mood",
"label_animation": "Animation",
"label_colors": "Colors",
"toast_created": "\"{name}\" created",
"toast_default_protected": "Default moods can't be deleted",
"toast_deleted": "Deleted"
}
}

View file

@ -1,91 +0,0 @@
{
"app": {
"name": "Moodlit",
"tagline": "Iluminación ambiental y estados de ánimo"
},
"nav": {
"home": "Inicio",
"moods": "Moods",
"sequences": "Secuencias",
"settings": "Ajustes",
"feedback": "Feedback"
},
"home": {
"title": "Tus Moods",
"subtitle": "Elige una iluminación ambiental",
"sequences": "Secuencias",
"sequencesDescription": "Encadena varios moods en una secuencia",
"favorites": "Favoritos",
"all": "Todos los Moods",
"custom": "Moods personalizados"
},
"sequences": {
"title": "Secuencias",
"subtitle": "Reproduce varios moods en secuencia",
"moods": "moods",
"empty": "Aún no hay secuencias",
"emptyDescription": "Crea una secuencia encadenando varios moods."
},
"mood": {
"play": "Reproducir",
"pause": "Pausar",
"edit": "Editar",
"delete": "Eliminar",
"addToFavorites": "Añadir a favoritos",
"removeFromFavorites": "Quitar de favoritos",
"animation": "Animación",
"colors": "Colores",
"startTimer": "Iniciar",
"stopTimer": "Detener temporizador",
"timerRunning": "Temporizador activo",
"stop": "Detener"
},
"settings": {
"title": "Ajustes",
"animationSpeed": "Velocidad de animación",
"slow": "Lenta",
"normal": "Normal",
"fast": "Rápida",
"brightness": "Brillo",
"autoTimer": "Temporizador automático",
"autoTimerOff": "Desactivado",
"autoTimerMinutes": "{minutes} minutos",
"autoMoodSwitch": "Cambio automático de mood",
"autoMoodSwitchInterval": "Intervalo de cambio",
"reset": "Restablecer",
"resetConfirm": "¿Restablecer todos los ajustes?"
},
"createMood": {
"title": "Crear Mood",
"editTitle": "Editar Mood",
"name": "Nombre",
"namePlaceholder": "Ingresa un nombre...",
"colors": "Colores",
"addColor": "Añadir color",
"animation": "Tipo de animación",
"preview": "Vista previa"
},
"common": {
"save": "Guardar",
"cancel": "Cancelar",
"delete": "Eliminar",
"confirm": "Confirmar",
"loading": "Cargando...",
"error": "Error",
"success": "Éxito",
"create": "Crear"
},
"moodsPage": {
"page_title_html": "Moods - Moodlit - Mana",
"title": "Moods",
"action_close": "Cerrar",
"action_new_mood": "+ Nuevo mood",
"label_name": "Nombre",
"placeholder_name": "Mi mood",
"label_animation": "Animación",
"label_colors": "Colores",
"toast_created": "«{name}» creado",
"toast_default_protected": "Los moods predeterminados no se pueden eliminar",
"toast_deleted": "Eliminado"
}
}

View file

@ -1,91 +0,0 @@
{
"app": {
"name": "Moodlit",
"tagline": "Éclairage ambiant et ambiances"
},
"nav": {
"home": "Accueil",
"moods": "Ambiances",
"sequences": "Séquences",
"settings": "Paramètres",
"feedback": "Feedback"
},
"home": {
"title": "Vos ambiances",
"subtitle": "Choisissez une ambiance lumineuse",
"sequences": "Séquences",
"sequencesDescription": "Enchaînez plusieurs ambiances en une séquence",
"favorites": "Favoris",
"all": "Toutes les ambiances",
"custom": "Ambiances personnalisées"
},
"sequences": {
"title": "Séquences",
"subtitle": "Jouez plusieurs ambiances à la suite",
"moods": "ambiances",
"empty": "Pas encore de séquences",
"emptyDescription": "Créez une séquence en enchaînant plusieurs ambiances."
},
"mood": {
"play": "Lire",
"pause": "Pause",
"edit": "Modifier",
"delete": "Supprimer",
"addToFavorites": "Ajouter aux favoris",
"removeFromFavorites": "Retirer des favoris",
"animation": "Animation",
"colors": "Couleurs",
"startTimer": "Démarrer",
"stopTimer": "Arrêter le minuteur",
"timerRunning": "Minuteur en cours",
"stop": "Arrêter"
},
"settings": {
"title": "Paramètres",
"animationSpeed": "Vitesse d'animation",
"slow": "Lente",
"normal": "Normale",
"fast": "Rapide",
"brightness": "Luminosité",
"autoTimer": "Minuteur automatique",
"autoTimerOff": "Désactivé",
"autoTimerMinutes": "{minutes} minutes",
"autoMoodSwitch": "Changement automatique d'ambiance",
"autoMoodSwitchInterval": "Intervalle de changement",
"reset": "Réinitialiser",
"resetConfirm": "Réinitialiser tous les paramètres ?"
},
"createMood": {
"title": "Créer une ambiance",
"editTitle": "Modifier l'ambiance",
"name": "Nom",
"namePlaceholder": "Saisir un nom...",
"colors": "Couleurs",
"addColor": "Ajouter une couleur",
"animation": "Type d'animation",
"preview": "Aperçu"
},
"common": {
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"confirm": "Confirmer",
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès",
"create": "Créer"
},
"moodsPage": {
"page_title_html": "Moods - Moodlit - Mana",
"title": "Moods",
"action_close": "Fermer",
"action_new_mood": "+ Nouveau mood",
"label_name": "Nom",
"placeholder_name": "Mon mood",
"label_animation": "Animation",
"label_colors": "Couleurs",
"toast_created": "« {name} » créé",
"toast_default_protected": "Les moods par défaut ne peuvent pas être supprimés",
"toast_deleted": "Supprimé"
}
}

View file

@ -1,91 +0,0 @@
{
"app": {
"name": "Moodlit",
"tagline": "Illuminazione ambientale e atmosfere"
},
"nav": {
"home": "Home",
"moods": "Atmosfere",
"sequences": "Sequenze",
"settings": "Impostazioni",
"feedback": "Feedback"
},
"home": {
"title": "Le tue atmosfere",
"subtitle": "Scegli un'atmosfera luminosa",
"sequences": "Sequenze",
"sequencesDescription": "Concatena più atmosfere in una sequenza",
"favorites": "Preferiti",
"all": "Tutte le atmosfere",
"custom": "Atmosfere personalizzate"
},
"sequences": {
"title": "Sequenze",
"subtitle": "Riproduci più atmosfere in sequenza",
"moods": "atmosfere",
"empty": "Nessuna sequenza ancora",
"emptyDescription": "Crea una sequenza concatenando più atmosfere."
},
"mood": {
"play": "Riproduci",
"pause": "Pausa",
"edit": "Modifica",
"delete": "Elimina",
"addToFavorites": "Aggiungi ai preferiti",
"removeFromFavorites": "Rimuovi dai preferiti",
"animation": "Animazione",
"colors": "Colori",
"startTimer": "Avvia",
"stopTimer": "Ferma timer",
"timerRunning": "Timer in corso",
"stop": "Ferma"
},
"settings": {
"title": "Impostazioni",
"animationSpeed": "Velocità animazione",
"slow": "Lenta",
"normal": "Normale",
"fast": "Veloce",
"brightness": "Luminosità",
"autoTimer": "Timer automatico",
"autoTimerOff": "Disattivato",
"autoTimerMinutes": "{minutes} minuti",
"autoMoodSwitch": "Cambio automatico atmosfera",
"autoMoodSwitchInterval": "Intervallo di cambio",
"reset": "Ripristina",
"resetConfirm": "Ripristinare tutte le impostazioni?"
},
"createMood": {
"title": "Crea atmosfera",
"editTitle": "Modifica atmosfera",
"name": "Nome",
"namePlaceholder": "Inserisci un nome...",
"colors": "Colori",
"addColor": "Aggiungi colore",
"animation": "Tipo di animazione",
"preview": "Anteprima"
},
"common": {
"save": "Salva",
"cancel": "Annulla",
"delete": "Elimina",
"confirm": "Conferma",
"loading": "Caricamento...",
"error": "Errore",
"success": "Successo",
"create": "Crea"
},
"moodsPage": {
"page_title_html": "Moods - Moodlit - Mana",
"title": "Moods",
"action_close": "Chiudi",
"action_new_mood": "+ Nuovo mood",
"label_name": "Nome",
"placeholder_name": "Il mio mood",
"label_animation": "Animazione",
"label_colors": "Colori",
"toast_created": "«{name}» creato",
"toast_default_protected": "I mood predefiniti non possono essere eliminati",
"toast_deleted": "Eliminato"
}
}

View file

@ -2,7 +2,6 @@
"categories": {
"home": "Zuhause",
"work": "Arbeit",
"food": "Essen",
"shopping": "Einkauf",
"transit": "Transit",
"leisure": "Freizeit",

View file

@ -2,7 +2,6 @@
"categories": {
"home": "Home",
"work": "Work",
"food": "Food",
"shopping": "Shopping",
"transit": "Transit",
"leisure": "Leisure",

View file

@ -2,7 +2,6 @@
"categories": {
"home": "Casa",
"work": "Trabajo",
"food": "Comida",
"shopping": "Compras",
"transit": "Tránsito",
"leisure": "Ocio",

View file

@ -2,7 +2,6 @@
"categories": {
"home": "Maison",
"work": "Travail",
"food": "Repas",
"shopping": "Achats",
"transit": "Transit",
"leisure": "Loisirs",

View file

@ -2,7 +2,6 @@
"categories": {
"home": "Casa",
"work": "Lavoro",
"food": "Cibo",
"shopping": "Shopping",
"transit": "Transito",
"leisure": "Tempo libero",

View file

@ -1,257 +0,0 @@
{
"categories": {
"all": "Alle",
"top": "Oberteile",
"bottom": "Hosen",
"dress": "Kleider",
"outerwear": "Jacken",
"shoes": "Schuhe",
"bag": "Taschen",
"accessory": "Accessoires",
"glasses": "Brillen",
"jewelry": "Schmuck",
"hat": "Kopfbedeckung",
"other": "Sonstiges"
},
"categories_singular": {
"top": "Oberteil",
"bottom": "Hose",
"dress": "Kleid",
"outerwear": "Jacke",
"shoes": "Schuh",
"bag": "Tasche",
"accessory": "Accessoire",
"glasses": "Brille",
"jewelry": "Schmuck",
"hat": "Kopfbedeckung",
"other": "Item"
},
"occasions": {
"casual": "Casual",
"work": "Arbeit",
"formal": "Festlich",
"workout": "Sport",
"date": "Date",
"travel": "Reise",
"event": "Event",
"sleep": "Schlafanzug",
"other": "Sonstiges"
},
"seasons": {
"spring": "Frühling",
"summer": "Sommer",
"autumn": "Herbst",
"winter": "Winter"
},
"piece_singular": "Stück",
"piece_plural": "Stücke",
"upload_failed": "Upload fehlgeschlagen",
"list_view": {
"aria_tabs": "Ansicht wechseln",
"tab_garments": "Kleidung",
"tab_outfits": "Outfits",
"face_saved_title": "Gesichtsbild gespeichert",
"face_saved_hint": "Perfekt — als nächstes lädst du unten dein erstes Kleidungsstück hoch.",
"dismiss": "Schließen",
"face_prompt_title": "Lade ein Gesichtsbild hoch",
"face_prompt_desc": "Wir brauchen dich auf Bild, damit Try-On Kleidung an dir visualisieren kann. Das Bild bleibt lokal und wird nur für deine eigenen Generierungen genutzt.",
"face_uploading_label": "Wird hochgeladen…",
"face_upload_label": "Gesichtsbild hochladen",
"face_upload_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
"face_uploading_chip": "Lade…"
},
"grid_view": {
"upload_label_all": "Kleidungsstück hochladen",
"upload_label_for_category": "{category} hochladen",
"upload_hint": "Foto frontal, heller Hintergrund — bessere Try-On-Ergebnisse",
"empty_title": "Noch nichts im Schrank.",
"empty_hint": "Zieh ein Foto in die Zone oben — oder klick sie an, um eins auszuwählen.",
"no_entries_under": "Keine Einträge unter {category}.",
"space_footer": "Dieser Schrank gehört zu {name} — Uploads landen nur hier, nicht in deinem persönlichen Schrank."
},
"outfits_view": {
"title": "Outfits",
"count_singular": "Zusammenstellung",
"count_plural": "Zusammenstellungen",
"action_new": "Neues Outfit",
"empty_title": "Noch keine Outfits.",
"empty_no_garments": "Füge zuerst ein paar Kleidungsstücke im Tab \"Kleidung\" hinzu — danach lassen sie sich hier zu Outfits kombinieren.",
"empty_with_garments": "Kombiniere deine Kleidungsstücke zu Looks, die du dann mit KI an dir selbst anprobieren kannst.",
"action_compose_first": "Erstes Outfit komponieren"
},
"detail_garment": {
"loading": "Lädt…",
"not_found_title": "Nicht gefunden.",
"not_found_desc": "Das Kleidungsstück wurde gelöscht oder gehört zu einem anderen Space.",
"action_enlarge": "Foto vergrößern",
"action_edit": "Bearbeiten",
"label_brand": "Marke",
"label_color": "Farbe",
"label_size": "Größe",
"label_material": "Material",
"label_price": "Preis",
"label_wear_count": "Getragen",
"wear_count_value": "{count}×",
"last_worn_suffix": " · zuletzt {date}",
"action_comic": "Als Comic-Character",
"action_comic_title": "Aus diesem Kleidungsstück einen Comic-Character generieren",
"action_marking": "Gespeichert…",
"action_mark_worn": "Heute getragen",
"action_unarchive": "Wieder aktiv setzen",
"action_archive": "Archivieren",
"action_delete": "Löschen",
"section_try_ons": "Anproben · {count}",
"section_outfits": "In Outfits · {count}",
"no_try_on_yet": "Noch keine Anprobe",
"action_open_picture": "In Picture öffnen",
"confirm_delete": "\"{name}\" wirklich löschen?"
},
"detail_outfit": {
"back": "Zurück zum Kleiderschrank",
"breadcrumb": "Kleiderschrank · Outfits",
"loading": "Lädt…",
"not_found_title": "Outfit nicht gefunden.",
"not_found_desc": "Gelöscht oder in einem anderen Space.",
"try_on_preview_alt": "Try-On Vorschau",
"no_garments": "Keine Kleidungsstücke",
"action_comic": "Als Comic-Character",
"action_comic_title": "Aus diesem Outfit einen Comic-Character generieren",
"try_on_history": "Try-On Verlauf",
"action_unfavorite": "Favorit entfernen",
"action_favorite": "Als Favorit markieren",
"action_edit": "Bearbeiten",
"label_visibility": "Sichtbarkeit",
"section_composition": "Zusammenstellung",
"composition_missing": "Referenzierte Kleidungsstücke wurden entfernt oder gehören zu einem anderen Space.",
"action_unarchive": "Wieder aktiv",
"action_archive": "Archivieren",
"action_delete": "Löschen",
"confirm_delete": "Outfit \"{name}\" wirklich löschen?"
},
"garment_form": {
"err_name_required": "Name darf nicht leer sein",
"err_save_failed": "Speichern fehlgeschlagen",
"label_name": "Name",
"placeholder_name": "z.B. Blau-weiß gestreiftes Hemd",
"label_category": "Kategorie",
"label_brand": "Marke",
"placeholder_brand": "z.B. Uniqlo",
"label_color": "Farbe",
"placeholder_color": "z.B. navy",
"label_size": "Größe",
"placeholder_size": "z.B. M oder 42",
"label_material": "Material",
"placeholder_material": "z.B. Baumwolle",
"label_tags": "Tags",
"tags_hint": "(komma-getrennt)",
"placeholder_tags": "formal, sommer, lieblingsstück",
"label_price": "Preis",
"aria_currency": "Währung",
"label_notes": "Notizen",
"placeholder_notes": "Anlass, Tragevorschriften, …",
"action_saving": "Speichere…",
"action_save": "Speichern",
"action_cancel": "Abbrechen"
},
"composer": {
"err_name_required": "Gib dem Outfit einen Namen.",
"err_no_garments": "Wähle mindestens ein Kleidungsstück aus.",
"err_save_failed": "Speichern fehlgeschlagen",
"section_library": "Kleiderschrank",
"available_singular": "{count} Stück verfügbar",
"available_plural": "{count} Stücke verfügbar",
"empty_title": "Nichts zum Kombinieren.",
"empty_hint_prefix": "Lade zuerst ein paar Kleidungsstücke im Tab",
"tab_garments_link": "Kleidung",
"empty_hint_suffix": "hoch.",
"label_name": "Name",
"placeholder_name": "z.B. Bürooutfit Juni",
"label_description": "Beschreibung",
"placeholder_description": "Für welchen Anlass? Besonderheiten?",
"label_occasion": "Anlass",
"no_occasion": "— kein Anlass —",
"label_seasons": "Jahreszeit",
"label_tags": "Tags",
"tags_hint": "(komma-getrennt)",
"placeholder_tags": "minimal, layering, meeting",
"section_composition": "Zusammenstellung",
"composition_count_singular": "· {count} Stück",
"composition_count_plural": "· {count} Stücke",
"composition_empty": "Klicke links auf Kleidungsstücke, um sie dem Outfit hinzuzufügen.",
"action_remove": "Aus Outfit entfernen",
"action_saving": "Speichere…",
"action_save_edit": "Änderungen speichern",
"action_save_new": "Outfit anlegen",
"action_cancel": "Abbrechen"
},
"try_on_garment": {
"err_failed": "Try-On fehlgeschlagen",
"err_upload": "Upload fehlgeschlagen",
"no_photo": "Lade erst ein Foto hoch, um dieses Stück an dir zu visualisieren.",
"refs_title": "Für Solo-Try-On brauchen wir dich auf Bild.",
"refs_accessory": "Ein Gesichtsbild reicht — das Stück wird darauf montiert.",
"refs_full": "Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.",
"upload_face": "Gesichtsbild hochladen",
"upload_body": "Ganzkörperbild hochladen",
"face_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
"body_hint": "Stehend, freier Hintergrund, gut erkennbare Haltung",
"refs_more_prefix": "Weitere Referenzen oder AI-Opt-ins pro Bild:",
"refs_link": "Meine Bilder",
"rendering": "Rendere…",
"cta": "An mir anprobieren",
"credits": "{count} Credits",
"accessory_hint": "Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).",
"space_hint_prefix": "Try-On nutzt deine Referenzbilder aus diesem Space",
"space_hint_suffix": ", nicht aus Persönlich.",
"result_label": "Ergebnis",
"try_on_alt": "Try-On",
"result_hint_prefix": "Gefunden in der",
"picture_gallery_link": "Picture-Galerie",
"result_hint_suffix": "als normale Generierung."
},
"try_on_outfit": {
"err_failed": "Try-On fehlgeschlagen",
"err_upload": "Upload fehlgeschlagen",
"refs_title": "Für Try-On brauchen wir dich auf Bild.",
"refs_accessory": "Ein Gesichtsbild reicht — der Rest bleibt wie auf deinem Foto.",
"refs_full": "Ein Gesichts- und ein Ganzkörperbild. Beide werden nur für deine eigenen Generierungen genutzt.",
"upload_face": "Gesichtsbild hochladen",
"upload_body": "Ganzkörperbild hochladen",
"face_hint": "Kopf + Schulter, möglichst neutrale Beleuchtung",
"body_hint": "Stehend, freier Hintergrund, gut erkennbare Haltung",
"refs_more_prefix": "Weitere Referenzen oder AI-Opt-ins pro Bild:",
"refs_link": "Meine Bilder",
"rendering": "Rendere…",
"cta": "Anprobieren",
"credits": "{count} Credits",
"accessory_hint": "Accessoire-Modus — nur das Gesicht wird gerendert (spart Credits).",
"many_garments_hint": "Mit {count} Kleidungsstücken ist der Referenz-Slot knapp — ältere Items werden evtl. nicht mitgezogen.",
"space_hint_prefix": "Try-On nutzt deine Referenzbilder aus diesem Space",
"space_hint_suffix": ", nicht aus Persönlich.",
"family_hint": "Kinder-Outfits werden trotzdem auf dein Gesicht gerendert.",
"empty_garments": "Füge mindestens ein {category} hinzu, um Try-On zu aktivieren."
},
"model_picker": {
"legend": "Modell",
"option_openai_label": "OpenAI",
"option_openai_hint": "GPT-image · Standard",
"option_pro_label": "Nano Banana Pro",
"option_pro_hint": "Google · hohe Konsistenz",
"option_flash_label": "Nano Banana 2",
"option_flash_hint": "Google · neuestes · günstig"
},
"garment_card": {
"wear_count_title": "{count}× getragen"
},
"outfit_card": {
"try_on_badge": "Try-On",
"try_on_preview_title": "Try-On Vorschau",
"empty": "Leer",
"favorite": "Favorit"
},
"compose": {
"title_edit": "Outfit bearbeiten",
"title_new": "Neues Outfit",
"back": "Zurück"
}
}

View file

@ -1,257 +0,0 @@
{
"categories": {
"all": "All",
"top": "Tops",
"bottom": "Bottoms",
"dress": "Dresses",
"outerwear": "Jackets",
"shoes": "Shoes",
"bag": "Bags",
"accessory": "Accessories",
"glasses": "Glasses",
"jewelry": "Jewelry",
"hat": "Hats",
"other": "Other"
},
"categories_singular": {
"top": "Top",
"bottom": "Bottom",
"dress": "Dress",
"outerwear": "Jacket",
"shoes": "Shoe",
"bag": "Bag",
"accessory": "Accessory",
"glasses": "Glasses",
"jewelry": "Jewelry",
"hat": "Hat",
"other": "Item"
},
"occasions": {
"casual": "Casual",
"work": "Work",
"formal": "Formal",
"workout": "Workout",
"date": "Date",
"travel": "Travel",
"event": "Event",
"sleep": "Sleepwear",
"other": "Other"
},
"seasons": {
"spring": "Spring",
"summer": "Summer",
"autumn": "Autumn",
"winter": "Winter"
},
"piece_singular": "piece",
"piece_plural": "pieces",
"upload_failed": "Upload failed",
"list_view": {
"aria_tabs": "Switch view",
"tab_garments": "Clothing",
"tab_outfits": "Outfits",
"face_saved_title": "Face photo saved",
"face_saved_hint": "Perfect — next, upload your first piece of clothing below.",
"dismiss": "Close",
"face_prompt_title": "Upload a face photo",
"face_prompt_desc": "We need you in a photo so Try-On can visualize clothing on you. The image stays local and is only used for your own generations.",
"face_uploading_label": "Uploading…",
"face_upload_label": "Upload face photo",
"face_upload_hint": "Head + shoulders, neutral lighting if possible",
"face_uploading_chip": "Loading…"
},
"grid_view": {
"upload_label_all": "Upload garment",
"upload_label_for_category": "Upload {category}",
"upload_hint": "Front-on photo, bright background — better Try-On results",
"empty_title": "Nothing in the wardrobe yet.",
"empty_hint": "Drag a photo into the zone above — or click it to pick one.",
"no_entries_under": "No items under {category}.",
"space_footer": "This wardrobe belongs to {name} — uploads only land here, not in your personal wardrobe."
},
"outfits_view": {
"title": "Outfits",
"count_singular": "outfit",
"count_plural": "outfits",
"action_new": "New outfit",
"empty_title": "No outfits yet.",
"empty_no_garments": "First add a few pieces of clothing in the \"Clothing\" tab — then you can combine them into outfits here.",
"empty_with_garments": "Combine your clothing pieces into looks that you can then try on yourself with AI.",
"action_compose_first": "Compose first outfit"
},
"detail_garment": {
"loading": "Loading…",
"not_found_title": "Not found.",
"not_found_desc": "The garment was deleted or belongs to another space.",
"action_enlarge": "Enlarge photo",
"action_edit": "Edit",
"label_brand": "Brand",
"label_color": "Color",
"label_size": "Size",
"label_material": "Material",
"label_price": "Price",
"label_wear_count": "Worn",
"wear_count_value": "{count}×",
"last_worn_suffix": " · last on {date}",
"action_comic": "As comic character",
"action_comic_title": "Generate a comic character from this garment",
"action_marking": "Saving…",
"action_mark_worn": "Worn today",
"action_unarchive": "Set active again",
"action_archive": "Archive",
"action_delete": "Delete",
"section_try_ons": "Try-Ons · {count}",
"section_outfits": "In outfits · {count}",
"no_try_on_yet": "No try-on yet",
"action_open_picture": "Open in Picture",
"confirm_delete": "Really delete \"{name}\"?"
},
"detail_outfit": {
"back": "Back to wardrobe",
"breadcrumb": "Wardrobe · Outfits",
"loading": "Loading…",
"not_found_title": "Outfit not found.",
"not_found_desc": "Deleted or in another space.",
"try_on_preview_alt": "Try-On preview",
"no_garments": "No garments",
"action_comic": "As comic character",
"action_comic_title": "Generate a comic character from this outfit",
"try_on_history": "Try-On history",
"action_unfavorite": "Remove favorite",
"action_favorite": "Mark as favorite",
"action_edit": "Edit",
"label_visibility": "Visibility",
"section_composition": "Composition",
"composition_missing": "Referenced garments were removed or belong to another space.",
"action_unarchive": "Set active",
"action_archive": "Archive",
"action_delete": "Delete",
"confirm_delete": "Really delete outfit \"{name}\"?"
},
"garment_form": {
"err_name_required": "Name cannot be empty",
"err_save_failed": "Save failed",
"label_name": "Name",
"placeholder_name": "e.g. Blue and white striped shirt",
"label_category": "Category",
"label_brand": "Brand",
"placeholder_brand": "e.g. Uniqlo",
"label_color": "Color",
"placeholder_color": "e.g. navy",
"label_size": "Size",
"placeholder_size": "e.g. M or 42",
"label_material": "Material",
"placeholder_material": "e.g. cotton",
"label_tags": "Tags",
"tags_hint": "(comma-separated)",
"placeholder_tags": "formal, summer, favorite",
"label_price": "Price",
"aria_currency": "Currency",
"label_notes": "Notes",
"placeholder_notes": "Occasion, care instructions, …",
"action_saving": "Saving…",
"action_save": "Save",
"action_cancel": "Cancel"
},
"composer": {
"err_name_required": "Give the outfit a name.",
"err_no_garments": "Pick at least one garment.",
"err_save_failed": "Save failed",
"section_library": "Wardrobe",
"available_singular": "{count} piece available",
"available_plural": "{count} pieces available",
"empty_title": "Nothing to combine.",
"empty_hint_prefix": "First upload a few garments in the",
"tab_garments_link": "Clothing",
"empty_hint_suffix": "tab.",
"label_name": "Name",
"placeholder_name": "e.g. Office outfit June",
"label_description": "Description",
"placeholder_description": "For what occasion? Anything special?",
"label_occasion": "Occasion",
"no_occasion": "— no occasion —",
"label_seasons": "Season",
"label_tags": "Tags",
"tags_hint": "(comma-separated)",
"placeholder_tags": "minimal, layering, meeting",
"section_composition": "Composition",
"composition_count_singular": "· {count} piece",
"composition_count_plural": "· {count} pieces",
"composition_empty": "Click garments on the left to add them to the outfit.",
"action_remove": "Remove from outfit",
"action_saving": "Saving…",
"action_save_edit": "Save changes",
"action_save_new": "Create outfit",
"action_cancel": "Cancel"
},
"try_on_garment": {
"err_failed": "Try-On failed",
"err_upload": "Upload failed",
"no_photo": "Upload a photo first to visualize this piece on yourself.",
"refs_title": "We need you in a photo for solo try-on.",
"refs_accessory": "A face photo is enough — the piece is mounted onto it.",
"refs_full": "A face and a full-body photo. Both are only used for your own generations.",
"upload_face": "Upload face photo",
"upload_body": "Upload full-body photo",
"face_hint": "Head + shoulders, neutral lighting if possible",
"body_hint": "Standing, free background, posture clearly visible",
"refs_more_prefix": "More references or per-image AI opt-ins:",
"refs_link": "My Images",
"rendering": "Rendering…",
"cta": "Try on me",
"credits": "{count} credits",
"accessory_hint": "Accessory mode — only the face is rendered (saves credits).",
"space_hint_prefix": "Try-On uses your reference images from this space",
"space_hint_suffix": ", not from Personal.",
"result_label": "Result",
"try_on_alt": "Try-On",
"result_hint_prefix": "Found in the",
"picture_gallery_link": "Picture gallery",
"result_hint_suffix": "as a regular generation."
},
"try_on_outfit": {
"err_failed": "Try-On failed",
"err_upload": "Upload failed",
"refs_title": "We need you in a photo for try-on.",
"refs_accessory": "A face photo is enough — the rest stays as in your photo.",
"refs_full": "A face and a full-body photo. Both are only used for your own generations.",
"upload_face": "Upload face photo",
"upload_body": "Upload full-body photo",
"face_hint": "Head + shoulders, neutral lighting if possible",
"body_hint": "Standing, free background, posture clearly visible",
"refs_more_prefix": "More references or per-image AI opt-ins:",
"refs_link": "My Images",
"rendering": "Rendering…",
"cta": "Try on",
"credits": "{count} credits",
"accessory_hint": "Accessory mode — only the face is rendered (saves credits).",
"many_garments_hint": "With {count} garments the reference slot is tight — older items may not be carried over.",
"space_hint_prefix": "Try-On uses your reference images from this space",
"space_hint_suffix": ", not from Personal.",
"family_hint": "Kids' outfits are still rendered onto your face.",
"empty_garments": "Add at least one {category} to enable Try-On."
},
"model_picker": {
"legend": "Model",
"option_openai_label": "OpenAI",
"option_openai_hint": "GPT-image · Standard",
"option_pro_label": "Nano Banana Pro",
"option_pro_hint": "Google · high consistency",
"option_flash_label": "Nano Banana 2",
"option_flash_hint": "Google · newest · cheap"
},
"garment_card": {
"wear_count_title": "Worn {count}×"
},
"outfit_card": {
"try_on_badge": "Try-On",
"try_on_preview_title": "Try-On preview",
"empty": "Empty",
"favorite": "Favorite"
},
"compose": {
"title_edit": "Edit outfit",
"title_new": "New outfit",
"back": "Back"
}
}

Some files were not shown because too many files have changed in this diff Show more