feat(writing): full i18n coverage across 10 files — DE/EN/ES/FR/IT

Writing (Ghostwriter) module had ~47 hardcoded German strings across
BriefingForm (11), RefinementPanel (12), DetailView (7), StylesView (4),
ReferencePicker (4), ListView (3), StyleForm (2), VersionHistory (2),
ExportMenu (1), VersionEditor (1).

New `writing` namespace with 213 keys × 5 locales:
- `kinds.*` (12 draft kinds), `statuses.*`, `generation_statuses.*`,
  `tones.*` (10 presets), `style_sources.*` — Svelte components now use
  these instead of `KIND_LABELS[k].de` / `STATUS_LABELS[s].de` /
  `STYLE_SOURCE_LABELS[s].de` / `TONE_PRESETS[i].de`. Constants stay as
  static maps for non-Svelte callers (prompt builders, AI tools).
- `briefing_form.*`, `refinement_panel.*`, `selection_tools.*`,
  `detail_view.*` (incl. published-target chips, share row, version
  label, generate/checkpoint buttons, undo), `list_view.*` (hero +
  quick-start template), `styles_view.*`, `style_form.*`,
  `version_history.*` (token-usage line), `version_editor.*`,
  `export_menu.*`, `reference_picker.*` (7 source kinds).

- Baseline ratchet: 1687 → 1640 (47 strings cleared, 10 files fully clean)
- validate:i18n-parity: 41 namespaces × 5 locales — 3981 keys aligned
- svelte-check: no new errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-26 22:34:49 +02:00
parent 4e31c8d736
commit e2676252d3
16 changed files with 1528 additions and 201 deletions

View file

@ -0,0 +1,247 @@
{
"kinds": {
"blog": "Blog",
"essay": "Essay",
"email": "E-Mail",
"social": "Social",
"story": "Story",
"letter": "Brief",
"speech": "Rede",
"cover-letter": "Bewerbung",
"product-description": "Produkttext",
"press-release": "Pressetext",
"bio": "Bio",
"other": "Sonstiges"
},
"statuses": {
"draft": "Entwurf",
"refining": "In Überarbeitung",
"complete": "Fertig",
"published": "Veröffentlicht"
},
"generation_statuses": {
"queued": "In Warteschlange",
"running": "Läuft",
"succeeded": "Fertig",
"failed": "Fehlgeschlagen",
"cancelled": "Abgebrochen"
},
"tones": {
"neutral": "Neutral",
"warm": "Warm",
"formal": "Formell",
"casual": "Locker",
"professional": "Professionell",
"playful": "Verspielt",
"urgent": "Dringlich",
"empathetic": "Einfühlsam",
"assertive": "Selbstbewusst",
"humorous": "Humorvoll"
},
"style_sources": {
"preset": "Vorlage",
"custom-description": "Eigene Beschreibung",
"sample-trained": "Aus Textproben trainiert",
"self-trained": "Schreibe wie ich"
},
"briefing_form": {
"label_title": "Titel",
"placeholder_title": "Mein Blogpost über …",
"label_kind": "Textart",
"label_topic": "Worum geht's?",
"label_topic_hint": "(wird als Kern-Briefing an die KI übergeben)",
"placeholder_topic": "z.B. 'Was Mana von klassischen Produktivitätstools unterscheidet, aus Nutzersicht'",
"label_audience": "Zielgruppe",
"placeholder_audience": "z.B. Gründer, Eltern, …",
"label_tone": "Ton",
"tone_none": "— kein fester Ton —",
"label_target_length": "Länge (Wörter)",
"label_language": "Sprache",
"language_de": "Deutsch",
"language_en": "English",
"language_fr": "Français",
"language_es": "Español",
"language_it": "Italiano",
"label_style": "Stil",
"label_style_hint": "(optional — prägt Ton & Struktur der Generation)",
"label_references": "Quellen",
"label_references_hint": "(optional — flowen als Kontext in den Prompt ein)",
"label_extra": "Zusatzhinweise",
"label_extra_hint": "(optional)",
"placeholder_extra": "z.B. 'keine Buzzwords', 'mit einem Zitat beginnen', …",
"err_no_topic": "Bitte erst ein Thema eingeben — der Vorschlag braucht Kontext.",
"suggest_title": "Titel aus Briefing + Inhalt vorschlagen",
"suggest_title_no_topic": "Erst Thema ausfüllen",
"cancel": "Abbrechen",
"saving": "Speichert…",
"submit_create": "Draft anlegen",
"submit_update": "Speichern"
},
"refinement_panel": {
"running": "Läuft…",
"failed": "Fehlgeschlagen",
"ready": "Vorschlag bereit",
"close_aria": "Schließen",
"col_original": "Original",
"col_proposal": "Vorschlag",
"generating": "Generiert…",
"err_unknown": "Unbekannter Fehler.",
"empty_result": "Kein Ergebnis.",
"action_accept": "Übernehmen",
"action_retry": "Noch mal",
"action_discard": "Verwerfen",
"action_cancel": "Abbrechen"
},
"selection_tools": {
"shorten": "Kürzen",
"expand": "Erweitern",
"tone": "Ton ändern",
"rewrite": "Umschreiben",
"translate": "Übersetzen"
},
"detail_view": {
"loading": "Lädt…",
"not_found_title": "Dieser Draft existiert nicht (mehr).",
"not_found_back": "Zurück zur Übersicht",
"untitled_fallback": "Unbenannt",
"back_to_drafts": "← Alle Drafts",
"toggle_favorite_title": "Favorit",
"action_delete": "Löschen",
"confirm_delete": "\"{title}\" wirklich löschen?",
"share_row_title": "Öffentlicher Link kommt mit M10 (Publish-Hooks). Bis dahin: Token kopieren.",
"share_row_label": "🔗 Unlisted-Token:",
"share_row_copied": "✓ Kopiert",
"share_row_copy": "Kopieren",
"published_label": "📤 Veröffentlicht:",
"published_articles": "📚",
"published_articles_link": "Artikel",
"published_website": "🌐 Website",
"published_presi": "🎞 Präsi",
"published_mail": "✉️ Mail",
"published_social": "💬 Social",
"briefing_section_label": "Briefing",
"active_style_title": "Aktiver Stil",
"version_label": "Version {n}",
"ai_tag": "KI",
"generate_btn": "✨ Generate",
"regenerate_btn": "⟳ Neu generieren",
"generating_btn": "Schreibt…",
"generate_first_title": "Ersten Entwurf aus dem Briefing generieren (⌘G)",
"regenerate_title": "Kompletten Text neu generieren — neue Version (⌘G)",
"checkpoint_btn": " Checkpoint",
"checkpoint_saving": "Speichert…",
"checkpoint_title": "Aktuellen Text als neue Version einfrieren (⌘⇧S)",
"version_missing": "Diese Version existiert nicht mehr.",
"history_heading": "Versionen",
"undo_label": "↶ Rückgängig: {label}",
"undo_title": "Letzte Auswahl-Verfeinerung rückgängig (⌘Z)"
},
"list_view": {
"search_placeholder": "Nach Titel oder Thema suchen…",
"styles_title": "Stile verwalten",
"close_btn": "× Schließen",
"new_draft_btn": "+ Neuer Draft",
"fav_only": "Nur Favoriten",
"loading": "Lädt…",
"hero_title": "Dein KI-Ghostwriter",
"hero_pitch": "Brief Thema, Stil und Quellen — ein fertiger Entwurf entsteht. Verfeinere ihn absatzweise mit ⌘G zum Generieren, Markieren + Selection-Tools, oder direkt im Editor.",
"hero_meta_kinds": "12 Textarten",
"hero_meta_styles": "9 Stile",
"hero_meta_references": "7 Quellen",
"hero_meta_e2e": "E2E-verschlüsselt",
"quick_start_label": "Schnellstart",
"quick_start_title_template": "Neuer {kind}-Entwurf",
"empty_filtered": "Keine Drafts passen zum aktuellen Filter."
},
"styles_view": {
"back_to_writing": "← Zurück zu Writing",
"title": "Stile",
"subtitle": "Vorlagen und eigene Stile, die der Ghostwriter beim Generieren anwendet.",
"close_btn": "× Schließen",
"create_btn": "+ Eigener Stil",
"section_presets": "Vorlagen",
"section_presets_hint": "Eingebaute Stile — direkt im Briefing auswählbar. Nicht bearbeitbar; für Anpassungen lege einen eigenen Stil an.",
"badge_template": "Vorlage",
"section_my_styles": "Meine Stile",
"loading": "Lädt…",
"empty_my_styles_pre": "Keine eigenen Stile. Klick oben auf ",
"empty_my_styles_strong": "+ Eigener Stil",
"empty_my_styles_post": ", um einen anzulegen — z.B. \"Mein Corporate-Ton\" oder \"Persönliche Blog-Stimme\".",
"action_edit": "Bearbeiten",
"action_delete": "Löschen",
"confirm_delete": "\"{name}\" wirklich löschen?"
},
"style_form": {
"label_name": "Name",
"placeholder_name": "Mein Corporate-Ton",
"label_description": "Beschreibung",
"label_description_hint": "(die KI liest das wörtlich — sei konkret)",
"placeholder_description": "z.B. \"Kurze Sätze, aktive Formulierungen, keine Buzzwords. Erste-Person-Singular, du-Ansprache. Max. 3 Sätze pro Absatz. Jeder Abschnitt endet mit einer konkreten nächsten Aktion.\"",
"cancel": "Abbrechen",
"saving": "Speichert…",
"submit_create": "Stil anlegen",
"submit_update": "Speichern"
},
"version_history": {
"badge_ai": "KI",
"badge_ai_title": "KI-generiert",
"badge_active": "Aktiv",
"word_count": "{count} Wörter",
"cost_title": "Verbrauch + Modell der zugehörigen Generation",
"tokens_label": "{input} → {output} Tokens",
"restore": "Wiederherstellen"
},
"version_editor": {
"placeholder": "Hier schreibst du (oder die KI). Leer lassen für Generate.",
"word_count": "{count} Wörter",
"target_words": " / Ziel ~{words}",
"saving": "Speichert…",
"saved": "Gespeichert"
},
"export_menu": {
"trigger": "📤 Export",
"title": "Exportieren / Veröffentlichen",
"copy_md": "📋 Markdown kopieren",
"copy_text": "📋 Text kopieren",
"download_md": "↓ Als .md herunterladen",
"print_pdf": "🖨 Drucken / PDF",
"save_as_article": "📚 Als Artikel speichern",
"toast_md_copied": "✓ Markdown kopiert",
"toast_text_copied": "✓ Text kopiert",
"toast_copy_failed": "Kopieren fehlgeschlagen",
"toast_downloaded": "↓ Heruntergeladen",
"toast_saved_article": "✓ Als Artikel gespeichert",
"site_name": "Writing"
},
"reference_picker": {
"label_url_default": "Link",
"label_kontext": "Kontext-Dokument",
"label_unknown": "—",
"label_article_missing": "Artikel (fehlt)",
"label_note_missing": "Notiz (fehlt)",
"label_note_untitled": "Ohne Titel",
"label_library_missing": "Library-Eintrag (fehlt)",
"label_goal_missing": "Ziel (fehlt)",
"label_image_missing": "Bild (fehlt)",
"label_image_kind_fallback": "{kind}-Bild",
"add_label": "+ Quelle:",
"max_reached": "Max. {max} Quellen pro Draft erreicht. Entferne eine, um eine neue hinzuzufügen.",
"kind_article": "📄 Artikel",
"kind_note": "📝 Notiz",
"kind_library": "📚 Library",
"kind_kontext": "🗂 Kontext",
"kind_goal": "🎯 Ziel",
"kind_me-image": "🖼 Bild",
"kind_url": "🔗 URL",
"search_placeholder": "Suche…",
"no_results": "Keine Treffer.",
"no_goals": "Keine Ziele angelegt.",
"no_me_images": "Keine Bilder. Lege welche unter /profile/me-images an.",
"kontext_empty_pre": "Dieser Space hat noch kein Kontext-Dokument. Lege eines unter ",
"kontext_empty_post": " an.",
"kontext_link": "Kontext-Dokument verknüpfen",
"url_placeholder": "https://…",
"url_note_placeholder": "Kontext (optional)",
"url_add": "Hinzufügen"
}
}

View file

@ -0,0 +1,247 @@
{
"kinds": {
"blog": "Blog",
"essay": "Essay",
"email": "Email",
"social": "Social",
"story": "Story",
"letter": "Letter",
"speech": "Speech",
"cover-letter": "Cover letter",
"product-description": "Product",
"press-release": "Press",
"bio": "Bio",
"other": "Other"
},
"statuses": {
"draft": "Draft",
"refining": "Refining",
"complete": "Complete",
"published": "Published"
},
"generation_statuses": {
"queued": "Queued",
"running": "Running",
"succeeded": "Succeeded",
"failed": "Failed",
"cancelled": "Cancelled"
},
"tones": {
"neutral": "Neutral",
"warm": "Warm",
"formal": "Formal",
"casual": "Casual",
"professional": "Professional",
"playful": "Playful",
"urgent": "Urgent",
"empathetic": "Empathetic",
"assertive": "Assertive",
"humorous": "Humorous"
},
"style_sources": {
"preset": "Preset",
"custom-description": "Custom description",
"sample-trained": "Trained from samples",
"self-trained": "Write like me"
},
"briefing_form": {
"label_title": "Title",
"placeholder_title": "My blog post about …",
"label_kind": "Kind",
"label_topic": "What's it about?",
"label_topic_hint": "(passed to the AI as the core briefing)",
"placeholder_topic": "e.g. 'How Mana differs from classic productivity tools, from a user's perspective'",
"label_audience": "Audience",
"placeholder_audience": "e.g. founders, parents, …",
"label_tone": "Tone",
"tone_none": "— no fixed tone —",
"label_target_length": "Length (words)",
"label_language": "Language",
"language_de": "German",
"language_en": "English",
"language_fr": "French",
"language_es": "Spanish",
"language_it": "Italian",
"label_style": "Style",
"label_style_hint": "(optional — shapes tone & structure of generation)",
"label_references": "References",
"label_references_hint": "(optional — flow into the prompt as context)",
"label_extra": "Extra notes",
"label_extra_hint": "(optional)",
"placeholder_extra": "e.g. 'no buzzwords', 'start with a quote', …",
"err_no_topic": "Enter a topic first — the suggestion needs context.",
"suggest_title": "Suggest a title from briefing + content",
"suggest_title_no_topic": "Fill in topic first",
"cancel": "Cancel",
"saving": "Saving…",
"submit_create": "Create draft",
"submit_update": "Save"
},
"refinement_panel": {
"running": "Running…",
"failed": "Failed",
"ready": "Suggestion ready",
"close_aria": "Close",
"col_original": "Original",
"col_proposal": "Suggestion",
"generating": "Generating…",
"err_unknown": "Unknown error.",
"empty_result": "No result.",
"action_accept": "Accept",
"action_retry": "Retry",
"action_discard": "Discard",
"action_cancel": "Cancel"
},
"selection_tools": {
"shorten": "Shorten",
"expand": "Expand",
"tone": "Change tone",
"rewrite": "Rewrite",
"translate": "Translate"
},
"detail_view": {
"loading": "Loading…",
"not_found_title": "This draft no longer exists.",
"not_found_back": "Back to list",
"untitled_fallback": "Untitled",
"back_to_drafts": "← All drafts",
"toggle_favorite_title": "Favorite",
"action_delete": "Delete",
"confirm_delete": "Really delete \"{title}\"?",
"share_row_title": "Public link comes with M10 (publish hooks). Until then: copy the token.",
"share_row_label": "🔗 Unlisted token:",
"share_row_copied": "✓ Copied",
"share_row_copy": "Copy",
"published_label": "📤 Published:",
"published_articles": "📚",
"published_articles_link": "Article",
"published_website": "🌐 Website",
"published_presi": "🎞 Presi",
"published_mail": "✉️ Mail",
"published_social": "💬 Social",
"briefing_section_label": "Briefing",
"active_style_title": "Active style",
"version_label": "Version {n}",
"ai_tag": "AI",
"generate_btn": "✨ Generate",
"regenerate_btn": "⟳ Regenerate",
"generating_btn": "Writing…",
"generate_first_title": "Generate the first draft from the briefing (⌘G)",
"regenerate_title": "Regenerate the full text — new version (⌘G)",
"checkpoint_btn": " Checkpoint",
"checkpoint_saving": "Saving…",
"checkpoint_title": "Freeze current text as a new version (⌘⇧S)",
"version_missing": "This version no longer exists.",
"history_heading": "Versions",
"undo_label": "↶ Undo: {label}",
"undo_title": "Undo last selection refinement (⌘Z)"
},
"list_view": {
"search_placeholder": "Search by title or topic…",
"styles_title": "Manage styles",
"close_btn": "× Close",
"new_draft_btn": "+ New draft",
"fav_only": "Favorites only",
"loading": "Loading…",
"hero_title": "Your AI ghostwriter",
"hero_pitch": "Brief topic, style and references — a finished draft appears. Refine it paragraph by paragraph with ⌘G to generate, select + selection tools, or directly in the editor.",
"hero_meta_kinds": "12 kinds",
"hero_meta_styles": "9 styles",
"hero_meta_references": "7 references",
"hero_meta_e2e": "E2E-encrypted",
"quick_start_label": "Quick start",
"quick_start_title_template": "New {kind} draft",
"empty_filtered": "No drafts match the current filter."
},
"styles_view": {
"back_to_writing": "← Back to Writing",
"title": "Styles",
"subtitle": "Templates and your own styles the ghostwriter applies when generating.",
"close_btn": "× Close",
"create_btn": "+ Custom style",
"section_presets": "Templates",
"section_presets_hint": "Built-in styles — pick directly in the briefing. Not editable; create your own style for adjustments.",
"badge_template": "Template",
"section_my_styles": "My styles",
"loading": "Loading…",
"empty_my_styles_pre": "No custom styles yet. Click ",
"empty_my_styles_strong": "+ Custom style",
"empty_my_styles_post": " above to create one — e.g. \"My corporate tone\" or \"Personal blog voice\".",
"action_edit": "Edit",
"action_delete": "Delete",
"confirm_delete": "Really delete \"{name}\"?"
},
"style_form": {
"label_name": "Name",
"placeholder_name": "My corporate tone",
"label_description": "Description",
"label_description_hint": "(the AI reads this verbatim — be concrete)",
"placeholder_description": "e.g. \"Short sentences, active phrasing, no buzzwords. First-person-singular, casual you. Max. 3 sentences per paragraph. Each section ends with a concrete next action.\"",
"cancel": "Cancel",
"saving": "Saving…",
"submit_create": "Create style",
"submit_update": "Save"
},
"version_history": {
"badge_ai": "AI",
"badge_ai_title": "AI-generated",
"badge_active": "Active",
"word_count": "{count} words",
"cost_title": "Token usage + model of the linked generation",
"tokens_label": "{input} → {output} tokens",
"restore": "Restore"
},
"version_editor": {
"placeholder": "Write here (or let the AI). Leave empty to Generate.",
"word_count": "{count} words",
"target_words": " / target ~{words}",
"saving": "Saving…",
"saved": "Saved"
},
"export_menu": {
"trigger": "📤 Export",
"title": "Export / publish",
"copy_md": "📋 Copy markdown",
"copy_text": "📋 Copy text",
"download_md": "↓ Download as .md",
"print_pdf": "🖨 Print / PDF",
"save_as_article": "📚 Save as article",
"toast_md_copied": "✓ Markdown copied",
"toast_text_copied": "✓ Text copied",
"toast_copy_failed": "Copy failed",
"toast_downloaded": "↓ Downloaded",
"toast_saved_article": "✓ Saved as article",
"site_name": "Writing"
},
"reference_picker": {
"label_url_default": "Link",
"label_kontext": "Context document",
"label_unknown": "—",
"label_article_missing": "Article (missing)",
"label_note_missing": "Note (missing)",
"label_note_untitled": "Untitled",
"label_library_missing": "Library entry (missing)",
"label_goal_missing": "Goal (missing)",
"label_image_missing": "Image (missing)",
"label_image_kind_fallback": "{kind} image",
"add_label": "+ Source:",
"max_reached": "Max. {max} sources per draft reached. Remove one to add a new one.",
"kind_article": "📄 Article",
"kind_note": "📝 Note",
"kind_library": "📚 Library",
"kind_kontext": "🗂 Context",
"kind_goal": "🎯 Goal",
"kind_me-image": "🖼 Image",
"kind_url": "🔗 URL",
"search_placeholder": "Search…",
"no_results": "No matches.",
"no_goals": "No goals defined.",
"no_me_images": "No images. Add some under /profile/me-images.",
"kontext_empty_pre": "This space doesn't have a context document yet. Create one under ",
"kontext_empty_post": ".",
"kontext_link": "Link context document",
"url_placeholder": "https://…",
"url_note_placeholder": "Context (optional)",
"url_add": "Add"
}
}

View file

@ -0,0 +1,247 @@
{
"kinds": {
"blog": "Blog",
"essay": "Ensayo",
"email": "Correo",
"social": "Social",
"story": "Relato",
"letter": "Carta",
"speech": "Discurso",
"cover-letter": "Carta de presentación",
"product-description": "Producto",
"press-release": "Nota de prensa",
"bio": "Biografía",
"other": "Otro"
},
"statuses": {
"draft": "Borrador",
"refining": "En revisión",
"complete": "Listo",
"published": "Publicado"
},
"generation_statuses": {
"queued": "En cola",
"running": "En curso",
"succeeded": "Listo",
"failed": "Fallido",
"cancelled": "Cancelado"
},
"tones": {
"neutral": "Neutral",
"warm": "Cálido",
"formal": "Formal",
"casual": "Casual",
"professional": "Profesional",
"playful": "Lúdico",
"urgent": "Urgente",
"empathetic": "Empático",
"assertive": "Asertivo",
"humorous": "Humorístico"
},
"style_sources": {
"preset": "Plantilla",
"custom-description": "Descripción propia",
"sample-trained": "Entrenado con muestras",
"self-trained": "Escribir como yo"
},
"briefing_form": {
"label_title": "Título",
"placeholder_title": "Mi entrada de blog sobre …",
"label_kind": "Tipo",
"label_topic": "¿De qué trata?",
"label_topic_hint": "(se pasa a la IA como briefing principal)",
"placeholder_topic": "p. ej. 'En qué se diferencia Mana de las herramientas clásicas de productividad, desde la perspectiva del usuario'",
"label_audience": "Público",
"placeholder_audience": "p. ej. fundadores, padres, …",
"label_tone": "Tono",
"tone_none": "— sin tono fijo —",
"label_target_length": "Longitud (palabras)",
"label_language": "Idioma",
"language_de": "Alemán",
"language_en": "Inglés",
"language_fr": "Francés",
"language_es": "Español",
"language_it": "Italiano",
"label_style": "Estilo",
"label_style_hint": "(opcional — define el tono y la estructura de la generación)",
"label_references": "Fuentes",
"label_references_hint": "(opcional — pasan al prompt como contexto)",
"label_extra": "Notas adicionales",
"label_extra_hint": "(opcional)",
"placeholder_extra": "p. ej. 'sin clichés', 'empezar con una cita', …",
"err_no_topic": "Introduce primero un tema — la sugerencia necesita contexto.",
"suggest_title": "Sugerir un título a partir del briefing y el contenido",
"suggest_title_no_topic": "Rellena primero el tema",
"cancel": "Cancelar",
"saving": "Guardando…",
"submit_create": "Crear borrador",
"submit_update": "Guardar"
},
"refinement_panel": {
"running": "En curso…",
"failed": "Fallido",
"ready": "Sugerencia lista",
"close_aria": "Cerrar",
"col_original": "Original",
"col_proposal": "Sugerencia",
"generating": "Generando…",
"err_unknown": "Error desconocido.",
"empty_result": "Sin resultado.",
"action_accept": "Aceptar",
"action_retry": "Otra vez",
"action_discard": "Descartar",
"action_cancel": "Cancelar"
},
"selection_tools": {
"shorten": "Acortar",
"expand": "Ampliar",
"tone": "Cambiar tono",
"rewrite": "Reescribir",
"translate": "Traducir"
},
"detail_view": {
"loading": "Cargando…",
"not_found_title": "Este borrador ya no existe.",
"not_found_back": "Volver al listado",
"untitled_fallback": "Sin título",
"back_to_drafts": "← Todos los borradores",
"toggle_favorite_title": "Favorito",
"action_delete": "Eliminar",
"confirm_delete": "¿Eliminar de verdad \"{title}\"?",
"share_row_title": "El enlace público llega con M10 (publish hooks). Mientras tanto: copia el token.",
"share_row_label": "🔗 Token unlisted:",
"share_row_copied": "✓ Copiado",
"share_row_copy": "Copiar",
"published_label": "📤 Publicado:",
"published_articles": "📚",
"published_articles_link": "Artículo",
"published_website": "🌐 Sitio web",
"published_presi": "🎞 Presentación",
"published_mail": "✉️ Correo",
"published_social": "💬 Social",
"briefing_section_label": "Briefing",
"active_style_title": "Estilo activo",
"version_label": "Versión {n}",
"ai_tag": "IA",
"generate_btn": "✨ Generar",
"regenerate_btn": "⟳ Regenerar",
"generating_btn": "Escribiendo…",
"generate_first_title": "Generar el primer borrador a partir del briefing (⌘G)",
"regenerate_title": "Regenerar el texto completo — nueva versión (⌘G)",
"checkpoint_btn": " Punto de control",
"checkpoint_saving": "Guardando…",
"checkpoint_title": "Congelar el texto actual como nueva versión (⌘⇧S)",
"version_missing": "Esta versión ya no existe.",
"history_heading": "Versiones",
"undo_label": "↶ Deshacer: {label}",
"undo_title": "Deshacer la última refinación de selección (⌘Z)"
},
"list_view": {
"search_placeholder": "Buscar por título o tema…",
"styles_title": "Gestionar estilos",
"close_btn": "× Cerrar",
"new_draft_btn": "+ Nuevo borrador",
"fav_only": "Solo favoritos",
"loading": "Cargando…",
"hero_title": "Tu ghostwriter de IA",
"hero_pitch": "Brief de tema, estilo y fuentes — aparece un borrador terminado. Refínalo párrafo a párrafo con ⌘G para generar, seleccionar + selection tools, o directamente en el editor.",
"hero_meta_kinds": "12 tipos",
"hero_meta_styles": "9 estilos",
"hero_meta_references": "7 fuentes",
"hero_meta_e2e": "Cifrado E2E",
"quick_start_label": "Inicio rápido",
"quick_start_title_template": "Nuevo borrador {kind}",
"empty_filtered": "Ningún borrador coincide con el filtro actual."
},
"styles_view": {
"back_to_writing": "← Volver a Writing",
"title": "Estilos",
"subtitle": "Plantillas y estilos propios que el ghostwriter aplica al generar.",
"close_btn": "× Cerrar",
"create_btn": "+ Estilo propio",
"section_presets": "Plantillas",
"section_presets_hint": "Estilos integrados — selecciónalos directamente en el briefing. No editables; crea uno propio para ajustes.",
"badge_template": "Plantilla",
"section_my_styles": "Mis estilos",
"loading": "Cargando…",
"empty_my_styles_pre": "Aún no hay estilos propios. Pulsa arriba ",
"empty_my_styles_strong": "+ Estilo propio",
"empty_my_styles_post": " para crear uno — p. ej. \"Mi tono corporativo\" o \"Voz de blog personal\".",
"action_edit": "Editar",
"action_delete": "Eliminar",
"confirm_delete": "¿Eliminar de verdad \"{name}\"?"
},
"style_form": {
"label_name": "Nombre",
"placeholder_name": "Mi tono corporativo",
"label_description": "Descripción",
"label_description_hint": "(la IA lo lee literalmente — sé concreto)",
"placeholder_description": "p. ej. \"Frases cortas, voz activa, sin clichés. Primera persona del singular, tuteo. Máx. 3 frases por párrafo. Cada sección termina con una acción concreta siguiente.\"",
"cancel": "Cancelar",
"saving": "Guardando…",
"submit_create": "Crear estilo",
"submit_update": "Guardar"
},
"version_history": {
"badge_ai": "IA",
"badge_ai_title": "Generado por IA",
"badge_active": "Activa",
"word_count": "{count} palabras",
"cost_title": "Uso de tokens + modelo de la generación asociada",
"tokens_label": "{input} → {output} tokens",
"restore": "Restaurar"
},
"version_editor": {
"placeholder": "Escribe aquí (o deja a la IA). Vacío para generar.",
"word_count": "{count} palabras",
"target_words": " / objetivo ~{words}",
"saving": "Guardando…",
"saved": "Guardado"
},
"export_menu": {
"trigger": "📤 Exportar",
"title": "Exportar / publicar",
"copy_md": "📋 Copiar markdown",
"copy_text": "📋 Copiar texto",
"download_md": "↓ Descargar como .md",
"print_pdf": "🖨 Imprimir / PDF",
"save_as_article": "📚 Guardar como artículo",
"toast_md_copied": "✓ Markdown copiado",
"toast_text_copied": "✓ Texto copiado",
"toast_copy_failed": "Error al copiar",
"toast_downloaded": "↓ Descargado",
"toast_saved_article": "✓ Guardado como artículo",
"site_name": "Writing"
},
"reference_picker": {
"label_url_default": "Enlace",
"label_kontext": "Documento de contexto",
"label_unknown": "—",
"label_article_missing": "Artículo (no encontrado)",
"label_note_missing": "Nota (no encontrada)",
"label_note_untitled": "Sin título",
"label_library_missing": "Entrada de biblioteca (no encontrada)",
"label_goal_missing": "Objetivo (no encontrado)",
"label_image_missing": "Imagen (no encontrada)",
"label_image_kind_fallback": "Imagen {kind}",
"add_label": "+ Fuente:",
"max_reached": "Máx. {max} fuentes por borrador alcanzado. Quita una para añadir otra.",
"kind_article": "📄 Artículo",
"kind_note": "📝 Nota",
"kind_library": "📚 Biblioteca",
"kind_kontext": "🗂 Contexto",
"kind_goal": "🎯 Objetivo",
"kind_me-image": "🖼 Imagen",
"kind_url": "🔗 URL",
"search_placeholder": "Buscar…",
"no_results": "Sin resultados.",
"no_goals": "No hay objetivos definidos.",
"no_me_images": "Sin imágenes. Añade algunas en /profile/me-images.",
"kontext_empty_pre": "Este espacio aún no tiene documento de contexto. Crea uno en ",
"kontext_empty_post": ".",
"kontext_link": "Vincular documento de contexto",
"url_placeholder": "https://…",
"url_note_placeholder": "Contexto (opcional)",
"url_add": "Añadir"
}
}

View file

@ -0,0 +1,247 @@
{
"kinds": {
"blog": "Blog",
"essay": "Essai",
"email": "E-mail",
"social": "Social",
"story": "Histoire",
"letter": "Lettre",
"speech": "Discours",
"cover-letter": "Lettre de motivation",
"product-description": "Produit",
"press-release": "Communiqué",
"bio": "Bio",
"other": "Autre"
},
"statuses": {
"draft": "Brouillon",
"refining": "En révision",
"complete": "Terminé",
"published": "Publié"
},
"generation_statuses": {
"queued": "En attente",
"running": "En cours",
"succeeded": "Terminé",
"failed": "Échec",
"cancelled": "Annulé"
},
"tones": {
"neutral": "Neutre",
"warm": "Chaleureux",
"formal": "Formel",
"casual": "Décontracté",
"professional": "Professionnel",
"playful": "Ludique",
"urgent": "Urgent",
"empathetic": "Empathique",
"assertive": "Affirmé",
"humorous": "Humoristique"
},
"style_sources": {
"preset": "Modèle",
"custom-description": "Description personnelle",
"sample-trained": "Entraîné sur des échantillons",
"self-trained": "Écris comme moi"
},
"briefing_form": {
"label_title": "Titre",
"placeholder_title": "Mon article de blog sur …",
"label_kind": "Type",
"label_topic": "De quoi ça parle ?",
"label_topic_hint": "(transmis à l'IA comme briefing principal)",
"placeholder_topic": "p. ex. 'Ce qui distingue Mana des outils de productivité classiques, du point de vue utilisateur'",
"label_audience": "Audience",
"placeholder_audience": "p. ex. fondateurs, parents, …",
"label_tone": "Ton",
"tone_none": "— pas de ton fixe —",
"label_target_length": "Longueur (mots)",
"label_language": "Langue",
"language_de": "Allemand",
"language_en": "Anglais",
"language_fr": "Français",
"language_es": "Espagnol",
"language_it": "Italien",
"label_style": "Style",
"label_style_hint": "(optionnel — façonne le ton et la structure de la génération)",
"label_references": "Sources",
"label_references_hint": "(optionnel — passées au prompt comme contexte)",
"label_extra": "Notes supplémentaires",
"label_extra_hint": "(optionnel)",
"placeholder_extra": "p. ex. 'pas de buzzwords', 'commencer par une citation', …",
"err_no_topic": "Saisis d'abord un sujet — la suggestion a besoin de contexte.",
"suggest_title": "Suggérer un titre à partir du briefing + contenu",
"suggest_title_no_topic": "Remplis d'abord le sujet",
"cancel": "Annuler",
"saving": "Enregistrement…",
"submit_create": "Créer le brouillon",
"submit_update": "Enregistrer"
},
"refinement_panel": {
"running": "En cours…",
"failed": "Échec",
"ready": "Suggestion prête",
"close_aria": "Fermer",
"col_original": "Original",
"col_proposal": "Suggestion",
"generating": "Génération…",
"err_unknown": "Erreur inconnue.",
"empty_result": "Aucun résultat.",
"action_accept": "Accepter",
"action_retry": "Réessayer",
"action_discard": "Rejeter",
"action_cancel": "Annuler"
},
"selection_tools": {
"shorten": "Raccourcir",
"expand": "Développer",
"tone": "Changer de ton",
"rewrite": "Réécrire",
"translate": "Traduire"
},
"detail_view": {
"loading": "Chargement…",
"not_found_title": "Ce brouillon n'existe plus.",
"not_found_back": "Retour à la liste",
"untitled_fallback": "Sans titre",
"back_to_drafts": "← Tous les brouillons",
"toggle_favorite_title": "Favori",
"action_delete": "Supprimer",
"confirm_delete": "Vraiment supprimer « {title} » ?",
"share_row_title": "Le lien public arrive avec M10 (publish hooks). En attendant : copie le token.",
"share_row_label": "🔗 Token unlisted :",
"share_row_copied": "✓ Copié",
"share_row_copy": "Copier",
"published_label": "📤 Publié :",
"published_articles": "📚",
"published_articles_link": "Article",
"published_website": "🌐 Site",
"published_presi": "🎞 Présentation",
"published_mail": "✉️ E-mail",
"published_social": "💬 Social",
"briefing_section_label": "Briefing",
"active_style_title": "Style actif",
"version_label": "Version {n}",
"ai_tag": "IA",
"generate_btn": "✨ Générer",
"regenerate_btn": "⟳ Régénérer",
"generating_btn": "Écriture…",
"generate_first_title": "Générer le premier brouillon depuis le briefing (⌘G)",
"regenerate_title": "Régénérer le texte complet — nouvelle version (⌘G)",
"checkpoint_btn": " Point de contrôle",
"checkpoint_saving": "Enregistrement…",
"checkpoint_title": "Figer le texte actuel comme nouvelle version (⌘⇧S)",
"version_missing": "Cette version n'existe plus.",
"history_heading": "Versions",
"undo_label": "↶ Annuler : {label}",
"undo_title": "Annuler la dernière révision de sélection (⌘Z)"
},
"list_view": {
"search_placeholder": "Rechercher par titre ou sujet…",
"styles_title": "Gérer les styles",
"close_btn": "× Fermer",
"new_draft_btn": "+ Nouveau brouillon",
"fav_only": "Favoris seulement",
"loading": "Chargement…",
"hero_title": "Ton ghostwriter IA",
"hero_pitch": "Brief sujet, style et sources — un brouillon fini apparaît. Affine-le paragraphe par paragraphe avec ⌘G pour générer, sélectionner + selection tools, ou directement dans l'éditeur.",
"hero_meta_kinds": "12 types",
"hero_meta_styles": "9 styles",
"hero_meta_references": "7 sources",
"hero_meta_e2e": "Chiffré E2E",
"quick_start_label": "Démarrage rapide",
"quick_start_title_template": "Nouveau brouillon {kind}",
"empty_filtered": "Aucun brouillon ne correspond au filtre actuel."
},
"styles_view": {
"back_to_writing": "← Retour à Writing",
"title": "Styles",
"subtitle": "Modèles et styles personnels que le ghostwriter applique lors de la génération.",
"close_btn": "× Fermer",
"create_btn": "+ Style personnel",
"section_presets": "Modèles",
"section_presets_hint": "Styles intégrés — sélectionnables directement dans le briefing. Non modifiables ; crée un style personnel pour des ajustements.",
"badge_template": "Modèle",
"section_my_styles": "Mes styles",
"loading": "Chargement…",
"empty_my_styles_pre": "Pas encore de styles personnels. Clique en haut sur ",
"empty_my_styles_strong": "+ Style personnel",
"empty_my_styles_post": " pour en créer un — p. ex. « Mon ton corporate » ou « Ma voix blog ».",
"action_edit": "Modifier",
"action_delete": "Supprimer",
"confirm_delete": "Vraiment supprimer « {name} » ?"
},
"style_form": {
"label_name": "Nom",
"placeholder_name": "Mon ton corporate",
"label_description": "Description",
"label_description_hint": "(l'IA lit ceci littéralement — sois concret)",
"placeholder_description": "p. ex. « Phrases courtes, formulations actives, pas de buzzwords. Première personne du singulier, tutoiement. Max. 3 phrases par paragraphe. Chaque section finit par une action concrète. »",
"cancel": "Annuler",
"saving": "Enregistrement…",
"submit_create": "Créer le style",
"submit_update": "Enregistrer"
},
"version_history": {
"badge_ai": "IA",
"badge_ai_title": "Généré par l'IA",
"badge_active": "Active",
"word_count": "{count} mots",
"cost_title": "Tokens utilisés + modèle de la génération associée",
"tokens_label": "{input} → {output} tokens",
"restore": "Restaurer"
},
"version_editor": {
"placeholder": "Écris ici (ou laisse l'IA). Vide pour générer.",
"word_count": "{count} mots",
"target_words": " / cible ~{words}",
"saving": "Enregistrement…",
"saved": "Enregistré"
},
"export_menu": {
"trigger": "📤 Export",
"title": "Exporter / publier",
"copy_md": "📋 Copier markdown",
"copy_text": "📋 Copier le texte",
"download_md": "↓ Télécharger en .md",
"print_pdf": "🖨 Imprimer / PDF",
"save_as_article": "📚 Enregistrer comme article",
"toast_md_copied": "✓ Markdown copié",
"toast_text_copied": "✓ Texte copié",
"toast_copy_failed": "Échec de la copie",
"toast_downloaded": "↓ Téléchargé",
"toast_saved_article": "✓ Enregistré comme article",
"site_name": "Writing"
},
"reference_picker": {
"label_url_default": "Lien",
"label_kontext": "Document de contexte",
"label_unknown": "—",
"label_article_missing": "Article (introuvable)",
"label_note_missing": "Note (introuvable)",
"label_note_untitled": "Sans titre",
"label_library_missing": "Entrée de bibliothèque (introuvable)",
"label_goal_missing": "Objectif (introuvable)",
"label_image_missing": "Image (introuvable)",
"label_image_kind_fallback": "Image {kind}",
"add_label": "+ Source :",
"max_reached": "Max. {max} sources par brouillon atteint. Retire-en une pour en ajouter une nouvelle.",
"kind_article": "📄 Article",
"kind_note": "📝 Note",
"kind_library": "📚 Bibliothèque",
"kind_kontext": "🗂 Contexte",
"kind_goal": "🎯 Objectif",
"kind_me-image": "🖼 Image",
"kind_url": "🔗 URL",
"search_placeholder": "Rechercher…",
"no_results": "Aucun résultat.",
"no_goals": "Aucun objectif défini.",
"no_me_images": "Aucune image. Ajoute-en sous /profile/me-images.",
"kontext_empty_pre": "Cet espace n'a pas encore de document de contexte. Crée-en un sous ",
"kontext_empty_post": ".",
"kontext_link": "Lier le document de contexte",
"url_placeholder": "https://…",
"url_note_placeholder": "Contexte (optionnel)",
"url_add": "Ajouter"
}
}

View file

@ -0,0 +1,247 @@
{
"kinds": {
"blog": "Blog",
"essay": "Saggio",
"email": "E-mail",
"social": "Social",
"story": "Racconto",
"letter": "Lettera",
"speech": "Discorso",
"cover-letter": "Lettera di presentazione",
"product-description": "Prodotto",
"press-release": "Comunicato stampa",
"bio": "Bio",
"other": "Altro"
},
"statuses": {
"draft": "Bozza",
"refining": "In revisione",
"complete": "Completato",
"published": "Pubblicato"
},
"generation_statuses": {
"queued": "In coda",
"running": "In esecuzione",
"succeeded": "Completato",
"failed": "Fallito",
"cancelled": "Annullato"
},
"tones": {
"neutral": "Neutro",
"warm": "Caloroso",
"formal": "Formale",
"casual": "Informale",
"professional": "Professionale",
"playful": "Giocoso",
"urgent": "Urgente",
"empathetic": "Empatico",
"assertive": "Assertivo",
"humorous": "Umoristico"
},
"style_sources": {
"preset": "Modello",
"custom-description": "Descrizione personale",
"sample-trained": "Allenato su campioni",
"self-trained": "Scrivi come me"
},
"briefing_form": {
"label_title": "Titolo",
"placeholder_title": "Il mio articolo del blog su …",
"label_kind": "Tipo",
"label_topic": "Di cosa parla?",
"label_topic_hint": "(passato all'IA come briefing principale)",
"placeholder_topic": "es. 'Cosa distingue Mana dagli strumenti classici di produttività, dal punto di vista dell'utente'",
"label_audience": "Pubblico",
"placeholder_audience": "es. fondatori, genitori, …",
"label_tone": "Tono",
"tone_none": "— nessun tono fisso —",
"label_target_length": "Lunghezza (parole)",
"label_language": "Lingua",
"language_de": "Tedesco",
"language_en": "Inglese",
"language_fr": "Francese",
"language_es": "Spagnolo",
"language_it": "Italiano",
"label_style": "Stile",
"label_style_hint": "(opzionale — definisce tono e struttura della generazione)",
"label_references": "Fonti",
"label_references_hint": "(opzionale — entrano nel prompt come contesto)",
"label_extra": "Note aggiuntive",
"label_extra_hint": "(opzionale)",
"placeholder_extra": "es. 'niente buzzword', 'inizia con una citazione', …",
"err_no_topic": "Inserisci prima un argomento — la proposta ha bisogno di contesto.",
"suggest_title": "Proponi un titolo da briefing + contenuto",
"suggest_title_no_topic": "Compila prima l'argomento",
"cancel": "Annulla",
"saving": "Salvataggio…",
"submit_create": "Crea bozza",
"submit_update": "Salva"
},
"refinement_panel": {
"running": "In esecuzione…",
"failed": "Fallito",
"ready": "Proposta pronta",
"close_aria": "Chiudi",
"col_original": "Originale",
"col_proposal": "Proposta",
"generating": "Generazione…",
"err_unknown": "Errore sconosciuto.",
"empty_result": "Nessun risultato.",
"action_accept": "Accetta",
"action_retry": "Riprova",
"action_discard": "Scarta",
"action_cancel": "Annulla"
},
"selection_tools": {
"shorten": "Accorcia",
"expand": "Espandi",
"tone": "Cambia tono",
"rewrite": "Riscrivi",
"translate": "Traduci"
},
"detail_view": {
"loading": "Caricamento…",
"not_found_title": "Questa bozza non esiste più.",
"not_found_back": "Torna all'elenco",
"untitled_fallback": "Senza titolo",
"back_to_drafts": "← Tutte le bozze",
"toggle_favorite_title": "Preferito",
"action_delete": "Elimina",
"confirm_delete": "Eliminare davvero \"{title}\"?",
"share_row_title": "Il link pubblico arriva con M10 (publish hooks). Per ora: copia il token.",
"share_row_label": "🔗 Token unlisted:",
"share_row_copied": "✓ Copiato",
"share_row_copy": "Copia",
"published_label": "📤 Pubblicato:",
"published_articles": "📚",
"published_articles_link": "Articolo",
"published_website": "🌐 Sito web",
"published_presi": "🎞 Presentazione",
"published_mail": "✉️ Mail",
"published_social": "💬 Social",
"briefing_section_label": "Briefing",
"active_style_title": "Stile attivo",
"version_label": "Versione {n}",
"ai_tag": "IA",
"generate_btn": "✨ Genera",
"regenerate_btn": "⟳ Rigenera",
"generating_btn": "Scrive…",
"generate_first_title": "Genera la prima bozza dal briefing (⌘G)",
"regenerate_title": "Rigenera tutto il testo — nuova versione (⌘G)",
"checkpoint_btn": " Checkpoint",
"checkpoint_saving": "Salvataggio…",
"checkpoint_title": "Congela il testo attuale come nuova versione (⌘⇧S)",
"version_missing": "Questa versione non esiste più.",
"history_heading": "Versioni",
"undo_label": "↶ Annulla: {label}",
"undo_title": "Annulla l'ultima rifinitura della selezione (⌘Z)"
},
"list_view": {
"search_placeholder": "Cerca per titolo o argomento…",
"styles_title": "Gestisci stili",
"close_btn": "× Chiudi",
"new_draft_btn": "+ Nuova bozza",
"fav_only": "Solo preferiti",
"loading": "Caricamento…",
"hero_title": "Il tuo ghostwriter IA",
"hero_pitch": "Brief argomento, stile e fonti — nasce una bozza completa. Affinala paragrafo per paragrafo con ⌘G per generare, selezione + selection tools, o direttamente nell'editor.",
"hero_meta_kinds": "12 tipi",
"hero_meta_styles": "9 stili",
"hero_meta_references": "7 fonti",
"hero_meta_e2e": "Cifratura E2E",
"quick_start_label": "Avvio rapido",
"quick_start_title_template": "Nuova bozza {kind}",
"empty_filtered": "Nessuna bozza corrisponde al filtro attuale."
},
"styles_view": {
"back_to_writing": "← Torna a Writing",
"title": "Stili",
"subtitle": "Modelli e stili personali che il ghostwriter applica durante la generazione.",
"close_btn": "× Chiudi",
"create_btn": "+ Stile personale",
"section_presets": "Modelli",
"section_presets_hint": "Stili integrati — selezionabili direttamente nel briefing. Non modificabili; crea uno stile personale per gli adattamenti.",
"badge_template": "Modello",
"section_my_styles": "I miei stili",
"loading": "Caricamento…",
"empty_my_styles_pre": "Ancora nessuno stile personale. Clicca sopra ",
"empty_my_styles_strong": "+ Stile personale",
"empty_my_styles_post": " per crearne uno — es. \"Il mio tono corporate\" o \"Voce blog personale\".",
"action_edit": "Modifica",
"action_delete": "Elimina",
"confirm_delete": "Eliminare davvero \"{name}\"?"
},
"style_form": {
"label_name": "Nome",
"placeholder_name": "Il mio tono corporate",
"label_description": "Descrizione",
"label_description_hint": "(l'IA lo legge alla lettera — sii concreto)",
"placeholder_description": "es. \"Frasi brevi, voce attiva, niente buzzword. Prima persona singolare, tu informale. Max 3 frasi per paragrafo. Ogni sezione termina con una prossima azione concreta.\"",
"cancel": "Annulla",
"saving": "Salvataggio…",
"submit_create": "Crea stile",
"submit_update": "Salva"
},
"version_history": {
"badge_ai": "IA",
"badge_ai_title": "Generato dall'IA",
"badge_active": "Attiva",
"word_count": "{count} parole",
"cost_title": "Uso token + modello della generazione collegata",
"tokens_label": "{input} → {output} token",
"restore": "Ripristina"
},
"version_editor": {
"placeholder": "Scrivi qui (o lascia all'IA). Vuoto per generare.",
"word_count": "{count} parole",
"target_words": " / obiettivo ~{words}",
"saving": "Salvataggio…",
"saved": "Salvato"
},
"export_menu": {
"trigger": "📤 Esporta",
"title": "Esporta / pubblica",
"copy_md": "📋 Copia markdown",
"copy_text": "📋 Copia testo",
"download_md": "↓ Scarica come .md",
"print_pdf": "🖨 Stampa / PDF",
"save_as_article": "📚 Salva come articolo",
"toast_md_copied": "✓ Markdown copiato",
"toast_text_copied": "✓ Testo copiato",
"toast_copy_failed": "Copia fallita",
"toast_downloaded": "↓ Scaricato",
"toast_saved_article": "✓ Salvato come articolo",
"site_name": "Writing"
},
"reference_picker": {
"label_url_default": "Link",
"label_kontext": "Documento di contesto",
"label_unknown": "—",
"label_article_missing": "Articolo (mancante)",
"label_note_missing": "Nota (mancante)",
"label_note_untitled": "Senza titolo",
"label_library_missing": "Voce di libreria (mancante)",
"label_goal_missing": "Obiettivo (mancante)",
"label_image_missing": "Immagine (mancante)",
"label_image_kind_fallback": "Immagine {kind}",
"add_label": "+ Fonte:",
"max_reached": "Max. {max} fonti per bozza raggiunto. Rimuovine una per aggiungerne una nuova.",
"kind_article": "📄 Articolo",
"kind_note": "📝 Nota",
"kind_library": "📚 Libreria",
"kind_kontext": "🗂 Contesto",
"kind_goal": "🎯 Obiettivo",
"kind_me-image": "🖼 Immagine",
"kind_url": "🔗 URL",
"search_placeholder": "Cerca…",
"no_results": "Nessun risultato.",
"no_goals": "Nessun obiettivo definito.",
"no_me_images": "Nessuna immagine. Aggiungile in /profile/me-images.",
"kontext_empty_pre": "Questo spazio non ha ancora un documento di contesto. Crealo in ",
"kontext_empty_post": ".",
"kontext_link": "Collega il documento di contesto",
"url_placeholder": "https://…",
"url_note_placeholder": "Contesto (opzionale)",
"url_add": "Aggiungi"
}
}

View file

@ -5,6 +5,7 @@
but the full form remains the canonical source-of-truth view.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { KIND_LABELS, TONE_PRESETS, LENGTH_PRESETS, DEFAULT_LANGUAGE } from '../constants';
import { draftsStore } from '../stores/drafts.svelte';
import { callWritingGeneration } from '../api';
@ -88,7 +89,7 @@
if (suggestingTitle) return;
const trimmedTopic = topic.trim();
if (!trimmedTopic) {
error = 'Bitte erst ein Thema eingeben — der Vorschlag braucht Kontext.';
error = $_('writing.briefing_form.err_no_topic');
return;
}
suggestingTitle = true;
@ -181,13 +182,13 @@
<form class="briefing" onsubmit={submit}>
<div class="row">
<label>
<span>Titel</span>
<span>{$_('writing.briefing_form.label_title')}</span>
<div class="title-row">
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
bind:value={title}
placeholder="Mein Blogpost über …"
placeholder={$_('writing.briefing_form.placeholder_title')}
required
autofocus
/>
@ -196,19 +197,21 @@
class="suggest-btn"
onclick={suggestTitle}
disabled={suggestingTitle || !topic.trim()}
title={topic.trim() ? 'Titel aus Briefing + Inhalt vorschlagen' : 'Erst Thema ausfüllen'}
title={topic.trim()
? $_('writing.briefing_form.suggest_title')
: $_('writing.briefing_form.suggest_title_no_topic')}
>
{suggestingTitle ? '…' : '✨'}
</button>
</div>
</label>
<label class="kind-select">
<span>Textart</span>
<span>{$_('writing.briefing_form.label_kind')}</span>
<select bind:value={kind}>
{#each KIND_ORDER as k (k)}
<option value={k}>
{KIND_LABELS[k].emoji}
{KIND_LABELS[k].de}
{$_('writing.kinds.' + k)}
</option>
{/each}
</select>
@ -216,26 +219,33 @@
</div>
<label>
<span>Worum geht's? <small>(wird als Kern-Briefing an die KI übergeben)</small></span>
<span
>{$_('writing.briefing_form.label_topic')}
<small>{$_('writing.briefing_form.label_topic_hint')}</small></span
>
<textarea
bind:value={topic}
rows="3"
placeholder="z.B. 'Was Mana von klassischen Produktivitätstools unterscheidet, aus Nutzersicht'"
placeholder={$_('writing.briefing_form.placeholder_topic')}
required
></textarea>
</label>
<div class="row">
<label>
<span>Zielgruppe</span>
<input type="text" bind:value={audience} placeholder="z.B. Gründer, Eltern, …" />
<span>{$_('writing.briefing_form.label_audience')}</span>
<input
type="text"
bind:value={audience}
placeholder={$_('writing.briefing_form.placeholder_audience')}
/>
</label>
<label>
<span>Ton</span>
<span>{$_('writing.briefing_form.label_tone')}</span>
<select bind:value={tone}>
<option value="">— kein fester Ton —</option>
<option value="">{$_('writing.briefing_form.tone_none')}</option>
{#each TONE_PRESETS as preset (preset.id)}
<option value={preset.id}>{preset.de}</option>
<option value={preset.id}>{$_('writing.tones.' + preset.id)}</option>
{/each}
</select>
</label>
@ -243,41 +253,46 @@
<div class="row">
<label>
<span>Länge (Wörter)</span>
<span>{$_('writing.briefing_form.label_target_length')}</span>
<input type="number" min="20" max="20000" step="20" bind:value={targetLengthValue} />
</label>
<label>
<span>Sprache</span>
<span>{$_('writing.briefing_form.label_language')}</span>
<select bind:value={language}>
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="it">Italiano</option>
<option value="de">{$_('writing.briefing_form.language_de')}</option>
<option value="en">{$_('writing.briefing_form.language_en')}</option>
<option value="fr">{$_('writing.briefing_form.language_fr')}</option>
<option value="es">{$_('writing.briefing_form.language_es')}</option>
<option value="it">{$_('writing.briefing_form.language_it')}</option>
</select>
</label>
</div>
<label>
<span>
Stil <small>(optional — prägt Ton & Struktur der Generation)</small>
{$_('writing.briefing_form.label_style')}
<small>{$_('writing.briefing_form.label_style_hint')}</small>
</span>
<StylePicker value={styleId} onchange={(next) => (styleId = next)} />
</label>
<div class="references-field">
<span class="field-label">
Quellen <small>(optional — flowen als Kontext in den Prompt ein)</small>
{$_('writing.briefing_form.label_references')}
<small>{$_('writing.briefing_form.label_references_hint')}</small>
</span>
<ReferencePicker {references} onchange={(next) => (references = next)} />
</div>
<label>
<span>Zusatzhinweise <small>(optional)</small></span>
<span
>{$_('writing.briefing_form.label_extra')}
<small>{$_('writing.briefing_form.label_extra_hint')}</small></span
>
<textarea
bind:value={extraInstructions}
rows="2"
placeholder="z.B. 'keine Buzzwords', 'mit einem Zitat beginnen', …"
placeholder={$_('writing.briefing_form.placeholder_extra')}
></textarea>
</label>
@ -286,14 +301,16 @@
{/if}
<div class="actions">
<button type="button" class="secondary" onclick={onclose} disabled={saving}> Abbrechen </button>
<button type="button" class="secondary" onclick={onclose} disabled={saving}>
{$_('writing.briefing_form.cancel')}
</button>
<button type="submit" class="primary" disabled={!isValid || saving}>
{#if saving}
Speichert…
{$_('writing.briefing_form.saving')}
{:else if mode === 'create'}
Draft anlegen
{$_('writing.briefing_form.submit_create')}
{:else}
Speichern
{$_('writing.briefing_form.submit_update')}
{/if}
</button>
</div>

View file

@ -10,6 +10,7 @@
component is just the menu surface + confirmation toasts.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { articlesStore } from '$lib/modules/articles/stores/articles.svelte';
import { draftsStore } from '../stores/drafts.svelte';
@ -43,13 +44,17 @@
async function copyMd() {
const ok = await copyTextToClipboard(draftToMarkdown(draft, currentVersion));
flash(ok ? '✓ Markdown kopiert' : 'Kopieren fehlgeschlagen');
flash(
ok ? $_('writing.export_menu.toast_md_copied') : $_('writing.export_menu.toast_copy_failed')
);
open = false;
}
async function copyPlain() {
const ok = await copyTextToClipboard(draftToPlainText(draft, currentVersion));
flash(ok ? '✓ Text kopiert' : 'Kopieren fehlgeschlagen');
flash(
ok ? $_('writing.export_menu.toast_text_copied') : $_('writing.export_menu.toast_copy_failed')
);
open = false;
}
@ -59,7 +64,7 @@
draftToMarkdown(draft, currentVersion),
'text/markdown;charset=utf-8'
);
flash('↓ Heruntergeladen');
flash($_('writing.export_menu.toast_downloaded'));
open = false;
}
@ -80,17 +85,17 @@
// doubles as a back-reference to the source draft.
const article = await articlesStore.saveFromExtracted({
originalUrl: `internal://writing/${draft.id}`,
title: draft.title || draft.briefing.topic || 'Unbenannt',
title: draft.title || draft.briefing.topic || $_('writing.detail_view.untitled_fallback'),
excerpt: content.slice(0, 240).trim() || null,
content,
htmlContent: content, // no HTML body yet — the articles reader handles plain text fine
author: null,
siteName: 'Writing',
siteName: $_('writing.export_menu.site_name'),
wordCount,
readingTimeMinutes: Math.max(1, Math.round(wordCount / 200)),
});
await draftsStore.recordPublish(draft.id, 'articles', article.id);
flash('✓ Als Artikel gespeichert');
flash($_('writing.export_menu.toast_saved_article'));
open = false;
// Give the toast a moment before navigating away.
setTimeout(() => goto(`/articles/${article.id}`), 600);
@ -109,27 +114,27 @@
class:active={open}
onclick={() => (open = !open)}
aria-expanded={open}
title="Exportieren / Veröffentlichen"
title={$_('writing.export_menu.title')}
>
📤 Export
{$_('writing.export_menu.trigger')}
</button>
{#if open}
<div class="dropdown" role="menu">
<button type="button" role="menuitem" onclick={copyMd} disabled={busy}>
📋 Markdown kopieren
{$_('writing.export_menu.copy_md')}
</button>
<button type="button" role="menuitem" onclick={copyPlain} disabled={busy}>
📋 Text kopieren
{$_('writing.export_menu.copy_text')}
</button>
<button type="button" role="menuitem" onclick={downloadMd} disabled={busy}>
↓ Als .md herunterladen
{$_('writing.export_menu.download_md')}
</button>
<button type="button" role="menuitem" onclick={printDraft} disabled={busy}>
🖨 Drucken / PDF
{$_('writing.export_menu.print_pdf')}
</button>
<hr />
<button type="button" role="menuitem" onclick={saveAsArticle} disabled={busy}>
📚 Als Artikel speichern
{$_('writing.export_menu.save_as_article')}
</button>
</div>
{/if}

View file

@ -16,6 +16,7 @@
BriefingForm's save handler.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useAllArticles } from '$lib/modules/articles/queries';
import { useAllNotes } from '$lib/modules/notes/queries';
import { useAllEntries as useAllLibraryEntries } from '$lib/modules/library/queries';
@ -63,28 +64,35 @@
const kontextDoc = $derived(kontext$.value);
function labelFor(ref: DraftReference): string {
if (ref.kind === 'url') return ref.url ?? 'Link';
if (ref.kind === 'kontext') return 'Kontext-Dokument';
if (!ref.targetId) return '—';
if (ref.kind === 'url') return ref.url ?? $_('writing.reference_picker.label_url_default');
if (ref.kind === 'kontext') return $_('writing.reference_picker.label_kontext');
if (!ref.targetId) return $_('writing.reference_picker.label_unknown');
if (ref.kind === 'article') {
const a = articlesById.get(ref.targetId);
return a ? a.title : 'Artikel (fehlt)';
return a ? a.title : $_('writing.reference_picker.label_article_missing');
}
if (ref.kind === 'note') {
const n = notesById.get(ref.targetId);
return n ? n.title || 'Ohne Titel' : 'Notiz (fehlt)';
return n
? n.title || $_('writing.reference_picker.label_note_untitled')
: $_('writing.reference_picker.label_note_missing');
}
if (ref.kind === 'library') {
const e = libraryById.get(ref.targetId);
return e ? e.title : 'Library-Eintrag (fehlt)';
return e ? e.title : $_('writing.reference_picker.label_library_missing');
}
if (ref.kind === 'goal') {
const g = goalsById.get(ref.targetId);
return g ? g.title : 'Ziel (fehlt)';
return g ? g.title : $_('writing.reference_picker.label_goal_missing');
}
if (ref.kind === 'me-image') {
const m = meImagesById.get(ref.targetId);
return m ? (m.label ?? `${m.kind}-Bild`) : 'Bild (fehlt)';
return m
? (m.label ??
$_('writing.reference_picker.label_image_kind_fallback', {
values: { kind: m.kind },
}))
: $_('writing.reference_picker.label_image_missing');
}
return ref.targetId;
}
@ -228,7 +236,7 @@
{#if canAddMore}
<div class="add-row">
<span class="add-label">+ Quelle:</span>
<span class="add-label">{$_('writing.reference_picker.add_label')}</span>
{#each SUPPORTED_KINDS as k (k)}
<button
type="button"
@ -236,30 +244,29 @@
class:active={mode === k}
onclick={() => openMode(k as PickerMode)}
>
{#if k === 'article'}📄 Artikel
{:else if k === 'note'}📝 Notiz
{:else if k === 'library'}📚 Library
{:else if k === 'kontext'}🗂 Kontext
{:else if k === 'goal'}🎯 Ziel
{:else if k === 'me-image'}🖼 Bild
{:else}🔗 URL{/if}
{$_('writing.reference_picker.kind_' + k)}
</button>
{/each}
</div>
{:else}
<p class="muted">
Max. {MAX_REFERENCES} Quellen pro Draft erreicht. Entferne eine, um eine neue hinzuzufügen.
{$_('writing.reference_picker.max_reached', { values: { max: MAX_REFERENCES } })}
</p>
{/if}
{#if mode === 'article' || mode === 'note' || mode === 'library' || mode === 'goal' || mode === 'me-image'}
<div class="search">
<!-- svelte-ignore a11y_autofocus -->
<input type="search" bind:value={searchQuery} placeholder="Suche…" autofocus />
<input
type="search"
bind:value={searchQuery}
placeholder={$_('writing.reference_picker.search_placeholder')}
autofocus
/>
<div class="results">
{#if mode === 'article'}
{#if filteredArticles.length === 0}
<p class="muted small">Keine Treffer.</p>
<p class="muted small">{$_('writing.reference_picker.no_results')}</p>
{:else}
{#each filteredArticles as a (a.id)}
<button
@ -276,7 +283,7 @@
{/if}
{:else if mode === 'note'}
{#if filteredNotes.length === 0}
<p class="muted small">Keine Treffer.</p>
<p class="muted small">{$_('writing.reference_picker.no_results')}</p>
{:else}
{#each filteredNotes as n (n.id)}
<button
@ -284,7 +291,7 @@
class="result"
onclick={() => addRef({ kind: 'note', targetId: n.id, note: null })}
>
<strong>{n.title || 'Ohne Titel'}</strong>
<strong>{n.title || $_('writing.reference_picker.label_note_untitled')}</strong>
{#if n.content}
<span class="meta">
{n.content.slice(0, 80).replace(/\s+/g, ' ')}
@ -296,7 +303,7 @@
{/if}
{:else if mode === 'library'}
{#if filteredLibrary.length === 0}
<p class="muted small">Keine Treffer.</p>
<p class="muted small">{$_('writing.reference_picker.no_results')}</p>
{:else}
{#each filteredLibrary as e (e.id)}
<button
@ -315,7 +322,7 @@
{/if}
{:else if mode === 'goal'}
{#if filteredGoals.length === 0}
<p class="muted small">Keine Ziele angelegt.</p>
<p class="muted small">{$_('writing.reference_picker.no_goals')}</p>
{:else}
{#each filteredGoals as g (g.id)}
<button
@ -333,7 +340,7 @@
{/if}
{:else if mode === 'me-image'}
{#if filteredMeImages.length === 0}
<p class="muted small">Keine Bilder. Lege welche unter /profile/me-images an.</p>
<p class="muted small">{$_('writing.reference_picker.no_me_images')}</p>
{:else}
{#each filteredMeImages as m (m.id)}
<button
@ -345,7 +352,12 @@
<img src={m.thumbnailUrl ?? m.publicUrl} alt="" class="thumb" />
{/if}
<span class="me-image-text">
<strong>{m.label ?? `${m.kind}-Bild`}</strong>
<strong
>{m.label ??
$_('writing.reference_picker.label_image_kind_fallback', {
values: { kind: m.kind },
})}</strong
>
<span class="meta">
{m.kind}{#if m.tags.length}
· {m.tags.join(', ')}
@ -362,12 +374,13 @@
<div class="search">
{#if !kontextDoc}
<p class="muted small">
Dieser Space hat noch kein Kontext-Dokument. Lege eines unter
<a href="/kontext">/kontext</a> an.
{$_('writing.reference_picker.kontext_empty_pre')}<a href="/kontext">/kontext</a>{$_(
'writing.reference_picker.kontext_empty_post'
)}
</p>
{:else}
<button type="button" class="result" onclick={addKontext}>
<strong>Kontext-Dokument verknüpfen</strong>
<strong>{$_('writing.reference_picker.kontext_link')}</strong>
<span class="meta">
{(kontextDoc.content ?? '').slice(0, 100).replace(/\s+/g, ' ')}
{(kontextDoc.content ?? '').length > 100 ? '…' : ''}
@ -378,10 +391,20 @@
{:else if mode === 'url'}
<div class="url-row">
<!-- svelte-ignore a11y_autofocus -->
<input type="url" bind:value={urlInput} placeholder="https://…" autofocus />
<input type="text" bind:value={urlNote} placeholder="Kontext (optional)" class="note-input" />
<input
type="url"
bind:value={urlInput}
placeholder={$_('writing.reference_picker.url_placeholder')}
autofocus
/>
<input
type="text"
bind:value={urlNote}
placeholder={$_('writing.reference_picker.url_note_placeholder')}
class="note-input"
/>
<button type="button" class="primary" disabled={!urlInput.trim()} onclick={addUrl}>
Hinzufügen
{$_('writing.reference_picker.url_add')}
</button>
</div>
{/if}

View file

@ -6,6 +6,7 @@
error message + a "Noch mal" button.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import type { SelectionToolKind } from './SelectionToolbar.svelte';
export interface RefinementState {
@ -40,43 +41,58 @@
<span class="dot" aria-hidden="true"></span>
<strong>{state.toolLabel}</strong>
{#if state.status === 'running'}
<span class="muted">Läuft…</span>
<span class="muted">{$_('writing.refinement_panel.running')}</span>
{:else if state.status === 'failed'}
<span class="err-label">Fehlgeschlagen</span>
<span class="err-label">{$_('writing.refinement_panel.failed')}</span>
{:else}
<span class="muted">Vorschlag bereit</span>
<span class="muted">{$_('writing.refinement_panel.ready')}</span>
{/if}
</div>
<button type="button" class="close" onclick={oncancel} aria-label="Schließen">×</button>
<button
type="button"
class="close"
onclick={oncancel}
aria-label={$_('writing.refinement_panel.close_aria')}>×</button
>
</header>
<div class="cols">
<div class="col">
<h4>Original</h4>
<h4>{$_('writing.refinement_panel.col_original')}</h4>
<p class="text">{state.originalText}</p>
</div>
<div class="col">
<h4>Vorschlag</h4>
<h4>{$_('writing.refinement_panel.col_proposal')}</h4>
{#if state.status === 'running'}
<p class="text muted italic">Generiert…</p>
<p class="text muted italic">{$_('writing.refinement_panel.generating')}</p>
{:else if state.status === 'failed'}
<p class="text err-text">{state.error ?? 'Unbekannter Fehler.'}</p>
<p class="text err-text">{state.error ?? $_('writing.refinement_panel.err_unknown')}</p>
{:else if state.refined}
<p class="text refined">{state.refined}</p>
{:else}
<p class="text muted italic">Kein Ergebnis.</p>
<p class="text muted italic">{$_('writing.refinement_panel.empty_result')}</p>
{/if}
</div>
</div>
<footer>
{#if state.status === 'succeeded'}
<button type="button" class="primary" onclick={onaccept}>Übernehmen</button>
<button type="button" class="secondary" onclick={onretry}>Noch mal</button>
<button type="button" class="secondary" onclick={oncancel}>Verwerfen</button>
<button type="button" class="primary" onclick={onaccept}
>{$_('writing.refinement_panel.action_accept')}</button
>
<button type="button" class="secondary" onclick={onretry}
>{$_('writing.refinement_panel.action_retry')}</button
>
<button type="button" class="secondary" onclick={oncancel}
>{$_('writing.refinement_panel.action_discard')}</button
>
{:else if state.status === 'failed'}
<button type="button" class="primary" onclick={onretry}>Noch mal</button>
<button type="button" class="secondary" onclick={oncancel}>Abbrechen</button>
<button type="button" class="primary" onclick={onretry}
>{$_('writing.refinement_panel.action_retry')}</button
>
<button type="button" class="secondary" onclick={oncancel}
>{$_('writing.refinement_panel.action_cancel')}</button
>
{/if}
</footer>
</section>

View file

@ -7,6 +7,7 @@
separate flows; those don't belong in this form.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { stylesStore } from '../stores/styles.svelte';
import type { WritingStyle } from '../types';
@ -59,20 +60,26 @@
<form class="style-form" onsubmit={submit}>
<label>
<span>Name</span>
<span>{$_('writing.style_form.label_name')}</span>
<!-- svelte-ignore a11y_autofocus -->
<input type="text" bind:value={name} placeholder="Mein Corporate-Ton" required autofocus />
<input
type="text"
bind:value={name}
placeholder={$_('writing.style_form.placeholder_name')}
required
autofocus
/>
</label>
<label>
<span>
Beschreibung
<small>(die KI liest das wörtlich — sei konkret)</small>
{$_('writing.style_form.label_description')}
<small>{$_('writing.style_form.label_description_hint')}</small>
</span>
<textarea
bind:value={description}
rows="5"
placeholder={'z.B. "Kurze Sätze, aktive Formulierungen, keine Buzzwords. Erste-Person-Singular, du-Ansprache. Max. 3 Sätze pro Absatz. Jeder Abschnitt endet mit einer konkreten nächsten Aktion."'}
placeholder={$_('writing.style_form.placeholder_description')}
required
></textarea>
</label>
@ -82,14 +89,16 @@
{/if}
<div class="actions">
<button type="button" class="secondary" onclick={onclose} disabled={saving}>Abbrechen</button>
<button type="button" class="secondary" onclick={onclose} disabled={saving}
>{$_('writing.style_form.cancel')}</button
>
<button type="submit" class="primary" disabled={!isValid || saving}>
{#if saving}
Speichert…
{$_('writing.style_form.saving')}
{:else if mode === 'create'}
Stil anlegen
{$_('writing.style_form.submit_create')}
{:else}
Speichern
{$_('writing.style_form.submit_update')}
{/if}
</button>
</div>

View file

@ -10,6 +10,7 @@
version restore) via the `forceContent` prop.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { draftsStore } from '../stores/drafts.svelte';
import type { DraftVersion } from '../types';
@ -124,20 +125,22 @@
onselect={captureSelection}
onmouseup={captureSelection}
onkeyup={captureSelection}
placeholder="Hier schreibst du (oder die KI). Leer lassen für Generate."
placeholder={$_('writing.version_editor.placeholder')}
spellcheck="true"
></textarea>
<footer>
<span>
{wordCount} Wörter{#if targetWords}
<span class="target"> / Ziel ~{targetWords}</span>
{/if}
{$_('writing.version_editor.word_count', {
values: { count: wordCount },
})}{#if targetWords}<span class="target"
>{$_('writing.version_editor.target_words', { values: { words: targetWords } })}</span
>{/if}
</span>
<span class="status" aria-live="polite">
{#if pending}
Speichert…
{$_('writing.version_editor.saving')}
{:else}
Gespeichert
{$_('writing.version_editor.saved')}
{/if}
</span>
</footer>

View file

@ -9,6 +9,7 @@
see what each draft cost without digging into Workbench audit views.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { draftsStore } from '../stores/drafts.svelte';
import type { DraftVersion, Generation } from '../types';
import { formatDate as formatLocaleDate } from '$lib/i18n/format';
@ -53,7 +54,11 @@
if (!gen) return null;
const parts: string[] = [];
if (gen.tokenUsage) {
parts.push(`${gen.tokenUsage.input} → ${gen.tokenUsage.output} Tokens`);
parts.push(
$_('writing.version_history.tokens_label', {
values: { input: gen.tokenUsage.input, output: gen.tokenUsage.output },
})
);
}
if (gen.durationMs) {
parts.push(`${(gen.durationMs / 1000).toFixed(1)}s`);
@ -71,25 +76,31 @@
<div class="meta">
<strong>v{version.versionNumber}</strong>
{#if version.isAiGenerated}
<span class="tag ai" title="KI-generiert">KI</span>
<span class="tag ai" title={$_('writing.version_history.badge_ai_title')}
>{$_('writing.version_history.badge_ai')}</span
>
{/if}
{#if isCurrent}
<span class="tag current">Aktiv</span>
<span class="tag current">{$_('writing.version_history.badge_active')}</span>
{/if}
</div>
<div class="stats">
<span>{version.wordCount} Wörter</span>
<span
>{$_('writing.version_history.word_count', {
values: { count: version.wordCount },
})}</span
>
<span class="date">{formatDate(version.createdAt)}</span>
</div>
{#if costLine}
<div class="cost" title="Verbrauch + Modell der zugehörigen Generation">{costLine}</div>
<div class="cost" title={$_('writing.version_history.cost_title')}>{costLine}</div>
{/if}
{#if version.summary}
<p class="summary">{version.summary}</p>
{/if}
{#if !isCurrent}
<button type="button" class="restore" onclick={() => restore(version.id)}>
Wiederherstellen
{$_('writing.version_history.restore')}
</button>
{/if}
</li>

View file

@ -8,6 +8,7 @@
-->
<script lang="ts">
import { formatDateTime } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
@ -92,13 +93,13 @@
// still trigger a re-sync.
let forceEditorContent = $state<string | null>(null);
const TOOL_LABEL: Record<SelectionToolKind, string> = {
'selection-shorten': 'Kürzen',
'selection-expand': 'Erweitern',
'selection-tone': 'Ton ändern',
'selection-rewrite': 'Umschreiben',
'selection-translate': 'Übersetzen',
};
const TOOL_LABEL = $derived<Record<SelectionToolKind, string>>({
'selection-shorten': $_('writing.selection_tools.shorten'),
'selection-expand': $_('writing.selection_tools.expand'),
'selection-tone': $_('writing.selection_tools.tone'),
'selection-rewrite': $_('writing.selection_tools.rewrite'),
'selection-translate': $_('writing.selection_tools.translate'),
});
async function setStatus(next: DraftStatus) {
if (!draft) return;
@ -147,7 +148,8 @@
async function remove() {
if (!draft) return;
if (!confirm(`"${draft.title}" wirklich löschen?`)) return;
if (!confirm($_('writing.detail_view.confirm_delete', { values: { title: draft.title } })))
return;
await draftsStore.deleteDraft(draft.id);
goto('/writing');
}
@ -307,11 +309,11 @@
</script>
{#if draft$.loading}
<p class="muted center">Lädt…</p>
<p class="muted center">{$_('writing.detail_view.loading')}</p>
{:else if !draft}
<div class="empty">
<p>Dieser Draft existiert nicht (mehr).</p>
<a href="/writing">Zurück zur Übersicht</a>
<p>{$_('writing.detail_view.not_found_title')}</p>
<a href="/writing">{$_('writing.detail_view.not_found_back')}</a>
</div>
{:else}
<!--
@ -321,7 +323,9 @@
body, without the workbench chrome.
-->
<article class="print-target" aria-hidden="true">
<h1 class="print-title">{draft.title || draft.briefing.topic || 'Unbenannt'}</h1>
<h1 class="print-title">
{draft.title || draft.briefing.topic || $_('writing.detail_view.untitled_fallback')}
</h1>
{#if currentVersion}
<div class="print-body">{currentVersion.content}</div>
{/if}
@ -330,13 +334,15 @@
<div class="shell">
<header class="head">
<div class="title-row">
<a href="/writing" class="back">← Alle Drafts</a>
<a href="/writing" class="back">{$_('writing.detail_view.back_to_drafts')}</a>
<div class="title-block">
<div class="kind" title={kind?.de}>
<div class="kind" title={kind ? $_('writing.kinds.' + draft.kind) : ''}>
<span aria-hidden="true">{kind?.emoji}</span>
{kind?.de}
{$_('writing.kinds.' + draft.kind)}
</div>
<h1>{draft.title || draft.briefing.topic || 'Unbenannt'}</h1>
<h1>
{draft.title || draft.briefing.topic || $_('writing.detail_view.untitled_fallback')}
</h1>
</div>
<div class="actions">
<button
@ -344,11 +350,13 @@
class="ghost"
onclick={toggleFavorite}
aria-pressed={draft.isFavorite}
title="Favorit"
title={$_('writing.detail_view.toggle_favorite_title')}
>
{draft.isFavorite ? '★' : '☆'}
</button>
<button type="button" class="ghost danger" onclick={remove}>Löschen</button>
<button type="button" class="ghost danger" onclick={remove}>
{$_('writing.detail_view.action_delete')}
</button>
</div>
</div>
@ -358,7 +366,7 @@
{#each STATUS_ORDER as s (s)}
{#if s !== draft.status}
<button type="button" class="tiny" onclick={() => setStatus(s)}>
{STATUS_LABELS[s].de}
{$_('writing.statuses.' + s)}
</button>
{/if}
{/each}
@ -369,33 +377,35 @@
</div>
{#if draft.visibility === 'unlisted' && draft.unlistedToken}
<div
class="share-row"
title="Öffentlicher Link kommt mit M10 (Publish-Hooks). Bis dahin: Token kopieren."
>
<span class="share-label">🔗 Unlisted-Token:</span>
<div class="share-row" title={$_('writing.detail_view.share_row_title')}>
<span class="share-label">{$_('writing.detail_view.share_row_label')}</span>
<code class="share-token">{draft.unlistedToken}</code>
<button type="button" class="tiny" onclick={copyShareToken}>
{shareCopied ? '✓ Kopiert' : 'Kopieren'}
{shareCopied
? $_('writing.detail_view.share_row_copied')
: $_('writing.detail_view.share_row_copy')}
</button>
</div>
{/if}
{#if draft.publishedTo.length > 0}
<div class="published-row">
<span class="published-label">📤 Veröffentlicht:</span>
<span class="published-label">{$_('writing.detail_view.published_label')}</span>
{#each draft.publishedTo as target (`${target.module}:${target.targetId}`)}
<span class="published-chip" title={formatDateTime(new Date(target.publishedAt))}>
{#if target.module === 'articles'}
📚 <a href={`/articles/${target.targetId}`}>Artikel</a>
{$_('writing.detail_view.published_articles')}
<a href={`/articles/${target.targetId}`}
>{$_('writing.detail_view.published_articles_link')}</a
>
{:else if target.module === 'website'}
🌐 Website
{$_('writing.detail_view.published_website')}
{:else if target.module === 'presi'}
🎞 Präsi
{$_('writing.detail_view.published_presi')}
{:else if target.module === 'mail'}
✉️ Mail
{$_('writing.detail_view.published_mail')}
{:else if target.module === 'social-relay'}
💬 Social
{$_('writing.detail_view.published_social')}
{:else}
{target.module}
{/if}
@ -407,11 +417,14 @@
<section class="briefing-section">
<button type="button" class="briefing-toggle" onclick={() => (briefingOpen = !briefingOpen)}>
{briefingOpen ? '▾' : '▸'} Briefing
{briefingOpen ? '▾' : '▸'}
{$_('writing.detail_view.briefing_section_label')}
{#if !briefingOpen}
<span class="preview">{draft.briefing.topic}</span>
{#if activeStyleName}
<span class="style-chip" title="Aktiver Stil">🎨 {activeStyleName}</span>
<span class="style-chip" title={$_('writing.detail_view.active_style_title')}
>🎨 {activeStyleName}</span
>
{/if}
{/if}
</button>
@ -425,9 +438,13 @@
{#if currentVersion}
<div class="editor-head">
<div class="version-label">
<strong>Version {currentVersion.versionNumber}</strong>
<strong
>{$_('writing.detail_view.version_label', {
values: { n: currentVersion.versionNumber },
})}</strong
>
{#if currentVersion.isAiGenerated}
<span class="ai-tag">KI</span>
<span class="ai-tag">{$_('writing.detail_view.ai_tag')}</span>
{/if}
</div>
<div class="editor-actions">
@ -437,15 +454,15 @@
onclick={generate}
disabled={generating}
title={hasDraftContent
? 'Kompletten Text neu generieren — neue Version (⌘G)'
: 'Ersten Entwurf aus dem Briefing generieren (⌘G)'}
? $_('writing.detail_view.regenerate_title')
: $_('writing.detail_view.generate_first_title')}
>
{#if generating}
Schreibt…
{$_('writing.detail_view.generating_btn')}
{:else if hasDraftContent}
⟳ Neu generieren
{$_('writing.detail_view.regenerate_btn')}
{:else}
✨ Generate
{$_('writing.detail_view.generate_btn')}
{/if}
</button>
<button
@ -453,9 +470,11 @@
class="checkpoint"
onclick={saveCheckpoint}
disabled={saving}
title="Aktuellen Text als neue Version einfrieren (⌘⇧S)"
title={$_('writing.detail_view.checkpoint_title')}
>
{saving ? 'Speichert…' : ' Checkpoint'}
{saving
? $_('writing.detail_view.checkpoint_saving')
: $_('writing.detail_view.checkpoint_btn')}
</button>
<ExportMenu {draft} {currentVersion} />
</div>
@ -489,9 +508,9 @@
type="button"
class="undo-btn"
onclick={undoLastRefinement}
title="Letzte Auswahl-Verfeinerung rückgängig (⌘Z)"
title={$_('writing.detail_view.undo_title')}
>
↶ Rückgängig: {refineUndo.label}
{$_('writing.detail_view.undo_label', { values: { label: refineUndo.label } })}
</button>
</div>
{/if}
@ -502,12 +521,12 @@
onselect={(sel) => (activeSelection = sel)}
/>
{:else}
<p class="muted">Diese Version existiert nicht mehr.</p>
<p class="muted">{$_('writing.detail_view.version_missing')}</p>
{/if}
</section>
<aside class="history-column">
<h2>Versionen</h2>
<h2>{$_('writing.detail_view.history_heading')}</h2>
<VersionHistory
versions={versions ?? []}
generations={generations ?? []}

View file

@ -10,6 +10,7 @@
unstarted draft.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { onMount, type Component } from 'svelte';
import { goto } from '$app/navigation';
import {
@ -175,14 +176,14 @@
type="search"
class="search"
bind:value={searchQuery}
placeholder="Nach Titel oder Thema suchen…"
placeholder={$_('writing.list_view.search_placeholder')}
/>
{/if}
<a
href="/writing/styles"
class="styles-link"
title="Stile verwalten"
aria-label="Stile verwalten"
title={$_('writing.list_view.styles_title')}
aria-label={$_('writing.list_view.styles_title')}
>
<Palette size={18} weight="regular" />
</a>
@ -193,7 +194,7 @@
onclick={() => (showCreate = !showCreate)}
aria-expanded={showCreate}
>
{showCreate ? '× Schließen' : '+ Neuer Draft'}
{showCreate ? $_('writing.list_view.close_btn') : $_('writing.list_view.new_draft_btn')}
</button>
</div>
@ -204,7 +205,7 @@
<StatusFilter active={activeStatus} onselect={(s) => (activeStatus = s)} />
<label class="fav-toggle">
<input type="checkbox" bind:checked={showFavoritesOnly} />
<span>Nur Favoriten</span>
<span>{$_('writing.list_view.fav_only')}</span>
</label>
</div>
</div>
@ -222,27 +223,24 @@
{/if}
{#if drafts$.loading}
<p class="muted center">Lädt…</p>
<p class="muted center">{$_('writing.list_view.loading')}</p>
{:else if isEmpty && !showCreate}
<!-- Hero empty-state: the "What is this?" view. -->
<section class="hero">
<div class="hero-icon" aria-hidden="true">
<NotePencil size={40} weight="duotone" />
</div>
<h2>Dein KI-Ghostwriter</h2>
<p class="hero-pitch">
Brief Thema, Stil und Quellen — ein fertiger Entwurf entsteht. Verfeinere ihn absatzweise
mit ⌘G zum Generieren, Markieren + Selection-Tools, oder direkt im Editor.
</p>
<h2>{$_('writing.list_view.hero_title')}</h2>
<p class="hero-pitch">{$_('writing.list_view.hero_pitch')}</p>
<ul class="hero-meta">
<li><Sparkle size={12} weight="fill" /> 12 Textarten</li>
<li>9 Stile</li>
<li>7 Quellen</li>
<li>E2E-verschlüsselt</li>
<li><Sparkle size={12} weight="fill" /> {$_('writing.list_view.hero_meta_kinds')}</li>
<li>{$_('writing.list_view.hero_meta_styles')}</li>
<li>{$_('writing.list_view.hero_meta_references')}</li>
<li>{$_('writing.list_view.hero_meta_e2e')}</li>
</ul>
<div class="quick-start">
<p class="quick-start-label">Schnellstart</p>
<p class="quick-start-label">{$_('writing.list_view.quick_start_label')}</p>
<div class="quick-grid">
{#each QUICK_START_KINDS as kind (kind)}
{@const Icon = QUICK_ICON[kind]}
@ -250,12 +248,14 @@
type="button"
class="quick-tile"
onclick={() => startWithKind(kind)}
title={`Neuer ${KIND_LABELS[kind].de}-Entwurf`}
title={$_('writing.list_view.quick_start_title_template', {
values: { kind: $_('writing.kinds.' + kind) },
})}
>
<span class="quick-icon" aria-hidden="true">
<Icon size={20} weight="regular" />
</span>
<span class="quick-label">{KIND_LABELS[kind].de}</span>
<span class="quick-label">{$_('writing.kinds.' + kind)}</span>
</button>
{/each}
</div>
@ -263,7 +263,7 @@
</section>
{:else if filtered.length === 0}
<div class="empty">
<p class="muted">Keine Drafts passen zum aktuellen Filter.</p>
<p class="muted">{$_('writing.list_view.empty_filtered')}</p>
</div>
{:else}
<div class="grid">

View file

@ -8,11 +8,11 @@
from a batch of user samples) via a separate flow.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { STYLE_PRESETS } from '../presets/styles';
import { useAllStyles } from '../queries';
import { stylesStore } from '../stores/styles.svelte';
import StyleForm from '../components/StyleForm.svelte';
import { STYLE_SOURCE_LABELS } from '../constants';
import type { WritingStyle } from '../types';
const styles$ = useAllStyles();
@ -25,17 +25,18 @@
);
async function remove(style: WritingStyle) {
if (!confirm(`"${style.name}" wirklich löschen?`)) return;
if (!confirm($_('writing.styles_view.confirm_delete', { values: { name: style.name } })))
return;
await stylesStore.deleteStyle(style.id);
}
</script>
<div class="styles-shell">
<header class="head">
<a href="/writing" class="back">← Zurück zu Writing</a>
<a href="/writing" class="back">{$_('writing.styles_view.back_to_writing')}</a>
<div>
<h1>Stile</h1>
<p class="muted">Vorlagen und eigene Stile, die der Ghostwriter beim Generieren anwendet.</p>
<h1>{$_('writing.styles_view.title')}</h1>
<p class="muted">{$_('writing.styles_view.subtitle')}</p>
</div>
<button
type="button"
@ -43,7 +44,7 @@
class:active={createOpen}
onclick={() => (createOpen = !createOpen)}
>
{createOpen ? '× Schließen' : '+ Eigener Stil'}
{createOpen ? $_('writing.styles_view.close_btn') : $_('writing.styles_view.create_btn')}
</button>
</header>
@ -54,17 +55,14 @@
{/if}
<section>
<h2>Vorlagen</h2>
<p class="muted small">
Eingebaute Stile — direkt im Briefing auswählbar. Nicht bearbeitbar; für Anpassungen lege
einen eigenen Stil an.
</p>
<h2>{$_('writing.styles_view.section_presets')}</h2>
<p class="muted small">{$_('writing.styles_view.section_presets_hint')}</p>
<div class="grid">
{#each STYLE_PRESETS as preset (preset.id)}
<article class="card preset">
<header class="card-head">
<strong>{preset.name.de}</strong>
<span class="tag">Vorlage</span>
<span class="tag">{$_('writing.styles_view.badge_template')}</span>
</header>
<p class="desc">{preset.description.de}</p>
{#if preset.principles.toneTraits.length}
@ -80,13 +78,14 @@
</section>
<section>
<h2>Meine Stile</h2>
<h2>{$_('writing.styles_view.section_my_styles')}</h2>
{#if styles$.loading}
<p class="muted small">Lädt…</p>
<p class="muted small">{$_('writing.styles_view.loading')}</p>
{:else if customStyles.length === 0}
<p class="muted small">
Keine eigenen Stile. Klick oben auf <strong>+ Eigener Stil</strong>, um einen anzulegen —
z.B. "Mein Corporate-Ton" oder "Persönliche Blog-Stimme".
{$_('writing.styles_view.empty_my_styles_pre')}<strong
>{$_('writing.styles_view.empty_my_styles_strong')}</strong
>{$_('writing.styles_view.empty_my_styles_post')}
</p>
{:else}
<div class="grid">
@ -94,7 +93,7 @@
<article class="card" class:editing={editingId === style.id}>
<header class="card-head">
<strong>{style.name}</strong>
<span class="tag">{STYLE_SOURCE_LABELS[style.source].de}</span>
<span class="tag">{$_('writing.style_sources.' + style.source)}</span>
</header>
{#if editingId === style.id}
<StyleForm mode="edit" {style} onclose={() => (editingId = null)} />
@ -109,10 +108,10 @@
{/if}
<div class="actions">
<button type="button" class="tiny" onclick={() => (editingId = style.id)}>
Bearbeiten
{$_('writing.styles_view.action_edit')}
</button>
<button type="button" class="tiny danger" onclick={() => remove(style)}>
Löschen
{$_('writing.styles_view.action_delete')}
</button>
</div>
{/if}

View file

@ -261,16 +261,6 @@
"apps/mana/apps/web/src/lib/modules/who/views/PlayView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/wishes/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/wishes/views/DetailView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/writing/components/BriefingForm.svelte": 11,
"apps/mana/apps/web/src/lib/modules/writing/components/ExportMenu.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/components/ReferencePicker.svelte": 4,
"apps/mana/apps/web/src/lib/modules/writing/components/RefinementPanel.svelte": 12,
"apps/mana/apps/web/src/lib/modules/writing/components/StyleForm.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/components/VersionEditor.svelte": 1,
"apps/mana/apps/web/src/lib/modules/writing/components/VersionHistory.svelte": 2,
"apps/mana/apps/web/src/lib/modules/writing/views/DetailView.svelte": 7,
"apps/mana/apps/web/src/lib/modules/writing/views/ListView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/writing/views/StylesView.svelte": 4,
"apps/mana/apps/web/src/routes/(app)/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/admin/+layout.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/admin/user-data/[userId]/+page.svelte": 19,