mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
feat(settings): full i18n coverage — DE/EN/ES/FR/IT
Settings page on the workbench was 100% hardcoded German across 14
files / ~5200 LOC. Added a `settings` namespace (~280 keys × 5 locales)
and wired every component through `$_()`.
- New apps/mana/apps/web/src/lib/i18n/locales/settings/{de,en,es,fr,it}.json
- searchIndex.ts now exports `getCategories(t)` + `searchSettings(t, q)`;
hash-anchor lookup goes through the locale-free `findCategoryByAnchor`
- VaultSection (recovery-code wizard, ZK opt-in, key rotate) + AiSettings
(4 tier cards), MyDataSection (DSGVO retention/danger-zone),
SyncSection, PrivacySection translated end-to-end
- Locale-aware Date/Number formatting (toLocaleString uses get(locale))
in SyncSection + MyDataSection
- validate:i18n-parity: 38 namespaces × 5 locales — 3323 keys aligned
- svelte-check: 7639 files, 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fea3adf5fe
commit
30eb7ef72d
18 changed files with 2614 additions and 551 deletions
|
|
@ -7,6 +7,7 @@
|
|||
* means for privacy + speed + cost. The cloud tier is gated behind
|
||||
* an additional consent step the first time it's enabled.
|
||||
*/
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
|
||||
import {
|
||||
isLocalLlmSupported,
|
||||
|
|
@ -62,53 +63,53 @@
|
|||
warning?: string;
|
||||
};
|
||||
|
||||
const tierCards: TierCard[] = [
|
||||
const tierCards = $derived<TierCard[]>([
|
||||
{
|
||||
tier: 'browser',
|
||||
icon: Cpu,
|
||||
title: 'Auf deinem Gerät',
|
||||
subtitle: 'Gemma 4 E2B (Google) im Browser',
|
||||
title: $_('settings.ai.tier_browser_title'),
|
||||
subtitle: $_('settings.ai.tier_browser_subtitle'),
|
||||
bullets: [
|
||||
'100% lokal — Daten verlassen dein Gerät nicht',
|
||||
'~500 MB einmaliger Download',
|
||||
'Braucht WebGPU + 2 GB freien GPU-Speicher',
|
||||
$_('settings.ai.tier_browser_bullet_1'),
|
||||
$_('settings.ai.tier_browser_bullet_2'),
|
||||
$_('settings.ai.tier_browser_bullet_3'),
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'mana-server',
|
||||
icon: HardDrive,
|
||||
title: 'Mana-Server',
|
||||
subtitle: 'Selbst-gehostet auf unserem Mac Mini',
|
||||
title: $_('settings.ai.tier_mana_server_title'),
|
||||
subtitle: $_('settings.ai.tier_mana_server_subtitle'),
|
||||
bullets: [
|
||||
'Schneller und stärker als Browser-LLM',
|
||||
'Daten laufen verschlüsselt zu unserem Server',
|
||||
'Keine Inhalte werden gespeichert',
|
||||
$_('settings.ai.tier_mana_server_bullet_1'),
|
||||
$_('settings.ai.tier_mana_server_bullet_2'),
|
||||
$_('settings.ai.tier_mana_server_bullet_3'),
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'byok',
|
||||
icon: Key,
|
||||
title: 'Eigener API-Key',
|
||||
subtitle: 'OpenAI, Anthropic, Google Gemini oder Mistral',
|
||||
title: $_('settings.ai.tier_byok_title'),
|
||||
subtitle: $_('settings.ai.tier_byok_subtitle'),
|
||||
bullets: [
|
||||
'Direkt aus dem Browser — keine Mana-Server-Zwischenstation',
|
||||
'Du zahlst beim Provider, wir sehen nichts davon',
|
||||
'Schlüssel werden verschlüsselt in deinem Vault gespeichert',
|
||||
$_('settings.ai.tier_byok_bullet_1'),
|
||||
$_('settings.ai.tier_byok_bullet_2'),
|
||||
$_('settings.ai.tier_byok_bullet_3'),
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'cloud',
|
||||
icon: Cloud,
|
||||
title: 'Google Gemini',
|
||||
subtitle: 'Cloud-API über unseren Server-Proxy',
|
||||
title: $_('settings.ai.tier_cloud_title'),
|
||||
subtitle: $_('settings.ai.tier_cloud_subtitle'),
|
||||
bullets: [
|
||||
'Stärkste Qualität für komplexe Aufgaben',
|
||||
'Schnellste Antworten',
|
||||
'Daten werden von Google verarbeitet',
|
||||
$_('settings.ai.tier_cloud_bullet_1'),
|
||||
$_('settings.ai.tier_cloud_bullet_2'),
|
||||
$_('settings.ai.tier_cloud_bullet_3'),
|
||||
],
|
||||
warning: 'Nur empfehlenswert für nicht-sensitive Inhalte',
|
||||
warning: $_('settings.ai.tier_cloud_warning'),
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
function isEnabled(tier: LlmTier): boolean {
|
||||
return settings.allowedTiers.includes(tier);
|
||||
|
|
@ -125,15 +126,11 @@
|
|||
</div>
|
||||
<div class="tier-body">
|
||||
<div class="tier-title-line">
|
||||
<span class="tier-title">Lokal (ohne KI)</span>
|
||||
<span class="tier-badge tier-badge-always">immer aktiv</span>
|
||||
<span class="tier-title">{$_('settings.ai.tier_local_title')}</span>
|
||||
<span class="tier-badge tier-badge-always">{$_('settings.ai.tier_local_badge')}</span>
|
||||
</div>
|
||||
<p class="tier-subtitle">Basis-Funktionen ohne jede KI</p>
|
||||
<p class="tier0-desc">
|
||||
Mana funktioniert auch ganz ohne KI: Datum-Erkennung, Suche und einfache Klassifikation
|
||||
laufen über klassische Algorithmen. Manche Funktionen sind dann begrenzt, dafür ist alles
|
||||
100% offline und kostet nichts.
|
||||
</p>
|
||||
<p class="tier-subtitle">{$_('settings.ai.tier_local_subtitle')}</p>
|
||||
<p class="tier0-desc">{$_('settings.ai.tier_local_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -157,7 +154,7 @@
|
|||
<div class="tier-title-line">
|
||||
<span class="tier-title">{card.title}</span>
|
||||
{#if enabled}
|
||||
<span class="tier-badge">aktiv</span>
|
||||
<span class="tier-badge">{$_('settings.ai.badge_active')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="tier-subtitle">{card.subtitle}</p>
|
||||
|
|
@ -176,12 +173,15 @@
|
|||
{#if card.tier === 'browser' && enabled && webgpuSupported}
|
||||
<div class="tier-extra">
|
||||
{#if browserCacheReady}
|
||||
<span class="status-ok">✓ Modell geladen</span>
|
||||
<span class="status-ok">{$_('settings.ai.tier_browser_loaded')}</span>
|
||||
{:else if localLlmStatus.current.state === 'downloading'}
|
||||
<span class="status-muted">
|
||||
Lade {defaultModelInfo.displayName} ({(
|
||||
localLlmStatus.current.progress * 100
|
||||
).toFixed(0)}%)…
|
||||
{$_('settings.ai.tier_browser_loading', {
|
||||
values: {
|
||||
name: defaultModelInfo.displayName,
|
||||
percent: (localLlmStatus.current.progress * 100).toFixed(0),
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
|
|
@ -195,8 +195,10 @@
|
|||
class="action-btn"
|
||||
>
|
||||
{loadingBrowser
|
||||
? 'Lade…'
|
||||
: `Modell laden (~${defaultModelInfo.downloadSizeMb} MB)`}
|
||||
? $_('settings.ai.tier_browser_load_btn_loading')
|
||||
: $_('settings.ai.tier_browser_load_btn', {
|
||||
values: { size: defaultModelInfo.downloadSizeMb },
|
||||
})}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -204,8 +206,7 @@
|
|||
|
||||
{#if card.tier === 'browser' && tierBlocked}
|
||||
<p class="tier-blocked-note">
|
||||
WebGPU nicht verfügbar in deinem Browser. Funktioniert in Chrome/Edge 113+ oder Safari
|
||||
18+.
|
||||
{$_('settings.ai.tier_browser_blocked')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
|
|
@ -227,11 +228,8 @@
|
|||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<p class="consent-title">Bestätigung erforderlich</p>
|
||||
<p class="consent-desc">
|
||||
Cloud-Anfragen senden deine Inhalte an Google. Bitte bestätige, dass du das
|
||||
verstanden hast und akzeptierst.
|
||||
</p>
|
||||
<p class="consent-title">{$_('settings.ai.tier_cloud_consent_title')}</p>
|
||||
<p class="consent-desc">{$_('settings.ai.tier_cloud_consent_desc')}</p>
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -241,7 +239,7 @@
|
|||
}}
|
||||
class="consent-btn"
|
||||
>
|
||||
Verstanden, Cloud aktivieren
|
||||
{$_('settings.ai.tier_cloud_consent_btn')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -253,7 +251,7 @@
|
|||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<span class="status-ok">✓ Cloud-Zustimmung erteilt</span>
|
||||
<span class="status-ok">{$_('settings.ai.tier_cloud_consent_ok')}</span>
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -263,7 +261,7 @@
|
|||
}}
|
||||
class="link-btn"
|
||||
>
|
||||
Zurücknehmen
|
||||
{$_('settings.ai.tier_cloud_consent_revoke')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -274,12 +272,14 @@
|
|||
|
||||
<!-- Active tier chain summary -->
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Aktuelle Reihenfolge:</span>
|
||||
<span class="summary-label">{$_('settings.ai.summary_label')}</span>
|
||||
<span class="summary-value">
|
||||
{#if settings.allowedTiers.length === 0}
|
||||
Nur lokal (ohne KI) — die meisten KI-Funktionen sind begrenzt.
|
||||
{$_('settings.ai.summary_local_only')}
|
||||
{:else}
|
||||
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback)
|
||||
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → {$_(
|
||||
'settings.ai.summary_chain_local_fallback'
|
||||
)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -288,18 +288,15 @@
|
|||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Bei Fehler auf „Lokal" zurückfallen</p>
|
||||
<p class="row-desc">
|
||||
Wenn die gewählte KI-Schicht eine Anfrage nicht beantworten kann, versucht Mana es mit der
|
||||
lokalen Variante (sofern verfügbar). Aus: zeigt stattdessen einen Fehler an.
|
||||
</p>
|
||||
<p class="row-title">{$_('settings.ai.fallback_title')}</p>
|
||||
<p class="row-desc">{$_('settings.ai.fallback_description')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
class:on={settings.fallbackToRulesOnError}
|
||||
onclick={() => setFallback(!settings.fallbackToRulesOnError)}
|
||||
aria-label="Fallback auf Lokal"
|
||||
aria-label={$_('settings.ai.fallback_aria')}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
|
|
@ -307,19 +304,15 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Quelle bei jedem KI-Resultat anzeigen</p>
|
||||
<p class="row-desc">
|
||||
Zeigt unter jeder KI-generierten Antwort eine kleine Markierung wie „Auf deinem Gerät"
|
||||
oder „via Google Gemini" — damit du immer siehst, wo deine Daten gerade verarbeitet
|
||||
wurden.
|
||||
</p>
|
||||
<p class="row-title">{$_('settings.ai.show_source_title')}</p>
|
||||
<p class="row-desc">{$_('settings.ai.show_source_description')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
class:on={settings.showSourceInUi}
|
||||
onclick={() => setShowSource(!settings.showSourceInUi)}
|
||||
aria-label="Quelle anzeigen"
|
||||
aria-label={$_('settings.ai.show_source_aria')}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, Trash, Key, PencilSimple, Check, X } from '@mana/shared-icons';
|
||||
import { BUILTIN_BYOK_PROVIDERS, formatCost, type ByokProviderId } from '@mana/shared-llm';
|
||||
import { byokVault } from '$lib/byok';
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
async function handleAdd() {
|
||||
addError = null;
|
||||
if (!addLabel.trim() || !addApiKey.trim()) {
|
||||
addError = 'Label und API-Key sind Pflicht';
|
||||
addError = $_('settings.byok.label_required');
|
||||
return;
|
||||
}
|
||||
saving = true;
|
||||
|
|
@ -68,7 +69,7 @@
|
|||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!confirm('Schluessel wirklich loeschen?')) return;
|
||||
if (!confirm($_('settings.byok.confirm_delete'))) return;
|
||||
await byokVault.delete(id);
|
||||
await reload();
|
||||
}
|
||||
|
|
@ -115,9 +116,9 @@
|
|||
|
||||
<div class="byok-manager">
|
||||
{#if vaultLocked}
|
||||
<div class="notice">Vault ist gesperrt — bitte zuerst anmelden um Keys zu verwalten.</div>
|
||||
<div class="notice">{$_('settings.byok.vault_locked')}</div>
|
||||
{:else if loading}
|
||||
<div class="notice subtle">Laedt...</div>
|
||||
<div class="notice subtle">{$_('settings.byok.loading')}</div>
|
||||
{:else}
|
||||
{#if keys.length > 0}
|
||||
<div class="keys-list">
|
||||
|
|
@ -129,10 +130,10 @@
|
|||
bind:value={editLabel}
|
||||
maxlength="40"
|
||||
class="edit-input"
|
||||
placeholder="Label"
|
||||
placeholder={$_('settings.byok.field_label')}
|
||||
/>
|
||||
<select bind:value={editModel} class="edit-input">
|
||||
<option value="">Default</option>
|
||||
<option value="">{$_('settings.byok.field_default_short')}</option>
|
||||
{#each providerModels(k.provider) as m}
|
||||
<option value={m}>{m}</option>
|
||||
{/each}
|
||||
|
|
@ -144,21 +145,39 @@
|
|||
<div class="key-info">
|
||||
<div class="key-line">
|
||||
<span class="key-label">{k.label}</span>
|
||||
{#if k.isDefault}<span class="badge">Standard</span>{/if}
|
||||
{#if k.isDefault}
|
||||
<span class="badge">{$_('settings.byok.badge_default')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="key-meta">
|
||||
{providerDisplay(k.provider)} · {k.model || providerDefaultModel(k.provider)} ·
|
||||
{k.usageCount} Aufrufe · {formatCost(k.totalCostUsd)}
|
||||
{$_('settings.byok.meta_line', {
|
||||
values: {
|
||||
provider: providerDisplay(k.provider),
|
||||
model: k.model || providerDefaultModel(k.provider),
|
||||
count: k.usageCount,
|
||||
cost: formatCost(k.totalCostUsd),
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="key-actions">
|
||||
{#if !k.isDefault}
|
||||
<button class="btn-link" onclick={() => handleSetDefault(k.id)}>Standard</button>
|
||||
<button class="btn-link" onclick={() => handleSetDefault(k.id)}
|
||||
>{$_('settings.byok.set_default')}</button
|
||||
>
|
||||
{/if}
|
||||
<button class="btn-icon" onclick={() => startEdit(k)} title="Bearbeiten">
|
||||
<button
|
||||
class="btn-icon"
|
||||
onclick={() => startEdit(k)}
|
||||
title={$_('settings.byok.edit')}
|
||||
>
|
||||
<PencilSimple size={12} />
|
||||
</button>
|
||||
<button class="btn-icon danger" onclick={() => handleDelete(k.id)} title="Loeschen">
|
||||
<button
|
||||
class="btn-icon danger"
|
||||
onclick={() => handleDelete(k.id)}
|
||||
title={$_('settings.byok.delete')}
|
||||
>
|
||||
<Trash size={12} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -172,7 +191,7 @@
|
|||
<div class="add-form">
|
||||
<div class="form-row">
|
||||
<label class="field flex-1">
|
||||
<span class="field-label">Provider</span>
|
||||
<span class="field-label">{$_('settings.byok.field_provider')}</span>
|
||||
<select
|
||||
bind:value={addProvider}
|
||||
onchange={() => (addModel = providerDefaultModel(addProvider))}
|
||||
|
|
@ -183,17 +202,17 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="field flex-1">
|
||||
<span class="field-label">Label</span>
|
||||
<span class="field-label">{$_('settings.byok.field_label')}</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={addLabel}
|
||||
placeholder="z.B. Privat OpenAI"
|
||||
placeholder={$_('settings.byok.field_label_placeholder')}
|
||||
maxlength="40"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span class="field-label">API-Key</span>
|
||||
<span class="field-label">{$_('settings.byok.field_api_key')}</span>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={addApiKey}
|
||||
|
|
@ -203,15 +222,19 @@
|
|||
? 'sk-ant-...'
|
||||
: addProvider === 'gemini'
|
||||
? 'AIza...'
|
||||
: 'API-Key'}
|
||||
: $_('settings.byok.field_api_key_placeholder_generic')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<div class="form-row">
|
||||
<label class="field flex-1">
|
||||
<span class="field-label">Modell</span>
|
||||
<span class="field-label">{$_('settings.byok.field_model')}</span>
|
||||
<select bind:value={addModel}>
|
||||
<option value="">Default ({providerDefaultModel(addProvider)})</option>
|
||||
<option value=""
|
||||
>{$_('settings.byok.field_model_default', {
|
||||
values: { model: providerDefaultModel(addProvider) },
|
||||
})}</option
|
||||
>
|
||||
{#each providerModels(addProvider) as m}
|
||||
<option value={m}>{m}</option>
|
||||
{/each}
|
||||
|
|
@ -219,23 +242,25 @@
|
|||
</label>
|
||||
<label class="checkbox-field">
|
||||
<input type="checkbox" bind:checked={addIsDefault} />
|
||||
<span>Als Standard</span>
|
||||
<span>{$_('settings.byok.field_as_default')}</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if addError}
|
||||
<div class="error">{addError}</div>
|
||||
{/if}
|
||||
<div class="form-actions">
|
||||
<button class="btn-cancel" onclick={() => (showAdd = false)}>Abbrechen</button>
|
||||
<button class="btn-cancel" onclick={() => (showAdd = false)}
|
||||
>{$_('settings.byok.cancel')}</button
|
||||
>
|
||||
<button class="btn-primary" onclick={handleAdd} disabled={saving}>
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
{saving ? $_('settings.byok.saving') : $_('settings.byok.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button class="add-button" onclick={() => (showAdd = true)}>
|
||||
<Plus size={14} weight="bold" />
|
||||
{keys.length === 0 ? 'Ersten API-Key hinzufuegen' : 'Weiteren Key hinzufuegen'}
|
||||
{keys.length === 0 ? $_('settings.byok.add_first') : $_('settings.byok.add_more')}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
parent owns the active category.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { MagnifyingGlass, X } from '@mana/shared-icons';
|
||||
import {
|
||||
categories as defaultCategories,
|
||||
getCategories,
|
||||
searchSettings,
|
||||
type Category,
|
||||
type CategoryId,
|
||||
|
|
@ -21,10 +22,11 @@
|
|||
categories?: Category[];
|
||||
}
|
||||
|
||||
let { activeCategory, onSelect, onJump, categories = defaultCategories }: Props = $props();
|
||||
let { activeCategory, onSelect, onJump, categories: categoriesOverride }: Props = $props();
|
||||
const categories = $derived(categoriesOverride ?? getCategories($_));
|
||||
|
||||
let query = $state('');
|
||||
let results = $derived(searchSettings(query));
|
||||
let results = $derived(searchSettings($_, query));
|
||||
let highlightedCategoryIds = $derived(new Set(results.map((r) => r.category)));
|
||||
|
||||
function clearSearch() {
|
||||
|
|
@ -37,7 +39,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<aside class="settings-sidebar" aria-label="Einstellungs-Kategorien">
|
||||
<aside class="settings-sidebar" aria-label={$_('settings.sidebar.aria_categories')}>
|
||||
<!-- Search -->
|
||||
<div class="search-wrapper">
|
||||
<span class="search-icon">
|
||||
|
|
@ -45,17 +47,22 @@
|
|||
</span>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Einstellungen suchen…"
|
||||
placeholder={$_('settings.sidebar.search_placeholder')}
|
||||
bind:value={query}
|
||||
class="search-input"
|
||||
aria-label="Einstellungen durchsuchen"
|
||||
aria-label={$_('settings.sidebar.aria_search')}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') clearSearch();
|
||||
else if (e.key === 'Enter' && results[0]) handleResultClick(results[0]);
|
||||
}}
|
||||
/>
|
||||
{#if query}
|
||||
<button type="button" class="clear-btn" onclick={clearSearch} aria-label="Suche leeren">
|
||||
<button
|
||||
type="button"
|
||||
class="clear-btn"
|
||||
onclick={clearSearch}
|
||||
aria-label={$_('settings.sidebar.aria_clear')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -75,7 +82,9 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{:else if query}
|
||||
<div class="no-results">Keine Treffer für „{query}"</div>
|
||||
<div class="no-results">
|
||||
{$_('settings.sidebar.no_results', { values: { query } })}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="chip-row" role="tablist">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
* settings/searchIndex — single source of truth for the settings sidebar
|
||||
* categories and the in-page search index. Editing a single entry here
|
||||
* updates both the navigation and the search results.
|
||||
*
|
||||
* Labels and keywords come from the `settings` i18n namespace at runtime;
|
||||
* pass the svelte-i18n `$_` formatter into `getCategories()` /
|
||||
* `getSearchIndex()` so components stay reactive to locale changes.
|
||||
*/
|
||||
import type { Component } from 'svelte';
|
||||
import { Gear, Robot, ShieldCheck, Cloud, Tag } from '@mana/shared-icons';
|
||||
|
|
@ -17,39 +21,25 @@ export interface Category {
|
|||
anchors: string[];
|
||||
}
|
||||
|
||||
export const categories: Category[] = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'Allgemein',
|
||||
description: 'Theme, Sprache, Benachrichtigungen',
|
||||
icon: Gear,
|
||||
anchors: ['global'],
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
label: 'KI',
|
||||
description: 'Compute-Backend & Modelle',
|
||||
icon: Robot,
|
||||
anchors: ['ai-options'],
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: 'Sicherheit',
|
||||
description: 'Passkeys, 2FA, Verschlüsselung & Sitzungen',
|
||||
type TranslatorValue = string | number | boolean | Date | null | undefined;
|
||||
type Translator = (key: string, opts?: { values?: Record<string, TranslatorValue> }) => string;
|
||||
|
||||
interface CategoryDef {
|
||||
icon: Component;
|
||||
anchors: string[];
|
||||
i18nKey: string;
|
||||
}
|
||||
|
||||
const CATEGORY_DEFS: Record<CategoryId, CategoryDef> = {
|
||||
general: { icon: Gear, anchors: ['global'], i18nKey: 'general' },
|
||||
ai: { icon: Robot, anchors: ['ai-options'], i18nKey: 'ai' },
|
||||
security: {
|
||||
icon: ShieldCheck,
|
||||
anchors: ['passkeys', 'sessions', 'two-factor', 'vault', 'security-log'],
|
||||
i18nKey: 'security',
|
||||
},
|
||||
{
|
||||
id: 'privacy',
|
||||
label: 'Privatsphäre',
|
||||
description: 'Was ist gerade öffentlich oder per Link geteilt — mit Kill-Switch.',
|
||||
icon: ShieldCheck,
|
||||
anchors: ['privacy'],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Daten & Sync',
|
||||
description: 'Cloud-Sync, Export, Backup & DSGVO',
|
||||
privacy: { icon: ShieldCheck, anchors: ['privacy'], i18nKey: 'privacy' },
|
||||
data: {
|
||||
icon: Cloud,
|
||||
anchors: [
|
||||
'cloud-sync',
|
||||
|
|
@ -61,15 +51,30 @@ export const categories: Category[] = [
|
|||
'backup',
|
||||
'danger-zone',
|
||||
],
|
||||
i18nKey: 'data',
|
||||
},
|
||||
{
|
||||
id: 'tag-presets',
|
||||
label: 'Tag-Presets',
|
||||
description: 'Tag-Sets für neue Spaces',
|
||||
icon: Tag,
|
||||
anchors: ['tag-presets'],
|
||||
},
|
||||
];
|
||||
'tag-presets': { icon: Tag, anchors: ['tag-presets'], i18nKey: 'tag_presets' },
|
||||
};
|
||||
|
||||
const CATEGORY_IDS = Object.keys(CATEGORY_DEFS) as CategoryId[];
|
||||
|
||||
export function getCategories(t: Translator): Category[] {
|
||||
return CATEGORY_IDS.map((id) => {
|
||||
const def = CATEGORY_DEFS[id];
|
||||
return {
|
||||
id,
|
||||
label: t(`settings.categories.${def.i18nKey}.label`),
|
||||
description: t(`settings.categories.${def.i18nKey}.description`),
|
||||
icon: def.icon,
|
||||
anchors: def.anchors,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Locate the category that owns a given anchor — used by hash deep-links. */
|
||||
export function findCategoryByAnchor(anchor: string): CategoryId | undefined {
|
||||
return CATEGORY_IDS.find((id) => CATEGORY_DEFS[id].anchors.includes(anchor));
|
||||
}
|
||||
|
||||
export interface SearchEntry {
|
||||
/** Display label shown in the result list */
|
||||
|
|
@ -80,156 +85,66 @@ export interface SearchEntry {
|
|||
anchor: string;
|
||||
}
|
||||
|
||||
export const searchIndex: SearchEntry[] = [
|
||||
interface SearchEntryDef {
|
||||
i18nKey: string;
|
||||
category: CategoryId;
|
||||
anchor: string;
|
||||
}
|
||||
|
||||
const SEARCH_ENTRY_DEFS: SearchEntryDef[] = [
|
||||
// General
|
||||
{
|
||||
label: 'Theme',
|
||||
keywords: ['dark', 'light', 'farbe', 'design'],
|
||||
category: 'general',
|
||||
anchor: 'global',
|
||||
},
|
||||
{
|
||||
label: 'Sprache',
|
||||
keywords: ['language', 'i18n', 'deutsch', 'english'],
|
||||
category: 'general',
|
||||
anchor: 'global',
|
||||
},
|
||||
{
|
||||
label: 'Benachrichtigungen',
|
||||
keywords: ['notification', 'sound'],
|
||||
category: 'general',
|
||||
anchor: 'global',
|
||||
},
|
||||
|
||||
{ i18nKey: 'theme', category: 'general', anchor: 'global' },
|
||||
{ i18nKey: 'language', category: 'general', anchor: 'global' },
|
||||
{ i18nKey: 'notifications', category: 'general', anchor: 'global' },
|
||||
// AI
|
||||
{
|
||||
label: 'KI-Optionen',
|
||||
keywords: ['llm', 'ai', 'compute'],
|
||||
category: 'ai',
|
||||
anchor: 'ai-options',
|
||||
},
|
||||
{
|
||||
label: 'Browser-Modell (Gemma)',
|
||||
keywords: ['gemma', 'webgpu', 'lokal', 'offline'],
|
||||
category: 'ai',
|
||||
anchor: 'ai-options',
|
||||
},
|
||||
{
|
||||
label: 'Mana-Server (KI)',
|
||||
keywords: ['server', 'self-hosted'],
|
||||
category: 'ai',
|
||||
anchor: 'ai-options',
|
||||
},
|
||||
{
|
||||
label: 'Cloud-KI (Gemini)',
|
||||
keywords: ['google', 'cloud', 'gemini'],
|
||||
category: 'ai',
|
||||
anchor: 'ai-options',
|
||||
},
|
||||
|
||||
{ i18nKey: 'ai_options', category: 'ai', anchor: 'ai-options' },
|
||||
{ i18nKey: 'browser_model', category: 'ai', anchor: 'ai-options' },
|
||||
{ i18nKey: 'mana_server', category: 'ai', anchor: 'ai-options' },
|
||||
{ i18nKey: 'cloud_ai', category: 'ai', anchor: 'ai-options' },
|
||||
// Security
|
||||
{
|
||||
label: 'Passkeys',
|
||||
keywords: ['webauthn', 'fido', 'biometrie'],
|
||||
category: 'security',
|
||||
anchor: 'passkeys',
|
||||
},
|
||||
{
|
||||
label: 'Aktive Sessions',
|
||||
keywords: ['logout', 'gerät', 'device'],
|
||||
category: 'security',
|
||||
anchor: 'sessions',
|
||||
},
|
||||
{
|
||||
label: 'Zwei-Faktor (2FA)',
|
||||
keywords: ['totp', '2fa', 'mfa'],
|
||||
category: 'security',
|
||||
anchor: 'two-factor',
|
||||
},
|
||||
{
|
||||
label: 'Verschlüsselung',
|
||||
keywords: ['vault', 'encryption', 'aes', 'schlüssel', 'zero-knowledge'],
|
||||
category: 'security',
|
||||
anchor: 'vault',
|
||||
},
|
||||
{
|
||||
label: 'Sicherheits-Log',
|
||||
keywords: ['audit', 'history', 'verlauf'],
|
||||
category: 'security',
|
||||
anchor: 'security-log',
|
||||
},
|
||||
|
||||
{ i18nKey: 'passkeys', category: 'security', anchor: 'passkeys' },
|
||||
{ i18nKey: 'sessions', category: 'security', anchor: 'sessions' },
|
||||
{ i18nKey: 'two_factor', category: 'security', anchor: 'two-factor' },
|
||||
{ i18nKey: 'vault', category: 'security', anchor: 'vault' },
|
||||
{ i18nKey: 'security_log', category: 'security', anchor: 'security-log' },
|
||||
// Privacy
|
||||
{
|
||||
label: 'Privatsphäre-Übersicht',
|
||||
keywords: ['public', 'öffentlich', 'unlisted', 'teilen', 'sharing', 'link'],
|
||||
category: 'privacy',
|
||||
anchor: 'privacy',
|
||||
},
|
||||
{
|
||||
label: 'Alle auf privat zurücksetzen',
|
||||
keywords: ['kill-switch', 'kill switch', 'reset', 'privat', 'widerrufen'],
|
||||
category: 'privacy',
|
||||
anchor: 'privacy',
|
||||
},
|
||||
|
||||
{ i18nKey: 'privacy_overview', category: 'privacy', anchor: 'privacy' },
|
||||
{ i18nKey: 'reset_all_private', category: 'privacy', anchor: 'privacy' },
|
||||
// Data
|
||||
{
|
||||
label: 'Cloud Sync',
|
||||
keywords: ['sync', 'geräte'],
|
||||
category: 'data',
|
||||
anchor: 'cloud-sync',
|
||||
},
|
||||
{
|
||||
label: 'Daten exportieren',
|
||||
keywords: ['export', 'dsgvo', 'gdpr', 'json'],
|
||||
category: 'data',
|
||||
anchor: 'my-data',
|
||||
},
|
||||
{
|
||||
label: 'Authentifizierung',
|
||||
keywords: ['sessions', '2fa', 'login'],
|
||||
category: 'data',
|
||||
anchor: 'auth-data',
|
||||
},
|
||||
{
|
||||
label: 'Credits & Transaktionen',
|
||||
keywords: ['guthaben', 'transaktionen'],
|
||||
category: 'data',
|
||||
anchor: 'credits-data',
|
||||
},
|
||||
{
|
||||
label: 'Projektdaten',
|
||||
keywords: ['projekte', 'apps', 'statistik'],
|
||||
category: 'data',
|
||||
anchor: 'project-data',
|
||||
},
|
||||
{
|
||||
label: 'Aufbewahrungsfristen',
|
||||
keywords: ['retention', 'dsgvo', 'fristen'],
|
||||
category: 'data',
|
||||
anchor: 'retention',
|
||||
},
|
||||
{
|
||||
label: 'Backup & Wiederherstellung',
|
||||
keywords: ['backup', 'restore', 'import', 'archiv', '.mana'],
|
||||
category: 'data',
|
||||
anchor: 'backup',
|
||||
},
|
||||
{
|
||||
label: 'Konto löschen',
|
||||
keywords: ['delete', 'gdpr', 'dsgvo', 'gefahrenzone'],
|
||||
category: 'data',
|
||||
anchor: 'danger-zone',
|
||||
},
|
||||
{ i18nKey: 'cloud_sync', category: 'data', anchor: 'cloud-sync' },
|
||||
{ i18nKey: 'data_export', category: 'data', anchor: 'my-data' },
|
||||
{ i18nKey: 'auth_data', category: 'data', anchor: 'auth-data' },
|
||||
{ i18nKey: 'credits_data', category: 'data', anchor: 'credits-data' },
|
||||
{ i18nKey: 'project_data', category: 'data', anchor: 'project-data' },
|
||||
{ i18nKey: 'retention', category: 'data', anchor: 'retention' },
|
||||
{ i18nKey: 'backup', category: 'data', anchor: 'backup' },
|
||||
{ i18nKey: 'delete_account', category: 'data', anchor: 'danger-zone' },
|
||||
];
|
||||
|
||||
export function getSearchIndex(t: Translator): SearchEntry[] {
|
||||
return SEARCH_ENTRY_DEFS.map((def) => {
|
||||
const keywordsRaw = t(`settings.search.${def.i18nKey}.keywords`);
|
||||
return {
|
||||
label: t(`settings.search.${def.i18nKey}.label`),
|
||||
keywords: keywordsRaw
|
||||
? keywordsRaw
|
||||
.split(',')
|
||||
.map((k) => k.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
category: def.category,
|
||||
anchor: def.anchor,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Tiny case-insensitive ranker — exact > prefix > contains. */
|
||||
export function searchSettings(query: string, limit = 8): SearchEntry[] {
|
||||
export function searchSettings(t: Translator, query: string, limit = 8): SearchEntry[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
const index = getSearchIndex(t);
|
||||
const results: { entry: SearchEntry; score: number }[] = [];
|
||||
for (const entry of searchIndex) {
|
||||
for (const entry of index) {
|
||||
const haystacks = [
|
||||
entry.label.toLowerCase(),
|
||||
...(entry.keywords ?? []).map((k) => k.toLowerCase()),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Robot } from '@mana/shared-icons';
|
||||
import AiSettings from '../AiSettings.svelte';
|
||||
import SettingsPanel from '../SettingsPanel.svelte';
|
||||
|
|
@ -8,8 +9,8 @@
|
|||
<SettingsPanel id="ai-options">
|
||||
<SettingsSectionHeader
|
||||
icon={Robot}
|
||||
title="KI-Optionen"
|
||||
description="Wähle, welche KI-Schichten Mana verwenden darf — von gar keiner bis zu allen"
|
||||
title={$_('settings.ai.title')}
|
||||
description={$_('settings.ai.description')}
|
||||
tone="indigo"
|
||||
/>
|
||||
<AiSettings />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Gear } from '@mana/shared-icons';
|
||||
import type { ThemeMode, WeekStartDay } from '@mana/shared-theme';
|
||||
|
|
@ -20,12 +21,12 @@
|
|||
{ id: 'it', label: 'IT' },
|
||||
];
|
||||
|
||||
const colorSchemes = [
|
||||
{ id: 'ocean', label: 'Ozean', color: 'bg-blue-500' },
|
||||
{ id: 'nature', label: 'Natur', color: 'bg-green-500' },
|
||||
{ id: 'lume', label: 'Lume', color: 'bg-amber-500' },
|
||||
{ id: 'stone', label: 'Stein', color: 'bg-slate-500' },
|
||||
];
|
||||
const colorSchemes = $derived([
|
||||
{ id: 'ocean', label: $_('settings.general.color_scheme_ocean'), color: 'bg-blue-500' },
|
||||
{ id: 'nature', label: $_('settings.general.color_scheme_nature'), color: 'bg-green-500' },
|
||||
{ id: 'lume', label: $_('settings.general.color_scheme_lume'), color: 'bg-amber-500' },
|
||||
{ id: 'stone', label: $_('settings.general.color_scheme_stone'), color: 'bg-slate-500' },
|
||||
]);
|
||||
|
||||
async function setThemeMode(mode: ThemeMode) {
|
||||
await userSettings.updateGlobal({
|
||||
|
|
@ -69,15 +70,15 @@
|
|||
<SettingsPanel id="global">
|
||||
<SettingsSectionHeader
|
||||
icon={Gear}
|
||||
title="Allgemein"
|
||||
description="Sprache, Wochenstart & Sounds"
|
||||
title={$_('settings.general.title')}
|
||||
description={$_('settings.general.description')}
|
||||
/>
|
||||
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Anzeigesprache</p>
|
||||
<p class="row-desc">Sprache der Benutzeroberfläche</p>
|
||||
<p class="row-title">{$_('settings.general.language_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.language_description')}</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
{#each languages as lang}
|
||||
|
|
@ -92,8 +93,8 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Farbschema</p>
|
||||
<p class="row-desc">Akzentfarbe der Oberfläche</p>
|
||||
<p class="row-title">{$_('settings.general.color_scheme_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.color_scheme_description')}</p>
|
||||
</div>
|
||||
<div class="color-group">
|
||||
{#each colorSchemes as scheme}
|
||||
|
|
@ -109,11 +110,11 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Farbmodus</p>
|
||||
<p class="row-desc">Hell, Dunkel oder automatisch</p>
|
||||
<p class="row-title">{$_('settings.general.color_mode_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.color_mode_description')}</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
{#each [{ id: 'light', label: 'Hell' }, { id: 'dark', label: 'Dunkel' }, { id: 'system', label: 'System' }] as mode}
|
||||
{#each [{ id: 'light', label: $_('settings.general.color_mode_light') }, { id: 'dark', label: $_('settings.general.color_mode_dark') }, { id: 'system', label: $_('settings.general.color_mode_system') }] as mode}
|
||||
<button
|
||||
class="choice-btn"
|
||||
class:active={userSettings.globalSettings.theme.mode === mode.id}
|
||||
|
|
@ -125,33 +126,33 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Wochenstart</p>
|
||||
<p class="row-desc">Erster Tag der Woche in Kalendern</p>
|
||||
<p class="row-title">{$_('settings.general.week_start_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.week_start_description')}</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="choice-btn"
|
||||
class:active={userSettings.general?.weekStartsOn === 'monday'}
|
||||
onclick={() => setWeekStart('monday')}>Montag</button
|
||||
onclick={() => setWeekStart('monday')}>{$_('settings.general.week_start_monday')}</button
|
||||
>
|
||||
<button
|
||||
class="choice-btn"
|
||||
class:active={userSettings.general?.weekStartsOn === 'sunday'}
|
||||
onclick={() => setWeekStart('sunday')}>Sonntag</button
|
||||
onclick={() => setWeekStart('sunday')}>{$_('settings.general.week_start_sunday')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Sounds</p>
|
||||
<p class="row-desc">Sound-Effekte in allen Apps</p>
|
||||
<p class="row-title">{$_('settings.general.sounds_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.sounds_description')}</p>
|
||||
</div>
|
||||
<button
|
||||
class="toggle"
|
||||
class:on={userSettings.general?.soundsEnabled ?? true}
|
||||
onclick={() => setSounds(!(userSettings.general?.soundsEnabled ?? true))}
|
||||
aria-label="Sounds ein- oder ausschalten"
|
||||
aria-label={$_('settings.general.sounds_aria')}
|
||||
aria-pressed={userSettings.general?.soundsEnabled ?? true}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
|
|
@ -160,8 +161,8 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Onboarding erneut durchlaufen</p>
|
||||
<p class="row-desc">Name, Look und Module neu wählen</p>
|
||||
<p class="row-title">{$_('settings.general.onboarding_title')}</p>
|
||||
<p class="row-desc">{$_('settings.general.onboarding_description')}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -169,7 +170,9 @@
|
|||
onclick={restartOnboarding}
|
||||
disabled={restartingOnboarding}
|
||||
>
|
||||
{restartingOnboarding ? 'Starte…' : 'Starten'}
|
||||
{restartingOnboarding
|
||||
? $_('settings.general.onboarding_starting')
|
||||
: $_('settings.general.onboarding_start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
QrCode,
|
||||
|
|
@ -89,7 +91,8 @@
|
|||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
const lang = get(locale) ?? 'de';
|
||||
return new Date(dateStr).toLocaleDateString(lang, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
|
|
@ -99,7 +102,7 @@
|
|||
}
|
||||
|
||||
function formatNum(n: number): string {
|
||||
return n.toLocaleString('de-DE');
|
||||
return n.toLocaleString(get(locale) ?? 'de');
|
||||
}
|
||||
|
||||
function formatRelativeTime(dateStr: string | undefined): string {
|
||||
|
|
@ -108,11 +111,13 @@
|
|||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
if (diffMins < 1) return 'gerade eben';
|
||||
if (diffMins < 60) return `vor ${diffMins} Min`;
|
||||
if (diffHours < 24) return `vor ${diffHours} Std`;
|
||||
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||
return new Date(dateStr).toLocaleDateString('de-DE');
|
||||
if (diffMins < 1) return $_('settings.mydata.relative_just_now');
|
||||
if (diffMins < 60)
|
||||
return $_('settings.mydata.relative_minutes_ago', { values: { n: diffMins } });
|
||||
if (diffHours < 24)
|
||||
return $_('settings.mydata.relative_hours_ago', { values: { n: diffHours } });
|
||||
if (diffDays < 7) return $_('settings.mydata.relative_days_ago', { values: { n: diffDays } });
|
||||
return new Date(dateStr).toLocaleDateString(get(locale) ?? 'de');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
|
@ -124,8 +129,8 @@
|
|||
<SettingsPanel id="my-data">
|
||||
<SettingsSectionHeader
|
||||
icon={FileText}
|
||||
title="Meine Daten (DSGVO)"
|
||||
description="Übersicht über alle deine gespeicherten Daten"
|
||||
title={$_('settings.mydata.title')}
|
||||
description={$_('settings.mydata.description')}
|
||||
tone="purple"
|
||||
>
|
||||
{#snippet action()}
|
||||
|
|
@ -135,14 +140,15 @@
|
|||
type="button"
|
||||
class="btn-ghost"
|
||||
onclick={() => (showQRDialog = true)}
|
||||
title="Als QR-Code exportieren"
|
||||
title={$_('settings.mydata.qr_export')}
|
||||
>
|
||||
<QrCode size={14} />
|
||||
<span>QR</span>
|
||||
<span>{$_('settings.mydata.qr_short')}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-primary-sm" onclick={handleExport} disabled={exporting}>
|
||||
<DownloadSimple size={14} />
|
||||
<span>{exporting ? 'Exportiere…' : 'Exportieren'}</span>
|
||||
<span>{exporting ? $_('settings.mydata.exporting') : $_('settings.mydata.export')}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -154,7 +160,9 @@
|
|||
{:else if error}
|
||||
<div class="error-state">
|
||||
<p class="error-text">{error}</p>
|
||||
<button type="button" class="btn-primary-sm" onclick={loadMyData}> Erneut versuchen </button>
|
||||
<button type="button" class="btn-primary-sm" onclick={loadMyData}>
|
||||
{$_('settings.mydata.retry')}
|
||||
</button>
|
||||
</div>
|
||||
{:else if userData}
|
||||
<div class="rows">
|
||||
|
|
@ -163,7 +171,7 @@
|
|||
<span>{(userData.user.name || userData.user.email)[0].toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{userData.user.name || 'Kein Name'}</p>
|
||||
<p class="row-title">{userData.user.name || $_('settings.mydata.no_name')}</p>
|
||||
<p class="row-desc">{userData.user.email}</p>
|
||||
</div>
|
||||
<div class="badges">
|
||||
|
|
@ -171,12 +179,12 @@
|
|||
{#if userData.user.emailVerified}
|
||||
<span class="badge success">
|
||||
<CheckCircle size={12} weight="fill" />
|
||||
verifiziert
|
||||
{$_('settings.mydata.verified')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="badge warn">
|
||||
<WarningCircle size={12} weight="fill" />
|
||||
nicht verifiziert
|
||||
{$_('settings.mydata.not_verified')}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -184,22 +192,22 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Registriert am</p>
|
||||
<p class="row-title">{$_('settings.mydata.registered_at')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{formatDate(userData.user.createdAt)}</span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Gesamt-Entitäten</p>
|
||||
<p class="row-desc">Datensätze über alle Apps hinweg</p>
|
||||
<p class="row-title">{$_('settings.mydata.total_entities')}</p>
|
||||
<p class="row-desc">{$_('settings.mydata.total_entities_desc')}</p>
|
||||
</div>
|
||||
<span class="value">{formatNum(userData.totals.totalEntities)}</span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Projekte mit Daten</p>
|
||||
<p class="row-title">{$_('settings.mydata.projects_with_data')}</p>
|
||||
</div>
|
||||
<span class="value">
|
||||
{userData.totals.projectsWithData} / {userData.projects.length}
|
||||
|
|
@ -208,13 +216,12 @@
|
|||
</div>
|
||||
|
||||
<p class="footnote">
|
||||
Keine Tracking-Cookies — anonyme Analyse via Umami. Details in der
|
||||
<a
|
||||
{$_('settings.mydata.footnote_pre')}<a
|
||||
href="https://mana-landing.pages.dev/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="inline-link">Datenschutzerklärung</a
|
||||
>.
|
||||
class="inline-link">{$_('settings.mydata.footnote_link')}</a
|
||||
>{$_('settings.mydata.footnote_post')}
|
||||
</p>
|
||||
{/if}
|
||||
</SettingsPanel>
|
||||
|
|
@ -224,27 +231,37 @@
|
|||
<SettingsPanel id="auth-data">
|
||||
<SettingsSectionHeader
|
||||
icon={ShieldCheck}
|
||||
title="Authentifizierung"
|
||||
description="Sessions, Accounts & 2FA"
|
||||
title={$_('settings.mydata.auth_title')}
|
||||
description={$_('settings.mydata.auth_description')}
|
||||
tone="blue"
|
||||
/>
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Aktive Sessions</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.auth_sessions')}</p>
|
||||
</div>
|
||||
<span class="value">{userData.auth.sessionsCount}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Verknüpfte Accounts</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.auth_accounts')}</p>
|
||||
</div>
|
||||
<span class="value">{userData.auth.accountsCount}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Zwei-Faktor (2FA)</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.auth_two_fa')}</p>
|
||||
</div>
|
||||
<span class="badge" class:success={userData.auth.has2FA}>
|
||||
{userData.auth.has2FA ? 'Aktiviert' : 'Deaktiviert'}
|
||||
{userData.auth.has2FA
|
||||
? $_('settings.mydata.auth_two_fa_active')
|
||||
: $_('settings.mydata.auth_two_fa_inactive')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Letzter Login</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.auth_last_login')}</p>
|
||||
</div>
|
||||
<span class="row-meta">
|
||||
{userData.auth.lastLoginAt ? formatDate(userData.auth.lastLoginAt) : '—'}
|
||||
</span>
|
||||
|
|
@ -256,25 +273,33 @@
|
|||
<SettingsPanel id="credits-data">
|
||||
<SettingsSectionHeader
|
||||
icon={CurrencyCircleDollar}
|
||||
title="Credits"
|
||||
description="Guthaben & Transaktionen"
|
||||
title={$_('settings.mydata.credits_title')}
|
||||
description={$_('settings.mydata.credits_description')}
|
||||
tone="yellow"
|
||||
/>
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Aktueller Stand</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.credits_balance')}</p>
|
||||
</div>
|
||||
<span class="value emphasized">{formatNum(userData.credits.balance)}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Gesamt verdient</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.credits_total_earned')}</p>
|
||||
</div>
|
||||
<span class="value success-text">+{formatNum(userData.credits.totalEarned)}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Gesamt ausgegeben</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.credits_total_spent')}</p>
|
||||
</div>
|
||||
<span class="value danger-text">−{formatNum(userData.credits.totalSpent)}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Transaktionen</p></div>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.credits_transactions')}</p>
|
||||
</div>
|
||||
<span class="value">{userData.credits.transactionsCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -284,15 +309,15 @@
|
|||
<SettingsPanel id="project-data">
|
||||
<SettingsSectionHeader
|
||||
icon={FolderOpen}
|
||||
title="Projektdaten"
|
||||
description="Datensätze pro App"
|
||||
title={$_('settings.mydata.project_title')}
|
||||
description={$_('settings.mydata.project_description')}
|
||||
/>
|
||||
<table class="project-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-app">App</th>
|
||||
<th class="col-num">Einträge</th>
|
||||
<th class="col-time">Letzte Aktivität</th>
|
||||
<th class="col-app">{$_('settings.mydata.col_app')}</th>
|
||||
<th class="col-num">{$_('settings.mydata.col_count')}</th>
|
||||
<th class="col-time">{$_('settings.mydata.col_time')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -308,9 +333,9 @@
|
|||
class:error={!project.available}
|
||||
title={project.available
|
||||
? project.totalCount > 0
|
||||
? 'Aktiv'
|
||||
: 'Keine Daten'
|
||||
: project.error || 'Nicht verfügbar'}
|
||||
? $_('settings.mydata.project_status_active')
|
||||
: $_('settings.mydata.project_status_empty')
|
||||
: project.error || $_('settings.mydata.project_status_unavailable')}
|
||||
></span>
|
||||
<span class="app-icon">
|
||||
{#if AppIcon}
|
||||
|
|
@ -333,7 +358,9 @@
|
|||
{#if project.available}
|
||||
<span class="muted">{formatRelativeTime(project.lastActivityAt)}</span>
|
||||
{:else}
|
||||
<span class="muted err">{project.error || 'nicht erreichbar'}</span>
|
||||
<span class="muted err"
|
||||
>{project.error || $_('settings.mydata.project_unreachable')}</span
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -346,29 +373,39 @@
|
|||
<SettingsPanel id="retention">
|
||||
<SettingsSectionHeader
|
||||
icon={Clock}
|
||||
title="Aufbewahrungsfristen"
|
||||
description="Wie lange wir deine Daten speichern"
|
||||
title={$_('settings.mydata.retention_title')}
|
||||
description={$_('settings.mydata.retention_description')}
|
||||
/>
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Benutzerkonto & Profil</p></div>
|
||||
<span class="row-meta">Bis zur Löschung</span>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.retention_account')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{$_('settings.mydata.retention_account_value')}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Sessions & Login-Historie</p></div>
|
||||
<span class="row-meta">90 Tage nach Ablauf</span>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.retention_sessions')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{$_('settings.mydata.retention_sessions_value')}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Credit-Transaktionen</p></div>
|
||||
<span class="row-meta">10 Jahre (gesetzlich)</span>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.retention_credit')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{$_('settings.mydata.retention_credit_value')}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Security-Logs</p></div>
|
||||
<span class="row-meta">1 Jahr</span>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.retention_security')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{$_('settings.mydata.retention_security_value')}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row-info"><p class="row-title">Projektdaten (Chat, Todo, …)</p></div>
|
||||
<span class="row-meta">Bis zur Löschung</span>
|
||||
<div class="row-info">
|
||||
<p class="row-title">{$_('settings.mydata.retention_project')}</p>
|
||||
</div>
|
||||
<span class="row-meta">{$_('settings.mydata.retention_project_value')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
|
|
@ -380,21 +417,18 @@
|
|||
<SettingsPanel id="danger-zone">
|
||||
<SettingsSectionHeader
|
||||
icon={Warning}
|
||||
title="Gefahrenzone"
|
||||
description="Unwiderrufliche Aktionen"
|
||||
title={$_('settings.mydata.danger_title')}
|
||||
description={$_('settings.mydata.danger_description')}
|
||||
tone="red"
|
||||
/>
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Alle meine Daten löschen</p>
|
||||
<p class="row-desc">
|
||||
Löscht dein Konto und alle verbundenen Daten dauerhaft aus allen Projekten. Kann nicht
|
||||
rückgängig gemacht werden.
|
||||
</p>
|
||||
<p class="row-title">{$_('settings.mydata.danger_delete_title')}</p>
|
||||
<p class="row-desc">{$_('settings.mydata.danger_delete_desc')}</p>
|
||||
</div>
|
||||
<button type="button" class="btn-danger" onclick={() => (showDeleteDialog = true)}>
|
||||
Daten löschen
|
||||
{$_('settings.mydata.danger_delete_btn')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
immediately without the user reloading.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { ShieldCheck, Globe, Link as LinkIcon } from '@mana/shared-icons';
|
||||
import { liveQuery } from 'dexie';
|
||||
import SettingsPanel from '../SettingsPanel.svelte';
|
||||
|
|
@ -62,10 +63,12 @@
|
|||
busyKey = key;
|
||||
try {
|
||||
await setRecordVisibility(rec.collection, rec.id, 'space');
|
||||
toastStore.show?.(`„${rec.title}" ist jetzt privat`);
|
||||
toastStore.show?.($_('settings.privacy.toast_set_private', { values: { title: rec.title } }));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastStore.show?.(`Konnte „${rec.title}" nicht zurückstufen`);
|
||||
toastStore.show?.(
|
||||
$_('settings.privacy.toast_set_private_failed', { values: { title: rec.title } })
|
||||
);
|
||||
} finally {
|
||||
busyKey = null;
|
||||
}
|
||||
|
|
@ -77,13 +80,15 @@
|
|||
const { flipped, failed } = await resetAllExposedToSpace();
|
||||
confirmKill = false;
|
||||
if (failed > 0) {
|
||||
toastStore.show?.(`${flipped} Einträge auf privat — ${failed} fehlgeschlagen`);
|
||||
toastStore.show?.(
|
||||
$_('settings.privacy.toast_kill_partial', { values: { flipped, failed } })
|
||||
);
|
||||
} else {
|
||||
toastStore.show?.(`${flipped} Einträge auf privat zurückgesetzt`);
|
||||
toastStore.show?.($_('settings.privacy.toast_kill_done', { values: { flipped } }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toastStore.show?.('Kill-Switch fehlgeschlagen');
|
||||
toastStore.show?.($_('settings.privacy.toast_kill_failed'));
|
||||
} finally {
|
||||
killing = false;
|
||||
}
|
||||
|
|
@ -93,8 +98,8 @@
|
|||
<SettingsPanel id="privacy">
|
||||
<SettingsSectionHeader
|
||||
icon={ShieldCheck}
|
||||
title="Privatsphäre-Übersicht"
|
||||
description="Alle Einträge, die du gerade öffentlich zeigst oder per Link teilst — mit ein-Klick-Rückzieher."
|
||||
title={$_('settings.privacy.title')}
|
||||
description={$_('settings.privacy.description')}
|
||||
tone="indigo"
|
||||
/>
|
||||
|
||||
|
|
@ -103,22 +108,22 @@
|
|||
<Globe size={18} />
|
||||
<div>
|
||||
<span class="summary-count">{publicRecords.length}</span>
|
||||
<span class="summary-label">öffentlich</span>
|
||||
<span class="summary-label">{$_('settings.privacy.summary_public')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<LinkIcon size={18} />
|
||||
<div>
|
||||
<span class="summary-count">{unlistedRecords.length}</span>
|
||||
<span class="summary-label">per Link teilbar</span>
|
||||
<span class="summary-label">{$_('settings.privacy.summary_unlisted')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p class="muted">Lädt…</p>
|
||||
<p class="muted">{$_('settings.privacy.loading')}</p>
|
||||
{:else if exposed.length === 0}
|
||||
<p class="muted empty">Aktuell ist nichts öffentlich oder per Link geteilt — gut gemacht.</p>
|
||||
<p class="muted empty">{$_('settings.privacy.empty')}</p>
|
||||
{:else}
|
||||
<div class="groups">
|
||||
{#each grouped as [module, records] (module)}
|
||||
|
|
@ -133,19 +138,21 @@
|
|||
<div class="record-meta">
|
||||
<span class="record-title">{rec.title}</span>
|
||||
<span class="record-badge" class:badge-unlisted={rec.visibility === 'unlisted'}>
|
||||
{rec.visibility === 'public' ? 'Öffentlich' : 'Per Link'}
|
||||
{rec.visibility === 'public'
|
||||
? $_('settings.privacy.badge_public')
|
||||
: $_('settings.privacy.badge_unlisted')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="record-actions">
|
||||
{#if rec.openHref}
|
||||
<a class="link" href={rec.openHref}>Öffnen</a>
|
||||
<a class="link" href={rec.openHref}>{$_('settings.privacy.open')}</a>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
disabled={busyKey === `${rec.collection}/${rec.id}`}
|
||||
onclick={() => setPrivate(rec)}
|
||||
>
|
||||
Privat
|
||||
{$_('settings.privacy.set_private')}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -158,21 +165,21 @@
|
|||
<div class="kill-zone">
|
||||
{#if !confirmKill}
|
||||
<button class="btn btn-danger" onclick={() => (confirmKill = true)}>
|
||||
Alle auf privat zurücksetzen
|
||||
{$_('settings.privacy.kill_all')}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="confirm">
|
||||
<p>
|
||||
<strong>{exposed.length}</strong>
|
||||
{exposed.length === 1 ? 'Eintrag' : 'Einträge'} werden auf "Space" zurückgesetzt. Aktive Share-Links
|
||||
werden widerrufen. Fortfahren?
|
||||
{exposed.length === 1
|
||||
? $_('settings.privacy.confirm_one', { values: { count: exposed.length } })
|
||||
: $_('settings.privacy.confirm_other', { values: { count: exposed.length } })}
|
||||
</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn" onclick={() => (confirmKill = false)} disabled={killing}>
|
||||
Abbrechen
|
||||
{$_('settings.privacy.cancel')}
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={killSwitch} disabled={killing}>
|
||||
{killing ? 'Setze zurück…' : 'Ja, alles zurücksetzen'}
|
||||
{killing ? $_('settings.privacy.killing') : $_('settings.privacy.confirm_kill')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { ShieldCheck } from '@mana/shared-icons';
|
||||
import { PasskeyManager, TwoFactorSetup, AuditLog, SessionManager } from '@mana/shared-auth-ui';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
|
@ -34,8 +35,8 @@
|
|||
<SettingsPanel>
|
||||
<SettingsSectionHeader
|
||||
icon={ShieldCheck}
|
||||
title="Sicherheit"
|
||||
description="Passkeys, 2FA, Verschlüsselung & Sitzungen"
|
||||
title={$_('settings.security.title')}
|
||||
description={$_('settings.security.description')}
|
||||
tone="blue"
|
||||
/>
|
||||
</SettingsPanel>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { CloudCheck, Pause, Cloud } from '@mana/shared-icons';
|
||||
import { syncBilling } from '$lib/stores/sync-billing.svelte';
|
||||
import { creditsService, type CreditBalance } from '$lib/api/credits';
|
||||
|
|
@ -14,17 +16,23 @@
|
|||
yearly: 360,
|
||||
};
|
||||
|
||||
const INTERVAL_LABELS: Record<BillingInterval, string> = {
|
||||
monthly: 'Monatlich',
|
||||
quarterly: 'Quartalsweise',
|
||||
yearly: 'Jährlich',
|
||||
};
|
||||
const intervalLabels = $derived<Record<BillingInterval, string>>({
|
||||
monthly: $_('settings.sync.interval_monthly'),
|
||||
quarterly: $_('settings.sync.interval_quarterly'),
|
||||
yearly: $_('settings.sync.interval_yearly'),
|
||||
});
|
||||
|
||||
const INTERVAL_HINTS: Record<BillingInterval, string> = {
|
||||
monthly: 'jeden Monat abgerechnet · ~1 Credit/Tag',
|
||||
quarterly: 'alle 3 Monate abgerechnet',
|
||||
yearly: 'einmal jährlich abgerechnet',
|
||||
};
|
||||
const intervalShortLabels = $derived<Record<BillingInterval, string>>({
|
||||
monthly: $_('settings.sync.interval_short_monthly'),
|
||||
quarterly: $_('settings.sync.interval_short_quarterly'),
|
||||
yearly: $_('settings.sync.interval_short_yearly'),
|
||||
});
|
||||
|
||||
const intervalHints = $derived<Record<BillingInterval, string>>({
|
||||
monthly: $_('settings.sync.interval_hint_monthly'),
|
||||
quarterly: $_('settings.sync.interval_hint_quarterly'),
|
||||
yearly: $_('settings.sync.interval_hint_yearly'),
|
||||
});
|
||||
|
||||
let balance = $state<CreditBalance | null>(null);
|
||||
let loading = $state(false);
|
||||
|
|
@ -55,9 +63,9 @@
|
|||
try {
|
||||
await syncBilling.activate(selectedInterval);
|
||||
await loadBalance();
|
||||
toast.success('Cloud Sync aktiviert!');
|
||||
toast.success($_('settings.sync.toast_activated'));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Aktivierung fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('settings.sync.err_activation_failed');
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -65,14 +73,14 @@
|
|||
}
|
||||
|
||||
async function handleDeactivate() {
|
||||
if (!confirm('Cloud Sync wirklich deaktivieren? Deine Daten bleiben lokal erhalten.')) return;
|
||||
if (!confirm($_('settings.sync.deactivate_confirm'))) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
await syncBilling.deactivate();
|
||||
toast.success('Cloud Sync deaktiviert');
|
||||
toast.success($_('settings.sync.toast_deactivated'));
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Deaktivierung fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('settings.sync.err_deactivation_failed');
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -85,9 +93,13 @@
|
|||
error = null;
|
||||
try {
|
||||
await syncBilling.changeInterval(selectedInterval);
|
||||
toast.success(`Intervall auf ${INTERVAL_LABELS[selectedInterval]} geändert`);
|
||||
toast.success(
|
||||
$_('settings.sync.toast_interval_changed', {
|
||||
values: { label: intervalLabels[selectedInterval] },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Änderung fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('settings.sync.err_change_failed');
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
|
@ -95,11 +107,11 @@
|
|||
}
|
||||
|
||||
function formatCredits(amount: number): string {
|
||||
return amount.toLocaleString('de-DE');
|
||||
return amount.toLocaleString(get(locale) ?? 'de');
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
return new Date(dateStr).toLocaleDateString('de-DE', {
|
||||
return new Date(dateStr).toLocaleDateString(get(locale) ?? 'de', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
|
|
@ -110,8 +122,8 @@
|
|||
<SettingsPanel id="cloud-sync">
|
||||
<SettingsSectionHeader
|
||||
icon={Cloud}
|
||||
title="Cloud Sync"
|
||||
description="Synchronisiere deine Daten über alle Geräte"
|
||||
title={$_('settings.sync.title')}
|
||||
description={$_('settings.sync.description')}
|
||||
tone="blue"
|
||||
/>
|
||||
|
||||
|
|
@ -123,29 +135,31 @@
|
|||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Status</p>
|
||||
<p class="row-title">{$_('settings.sync.status_label')}</p>
|
||||
<p class="row-desc">
|
||||
{#if syncBilling.active && syncBilling.nextChargeAt}
|
||||
Nächste Abbuchung am {formatDate(syncBilling.nextChargeAt)}
|
||||
{$_('settings.sync.status_next_charge', {
|
||||
values: { date: formatDate(syncBilling.nextChargeAt) },
|
||||
})}
|
||||
{:else if syncBilling.active}
|
||||
Synchronisiert verschlüsselt über alle Geräte
|
||||
{$_('settings.sync.status_encrypted')}
|
||||
{:else if syncBilling.paused}
|
||||
Credits reichen nicht aus — lade Credits auf um fortzufahren
|
||||
{$_('settings.sync.status_insufficient')}
|
||||
{:else}
|
||||
Deine Daten sind nur lokal auf diesem Gerät gespeichert
|
||||
{$_('settings.sync.status_local_only')}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge" class:active={syncBilling.active} class:paused={syncBilling.paused}>
|
||||
{#if syncBilling.active}
|
||||
<CloudCheck size={14} weight="fill" />
|
||||
Aktiv
|
||||
{$_('settings.sync.badge_active')}
|
||||
{:else if syncBilling.paused}
|
||||
<Pause size={14} weight="fill" />
|
||||
Pausiert
|
||||
{$_('settings.sync.badge_paused')}
|
||||
{:else}
|
||||
<Cloud size={14} />
|
||||
Inaktiv
|
||||
{$_('settings.sync.badge_inactive')}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -153,9 +167,11 @@
|
|||
{#if balance}
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Verfügbare Credits</p>
|
||||
<p class="row-title">{$_('settings.sync.credits_available')}</p>
|
||||
<p class="row-desc">
|
||||
<a class="inline-link" href="/?app=credits&tab=packages">Credits aufladen</a>
|
||||
<a class="inline-link" href="/?app=credits&tab=packages"
|
||||
>{$_('settings.sync.credits_topup')}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<span class="value">{formatCredits(balance.balance)}</span>
|
||||
|
|
@ -164,9 +180,14 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Abrechnungsintervall</p>
|
||||
<p class="row-title">{$_('settings.sync.interval_label')}</p>
|
||||
<p class="row-desc">
|
||||
{SYNC_PRICES[selectedInterval]} Credits · {INTERVAL_HINTS[selectedInterval]}
|
||||
{$_('settings.sync.interval_price_hint', {
|
||||
values: {
|
||||
price: SYNC_PRICES[selectedInterval],
|
||||
hint: intervalHints[selectedInterval],
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
|
|
@ -176,7 +197,7 @@
|
|||
class:active={selectedInterval === iv}
|
||||
onclick={() => (selectedInterval = iv)}
|
||||
>
|
||||
{iv === 'monthly' ? 'Monat' : iv === 'quarterly' ? 'Quartal' : 'Jahr'}
|
||||
{intervalShortLabels[iv]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -191,12 +212,16 @@
|
|||
{#if syncBilling.active}
|
||||
{#if intervalChanged}
|
||||
<button class="btn-secondary" onclick={handleChangeInterval} disabled={loading}>
|
||||
{loading ? 'Wird geändert…' : `Auf ${INTERVAL_LABELS[selectedInterval]} wechseln`}
|
||||
{loading
|
||||
? $_('settings.sync.changing')
|
||||
: $_('settings.sync.change_to', {
|
||||
values: { label: intervalLabels[selectedInterval] },
|
||||
})}
|
||||
</button>
|
||||
<p class="muted-hint">Änderung gilt ab der nächsten Abbuchung</p>
|
||||
<p class="muted-hint">{$_('settings.sync.change_hint')}</p>
|
||||
{/if}
|
||||
<button class="btn-danger" onclick={handleDeactivate} disabled={loading}>
|
||||
{loading ? 'Wird deaktiviert…' : 'Cloud Sync deaktivieren'}
|
||||
{loading ? $_('settings.sync.deactivating') : $_('settings.sync.deactivate')}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
|
|
@ -205,23 +230,21 @@
|
|||
disabled={loading || insufficientCredits}
|
||||
>
|
||||
{#if loading}
|
||||
Wird aktiviert…
|
||||
{$_('settings.sync.activating')}
|
||||
{:else}
|
||||
Aktivieren · {SYNC_PRICES[selectedInterval]} Credits
|
||||
{$_('settings.sync.activate', { values: { price: SYNC_PRICES[selectedInterval] } })}
|
||||
{/if}
|
||||
</button>
|
||||
{#if insufficientCredits}
|
||||
<p class="warn-hint">
|
||||
Nicht genügend Credits. <a href="/?app=credits&tab=packages">Aufladen</a>
|
||||
{$_('settings.sync.insufficient_warn_pre')}
|
||||
<a href="/?app=credits&tab=packages">{$_('settings.sync.insufficient_topup')}</a>
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="footnote">
|
||||
Cloud Sync synchronisiert deine Daten verschlüsselt über alle Geräte. Lokale Daten bleiben
|
||||
immer erhalten — auch wenn Sync pausiert oder deaktiviert wird.
|
||||
</p>
|
||||
<p class="footnote">{$_('settings.sync.footnote')}</p>
|
||||
{/if}
|
||||
</SettingsPanel>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
See docs/plans/space-scoped-data-model.md §5.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, Trash, Star, CheckCircle } from '@mana/shared-icons';
|
||||
import { useUserTagPresets } from '$lib/data/tag-presets/queries';
|
||||
import { tagPresetsStore } from '$lib/data/tag-presets/store.svelte';
|
||||
|
|
@ -42,11 +43,11 @@
|
|||
async function createFromActiveSpace() {
|
||||
error = null;
|
||||
if (!newName.trim()) {
|
||||
error = 'Bitte einen Namen eingeben';
|
||||
error = $_('settings.tag_presets.name_required');
|
||||
return;
|
||||
}
|
||||
if (!activeSpace) {
|
||||
error = 'Kein aktiver Space';
|
||||
error = $_('settings.tag_presets.no_active_space');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +87,7 @@
|
|||
}
|
||||
|
||||
async function handleDelete(id: string, name: string) {
|
||||
if (!confirm(`Preset „${name}" löschen?`)) return;
|
||||
if (!confirm($_('settings.tag_presets.delete_confirm', { values: { name } }))) return;
|
||||
await tagPresetsStore.deletePreset(id);
|
||||
}
|
||||
|
||||
|
|
@ -97,17 +98,14 @@
|
|||
|
||||
<section id="tag-presets">
|
||||
<header>
|
||||
<h2>Tag-Presets</h2>
|
||||
<p class="hint">
|
||||
Gespeicherte Tag-Sets, die du beim Anlegen eines neuen Space als Start-Vorlage wählen kannst.
|
||||
Änderungen am Preset berühren bereits erstellte Spaces nicht — es ist eine Einweg-Kopie.
|
||||
</p>
|
||||
<h2>{$_('settings.tag_presets.title')}</h2>
|
||||
<p class="hint">{$_('settings.tag_presets.hint')}</p>
|
||||
</header>
|
||||
|
||||
<div class="create-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={'Preset-Name (z.B. „Mein Standard-Set")'}
|
||||
placeholder={$_('settings.tag_presets.name_placeholder')}
|
||||
bind:value={newName}
|
||||
disabled={creating || !activeSpace}
|
||||
/>
|
||||
|
|
@ -120,20 +118,17 @@
|
|||
<Plus size={14} />
|
||||
<span>
|
||||
{creating
|
||||
? 'Erstelle …'
|
||||
? $_('settings.tag_presets.creating')
|
||||
: activeSpace
|
||||
? `Aus „${activeSpace.name}" erstellen`
|
||||
: 'Lade Space …'}
|
||||
? $_('settings.tag_presets.create_from', { values: { name: activeSpace.name } })
|
||||
: $_('settings.tag_presets.loading_space')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{#if error}<p class="error">{error}</p>{/if}
|
||||
|
||||
{#if presets.value.length === 0}
|
||||
<p class="empty">
|
||||
Noch keine Presets. Lege das erste an, indem du dem aktuellen Tag-Set einen Namen gibst — es
|
||||
wird dann automatisch als Default für neue Spaces verwendet.
|
||||
</p>
|
||||
<p class="empty">{$_('settings.tag_presets.empty')}</p>
|
||||
{:else}
|
||||
<ul class="preset-list">
|
||||
{#each presets.value as preset (preset.id)}
|
||||
|
|
@ -144,14 +139,20 @@
|
|||
{#if preset.isDefault}
|
||||
<span class="default-badge">
|
||||
<CheckCircle size={12} weight="fill" />
|
||||
Default
|
||||
{$_('settings.tag_presets.badge_default')}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="preset-meta">
|
||||
{preset.tags.length} Tag{preset.tags.length === 1 ? '' : 's'}
|
||||
{preset.tags.length === 1
|
||||
? $_('settings.tag_presets.tag_count_one', {
|
||||
values: { count: preset.tags.length },
|
||||
})
|
||||
: $_('settings.tag_presets.tag_count_other', {
|
||||
values: { count: preset.tags.length },
|
||||
})}
|
||||
{#if preset.tags.some((t) => t.groupName)}
|
||||
· mit Gruppen
|
||||
· {$_('settings.tag_presets.with_groups')}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -161,8 +162,8 @@
|
|||
type="button"
|
||||
class="icon-btn"
|
||||
onclick={() => handleSetDefault(preset.id)}
|
||||
title="Als Default setzen"
|
||||
aria-label="Als Default setzen"
|
||||
title={$_('settings.tag_presets.aria_set_default')}
|
||||
aria-label={$_('settings.tag_presets.aria_set_default')}
|
||||
>
|
||||
<Star size={16} />
|
||||
</button>
|
||||
|
|
@ -171,8 +172,8 @@
|
|||
type="button"
|
||||
class="icon-btn danger"
|
||||
onclick={() => handleDelete(preset.id, preset.name)}
|
||||
title="Löschen"
|
||||
aria-label="Löschen"
|
||||
title={$_('settings.tag_presets.aria_delete')}
|
||||
aria-label={$_('settings.tag_presets.aria_delete')}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import {
|
||||
Lock,
|
||||
LockOpen,
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
const expected = (generatedCode ?? '').replace(/[\s-]/g, '').toUpperCase();
|
||||
const actual = confirmCodeInput.replace(/[\s-]/g, '').toUpperCase();
|
||||
if (actual !== expected) {
|
||||
zkError = 'Der eingegebene Code stimmt nicht mit dem angezeigten überein.';
|
||||
zkError = $_('settings.vault.err_code_mismatch');
|
||||
return;
|
||||
}
|
||||
zkSetupStep = 'enabling';
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
zkSetupStep = 'enabled';
|
||||
generatedCode = null;
|
||||
confirmCodeInput = '';
|
||||
toast.success('Zero-Knowledge-Modus aktiviert');
|
||||
toast.success($_('settings.vault.toast_zk_enabled'));
|
||||
} catch (e) {
|
||||
zkError = (e as Error).message;
|
||||
} finally {
|
||||
|
|
@ -94,7 +95,7 @@
|
|||
zkBusy = true;
|
||||
try {
|
||||
await vaultClient.disableZeroKnowledge();
|
||||
toast.success('Zero-Knowledge-Modus deaktiviert');
|
||||
toast.success($_('settings.vault.toast_zk_disabled'));
|
||||
confirmDisableZk = false;
|
||||
zkSetupStep = 'idle';
|
||||
} catch (e) {
|
||||
|
|
@ -109,7 +110,7 @@
|
|||
zkBusy = true;
|
||||
try {
|
||||
await vaultClient.clearRecoveryCode();
|
||||
toast.success('Recovery-Code entfernt');
|
||||
toast.success($_('settings.vault.toast_recovery_cleared'));
|
||||
confirmClearRecovery = false;
|
||||
zkSetupStep = 'idle';
|
||||
hasRecoveryWrap = false;
|
||||
|
|
@ -123,8 +124,8 @@
|
|||
function handleCopyCode() {
|
||||
if (!generatedCode) return;
|
||||
navigator.clipboard.writeText(generatedCode).then(
|
||||
() => toast.success('Code in die Zwischenablage kopiert'),
|
||||
() => toast.error('Konnte Code nicht kopieren')
|
||||
() => toast.success($_('settings.vault.toast_clipboard_ok')),
|
||||
() => toast.error($_('settings.vault.toast_clipboard_failed'))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -145,8 +146,8 @@
|
|||
function handleCopyRotatedCode() {
|
||||
if (!rotatedCode) return;
|
||||
navigator.clipboard.writeText(rotatedCode).then(
|
||||
() => toast.success('Code in die Zwischenablage kopiert'),
|
||||
() => toast.error('Konnte Code nicht kopieren')
|
||||
() => toast.success($_('settings.vault.toast_clipboard_ok')),
|
||||
() => toast.error($_('settings.vault.toast_clipboard_failed'))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -201,9 +202,9 @@
|
|||
const result = await vaultClient.unlock();
|
||||
vaultState = result;
|
||||
if (result.status === 'unlocked') {
|
||||
toast.success('Verschlüsselungsschlüssel geladen');
|
||||
toast.success($_('settings.vault.toast_unlocked'));
|
||||
} else {
|
||||
toast.error(`Schlüssel konnte nicht geladen werden: ${result.status}`);
|
||||
toast.error($_('settings.vault.toast_unlock_failed', { values: { status: result.status } }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,9 +215,11 @@
|
|||
vaultState = result;
|
||||
confirmRotate = false;
|
||||
if (result.status === 'unlocked') {
|
||||
toast.success('Schlüssel rotiert. Neue Schreibvorgänge verwenden den neuen Schlüssel.');
|
||||
toast.success($_('settings.vault.toast_rotated'));
|
||||
} else {
|
||||
toast.error(`Rotation fehlgeschlagen: ${result.status}`);
|
||||
toast.error(
|
||||
$_('settings.vault.toast_rotate_failed', { values: { status: result.status } })
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rotating = false;
|
||||
|
|
@ -229,15 +232,19 @@
|
|||
tone: BadgeTone;
|
||||
icon: Component;
|
||||
} {
|
||||
if (s.status === 'unlocked') return { label: 'Verschlüsselt', tone: 'green', icon: Lock };
|
||||
if (s.status === 'locked') return { label: 'Gesperrt', tone: 'amber', icon: LockOpen };
|
||||
if (s.status === 'unlocked')
|
||||
return { label: $_('settings.vault.badge_encrypted'), tone: 'green', icon: Lock };
|
||||
if (s.status === 'locked')
|
||||
return { label: $_('settings.vault.badge_locked'), tone: 'amber', icon: LockOpen };
|
||||
if (s.status === 'awaiting-recovery-code')
|
||||
return { label: 'Recovery-Code erforderlich', tone: 'amber', icon: Key };
|
||||
return { label: $_('settings.vault.badge_recovery_required'), tone: 'amber', icon: Key };
|
||||
if (s.reason === 'auth')
|
||||
return { label: 'Anmeldung erforderlich', tone: 'red', icon: WarningCircle };
|
||||
if (s.reason === 'network') return { label: 'Netzwerkfehler', tone: 'red', icon: WifiSlash };
|
||||
if (s.reason === 'server') return { label: 'Server-Fehler', tone: 'red', icon: WarningCircle };
|
||||
return { label: 'Unbekannter Fehler', tone: 'red', icon: WarningCircle };
|
||||
return { label: $_('settings.vault.badge_auth_required'), tone: 'red', icon: WarningCircle };
|
||||
if (s.reason === 'network')
|
||||
return { label: $_('settings.vault.badge_network_error'), tone: 'red', icon: WifiSlash };
|
||||
if (s.reason === 'server')
|
||||
return { label: $_('settings.vault.badge_server_error'), tone: 'red', icon: WarningCircle };
|
||||
return { label: $_('settings.vault.badge_unknown_error'), tone: 'red', icon: WarningCircle };
|
||||
}
|
||||
|
||||
const badge = $derived(statusBadge(vaultState));
|
||||
|
|
@ -260,8 +267,8 @@
|
|||
|
||||
<SettingsSectionHeader
|
||||
icon={Lock}
|
||||
title="Verschlüsselung"
|
||||
description="Sensitive Felder werden mit AES-GCM-256 verschlüsselt, bevor sie in die lokale Datenbank geschrieben werden."
|
||||
title={$_('settings.vault.title')}
|
||||
description={$_('settings.vault.description')}
|
||||
tone="blue"
|
||||
action={statusBadgeSnippet}
|
||||
/>
|
||||
|
|
@ -271,45 +278,47 @@
|
|||
<div class="status-body">
|
||||
{#if vaultState.status === 'unlocked'}
|
||||
<p class="status-text">
|
||||
Dein persönlicher Schlüssel ist auf diesem Gerät geladen.
|
||||
<strong>{totalEncryptedFields} Felder</strong>
|
||||
über <strong>{encryptedTables.length} Tabellen</strong> werden verschlüsselt gespeichert.
|
||||
{$_('settings.vault.status_unlocked_pre')}
|
||||
<strong
|
||||
>{$_('settings.vault.status_unlocked_fields', {
|
||||
values: { count: totalEncryptedFields },
|
||||
})}</strong
|
||||
>
|
||||
<strong
|
||||
>{$_('settings.vault.status_unlocked_tables', {
|
||||
values: { count: encryptedTables.length },
|
||||
})}</strong
|
||||
>
|
||||
{$_('settings.vault.status_unlocked_post')}
|
||||
</p>
|
||||
{:else if vaultState.status === 'locked'}
|
||||
<p class="status-text">
|
||||
Dein Schlüssel ist nicht geladen. Verschlüsselte Inhalte können nicht gelesen werden, bis du
|
||||
dich erneut anmeldest oder den Schlüssel manuell lädst.
|
||||
</p>
|
||||
<p class="status-text">{$_('settings.vault.status_locked')}</p>
|
||||
<button class="btn btn-primary" type="button" onclick={handleUnlock}>
|
||||
Schlüssel jetzt laden
|
||||
{$_('settings.vault.status_load_now')}
|
||||
</button>
|
||||
{:else}
|
||||
<p class="status-text">
|
||||
Es gab ein Problem beim Laden deines Verschlüsselungsschlüssels. Bitte melde dich neu an
|
||||
oder prüfe deine Internetverbindung.
|
||||
</p>
|
||||
<button class="btn" type="button" onclick={handleUnlock}>Erneut versuchen</button>
|
||||
<p class="status-text">{$_('settings.vault.status_error')}</p>
|
||||
<button class="btn" type="button" onclick={handleUnlock}
|
||||
>{$_('settings.vault.status_retry')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- What Mana can see (threat-model disclosure, moved near top) -->
|
||||
<div class="subsection">
|
||||
<h3 class="subsection-title">Was Mana sehen kann</h3>
|
||||
<h3 class="subsection-title">{$_('settings.vault.threat_title')}</h3>
|
||||
<ul class="disclosure-list">
|
||||
<li>
|
||||
<strong>Was Mana nie sieht:</strong> deine verschlüsselten Inhalte (Chat, Notizen, Träume, Memos,
|
||||
Kontaktdetails, Zyklus-Notizen, Transaktionsbeschreibungen, …). Sie verlassen dein Gerät nur als
|
||||
unleserlicher Blob.
|
||||
<strong>{$_('settings.vault.threat_never_label')}</strong>
|
||||
{$_('settings.vault.threat_never_body')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Was Mana technisch entschlüsseln könnte:</strong> deinen Master-Key, falls ein Mitarbeiter
|
||||
mit Zugriff auf den Schlüsselverschlüsselungsschlüssel aktiv darauf zugreift. In der Praxis ist
|
||||
das gegen alle realistischen Bedrohungen außer einer gerichtlich erzwungenen Offenlegung gegen
|
||||
Mana selbst geschützt.
|
||||
<strong>{$_('settings.vault.threat_could_label')}</strong>
|
||||
{$_('settings.vault.threat_could_body')}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Was strukturell sichtbar bleibt:</strong> Anzahl deiner Notizen / Chats / Kontakte, Zeitstempel,
|
||||
Verbindungen zwischen Records. Die Inhalte selbst nicht.
|
||||
<strong>{$_('settings.vault.threat_visible_label')}</strong>
|
||||
{$_('settings.vault.threat_visible_body')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -317,16 +326,14 @@
|
|||
<!-- Encrypted fields list -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-head">
|
||||
<h3 class="subsection-title">Verschlüsselte Felder</h3>
|
||||
<h3 class="subsection-title">{$_('settings.vault.fields_title')}</h3>
|
||||
<span class="subsection-meta">
|
||||
{totalEncryptedFields} Felder · {encryptedTables.length} Tabellen
|
||||
{$_('settings.vault.fields_meta', {
|
||||
values: { fields: totalEncryptedFields, tables: encryptedTables.length },
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p class="subsection-desc">
|
||||
Welche Spalten in welchen Tabellen verschlüsselt am Gerät liegen. Strukturelle Metadaten (IDs,
|
||||
Zeitstempel, Status-Flags) bleiben absichtlich im Klartext, damit Indizes, Sortierungen und
|
||||
Sync weiter funktionieren.
|
||||
</p>
|
||||
<p class="subsection-desc">{$_('settings.vault.fields_description')}</p>
|
||||
<ul class="table-list">
|
||||
{#each visibleTables as { table, fields } (table)}
|
||||
<li>
|
||||
|
|
@ -337,7 +344,9 @@
|
|||
</ul>
|
||||
{#if hiddenTableCount > 0}
|
||||
<button type="button" class="expand-btn" onclick={() => (fieldsExpanded = !fieldsExpanded)}>
|
||||
{fieldsExpanded ? 'Weniger anzeigen' : `${hiddenTableCount} weitere anzeigen`}
|
||||
{fieldsExpanded
|
||||
? $_('settings.vault.show_less')
|
||||
: $_('settings.vault.show_more', { values: { count: hiddenTableCount } })}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -345,21 +354,19 @@
|
|||
<!-- Zero-knowledge mode -->
|
||||
<div class="subsection">
|
||||
<div class="subsection-head">
|
||||
<h3 class="subsection-title">Zero-Knowledge-Modus</h3>
|
||||
<h3 class="subsection-title">{$_('settings.vault.zk_title')}</h3>
|
||||
<span class="zk-state-pill" class:on={zkActive}>
|
||||
{zkActive ? 'Aktiv' : 'Nicht aktiv'}
|
||||
{zkActive ? $_('settings.vault.zk_state_active') : $_('settings.vault.zk_state_inactive')}
|
||||
</span>
|
||||
</div>
|
||||
<p class="subsection-desc">
|
||||
<strong>Optional, fortgeschritten.</strong> Im Zero-Knowledge-Modus speichert Mana deinen
|
||||
Schlüssel <em>nur noch in einer Form, die wir selbst nicht entschlüsseln können</em>. Du
|
||||
brauchst dann beim Login von einem neuen Gerät deinen Recovery-Code, um deine Daten
|
||||
freizuschalten.
|
||||
<strong>{$_('settings.vault.zk_desc_optional')}</strong>{$_(
|
||||
'settings.vault.zk_desc_body_pre'
|
||||
)}<em>{$_('settings.vault.zk_desc_body_em')}</em>{$_('settings.vault.zk_desc_body_post')}
|
||||
</p>
|
||||
<p class="subsection-desc">
|
||||
<strong>Vorteil:</strong> selbst ein Mana-Mitarbeiter mit Vollzugriff auf den Server kann
|
||||
deine Inhalte nicht mehr lesen. <strong>Risiko:</strong> wenn du den Recovery-Code verlierst, sind
|
||||
deine Daten unwiderruflich weg — wir haben dann keinen Backup-Schlüssel mehr.
|
||||
<strong>{$_('settings.vault.zk_pro_label')}</strong>{$_('settings.vault.zk_pro_body')}
|
||||
<strong>{$_('settings.vault.zk_con_label')}</strong>{$_('settings.vault.zk_con_body')}
|
||||
</p>
|
||||
|
||||
{#if zkError}
|
||||
|
|
@ -373,10 +380,7 @@
|
|||
{#if hasRecoveryWrap}
|
||||
<div class="inline-alert tone-amber">
|
||||
<WarningCircle size={16} weight="fill" />
|
||||
<span>
|
||||
Du hast bereits einen Recovery-Code gespeichert, aber Zero-Knowledge ist noch nicht
|
||||
aktiv. Du kannst direkt aktivieren oder den Code zurücksetzen.
|
||||
</span>
|
||||
<span>{$_('settings.vault.zk_existing_warn')}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
|
|
@ -385,7 +389,7 @@
|
|||
disabled={vaultState.status !== 'unlocked' || zkBusy}
|
||||
onclick={handleEnableZeroKnowledge}
|
||||
>
|
||||
{zkBusy ? 'Aktiviere …' : 'Zero-Knowledge jetzt aktivieren'}
|
||||
{zkBusy ? $_('settings.vault.zk_enabling') : $_('settings.vault.zk_enable_now')}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
|
|
@ -393,7 +397,7 @@
|
|||
disabled={vaultState.status !== 'unlocked' || zkBusy}
|
||||
onclick={handleSetupRecoveryCode}
|
||||
>
|
||||
Neuen Recovery-Code generieren
|
||||
{$_('settings.vault.zk_generate_new')}
|
||||
</button>
|
||||
{#if !confirmClearRecovery}
|
||||
<button
|
||||
|
|
@ -402,7 +406,7 @@
|
|||
disabled={zkBusy}
|
||||
onclick={() => (confirmClearRecovery = true)}
|
||||
>
|
||||
Recovery-Code entfernen
|
||||
{$_('settings.vault.zk_clear')}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
|
|
@ -411,7 +415,7 @@
|
|||
disabled={zkBusy}
|
||||
onclick={handleClearRecoveryCode}
|
||||
>
|
||||
{zkBusy ? 'Entferne …' : 'Ja, entfernen'}
|
||||
{zkBusy ? $_('settings.vault.zk_clearing') : $_('settings.vault.zk_clear_confirm')}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
|
|
@ -419,7 +423,7 @@
|
|||
disabled={zkBusy}
|
||||
onclick={() => (confirmClearRecovery = false)}
|
||||
>
|
||||
Abbrechen
|
||||
{$_('settings.vault.cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -431,7 +435,7 @@
|
|||
disabled={vaultState.status !== 'unlocked' || zkBusy}
|
||||
onclick={handleSetupRecoveryCode}
|
||||
>
|
||||
{zkBusy ? 'Generiere …' : 'Recovery-Code einrichten'}
|
||||
{zkBusy ? $_('settings.vault.zk_generating') : $_('settings.vault.zk_setup')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -441,24 +445,24 @@
|
|||
<div class="wizard-step">
|
||||
<div class="step-head">
|
||||
<span class="step-num">1</span>
|
||||
<h4 class="step-title">Code sicher aufschreiben</h4>
|
||||
<h4 class="step-title">{$_('settings.vault.step_1_title')}</h4>
|
||||
</div>
|
||||
<p class="subsection-desc">
|
||||
Speichere diesen Code an einem sicheren Ort (Passwort-Manager, ausgedruckt im Tresor, …). <strong
|
||||
>Wir zeigen ihn dir nur ein einziges Mal.</strong
|
||||
{$_('settings.vault.step_1_desc_pre')}<strong
|
||||
>{$_('settings.vault.step_1_desc_strong')}</strong
|
||||
>
|
||||
</p>
|
||||
<div class="recovery-code">{generatedCode}</div>
|
||||
<div class="actions">
|
||||
<button class="btn" type="button" onclick={handleCopyCode}>
|
||||
<Copy size={14} weight="bold" />
|
||||
Kopieren
|
||||
{$_('settings.vault.copy')}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" onclick={handleStartConfirm}>
|
||||
Ich habe den Code gesichert →
|
||||
{$_('settings.vault.step_1_continue')}
|
||||
</button>
|
||||
<button class="btn btn-ghost" type="button" onclick={handleResetSetup}>
|
||||
Abbrechen
|
||||
{$_('settings.vault.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -468,17 +472,14 @@
|
|||
<div class="wizard-step">
|
||||
<div class="step-head">
|
||||
<span class="step-num">2</span>
|
||||
<h4 class="step-title">Code zurück eintippen</h4>
|
||||
<h4 class="step-title">{$_('settings.vault.step_2_title')}</h4>
|
||||
</div>
|
||||
<p class="subsection-desc">
|
||||
Tippe (oder paste) den Code, den du gerade gespeichert hast. So stellen wir sicher, dass
|
||||
der Backup wirklich vollständig ist.
|
||||
</p>
|
||||
<p class="subsection-desc">{$_('settings.vault.step_2_desc')}</p>
|
||||
<input
|
||||
class="recovery-input"
|
||||
type="text"
|
||||
bind:value={confirmCodeInput}
|
||||
placeholder="1A2B-3C4D-5E6F-..."
|
||||
placeholder={$_('settings.vault.step_2_placeholder')}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
|
|
@ -489,10 +490,10 @@
|
|||
disabled={!confirmCodeInput.trim()}
|
||||
onclick={handleConfirmCode}
|
||||
>
|
||||
Code bestätigen
|
||||
{$_('settings.vault.step_2_confirm')}
|
||||
</button>
|
||||
<button class="btn btn-ghost" type="button" onclick={handleResetSetup}>
|
||||
Abbrechen
|
||||
{$_('settings.vault.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -502,18 +503,16 @@
|
|||
<div class="wizard-step">
|
||||
<div class="step-head">
|
||||
<span class="step-num">3</span>
|
||||
<h4 class="step-title">Zero-Knowledge-Modus aktivieren</h4>
|
||||
<h4 class="step-title">{$_('settings.vault.step_3_title')}</h4>
|
||||
</div>
|
||||
<p class="subsection-desc">
|
||||
Wenn du jetzt aktivierst, löscht der Server seine Kopie deines Schlüssels. Ab sofort
|
||||
kannst du <strong>nur noch mit dem Recovery-Code</strong> auf deine verschlüsselten Daten zugreifen.
|
||||
{$_('settings.vault.step_3_desc_pre')}<strong
|
||||
>{$_('settings.vault.step_3_desc_strong')}</strong
|
||||
>{$_('settings.vault.step_3_desc_post')}
|
||||
</p>
|
||||
<div class="inline-alert tone-red">
|
||||
<WarningCircle size={16} weight="fill" />
|
||||
<span>
|
||||
Diese Aktion ist nicht rückgängig zu machen ohne den Recovery-Code. Wenn du deinen Code
|
||||
verlegst, sind deine Inhalte verloren.
|
||||
</span>
|
||||
<span>{$_('settings.vault.step_3_warn')}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
|
|
@ -522,10 +521,10 @@
|
|||
disabled={zkBusy}
|
||||
onclick={handleEnableZeroKnowledge}
|
||||
>
|
||||
{zkBusy ? 'Aktiviere …' : 'Ja, Zero-Knowledge-Modus aktivieren'}
|
||||
{zkBusy ? $_('settings.vault.zk_enabling') : $_('settings.vault.step_3_confirm')}
|
||||
</button>
|
||||
<button class="btn btn-ghost" type="button" disabled={zkBusy} onclick={handleResetSetup}>
|
||||
Abbrechen
|
||||
{$_('settings.vault.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
411
apps/mana/apps/web/src/lib/i18n/locales/settings/de.json
Normal file
411
apps/mana/apps/web/src/lib/i18n/locales/settings/de.json
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
{
|
||||
"categories": {
|
||||
"general": { "label": "Allgemein", "description": "Theme, Sprache, Benachrichtigungen" },
|
||||
"ai": { "label": "KI", "description": "Compute-Backend & Modelle" },
|
||||
"security": {
|
||||
"label": "Sicherheit",
|
||||
"description": "Passkeys, 2FA, Verschlüsselung & Sitzungen"
|
||||
},
|
||||
"privacy": {
|
||||
"label": "Privatsphäre",
|
||||
"description": "Was ist gerade öffentlich oder per Link geteilt — mit Kill-Switch."
|
||||
},
|
||||
"data": { "label": "Daten & Sync", "description": "Cloud-Sync, Export, Backup & DSGVO" },
|
||||
"tag_presets": { "label": "Tag-Presets", "description": "Tag-Sets für neue Spaces" }
|
||||
},
|
||||
"sidebar": {
|
||||
"aria_categories": "Einstellungs-Kategorien",
|
||||
"search_placeholder": "Einstellungen suchen…",
|
||||
"aria_search": "Einstellungen durchsuchen",
|
||||
"aria_clear": "Suche leeren",
|
||||
"no_results": "Keine Treffer für „{query}\""
|
||||
},
|
||||
"search": {
|
||||
"theme": { "label": "Theme", "keywords": "dark, light, farbe, design" },
|
||||
"language": { "label": "Sprache", "keywords": "language, i18n, deutsch, english" },
|
||||
"notifications": { "label": "Benachrichtigungen", "keywords": "notification, sound" },
|
||||
"ai_options": { "label": "KI-Optionen", "keywords": "llm, ai, compute" },
|
||||
"browser_model": {
|
||||
"label": "Browser-Modell (Gemma)",
|
||||
"keywords": "gemma, webgpu, lokal, offline"
|
||||
},
|
||||
"mana_server": { "label": "Mana-Server (KI)", "keywords": "server, self-hosted" },
|
||||
"cloud_ai": { "label": "Cloud-KI (Gemini)", "keywords": "google, cloud, gemini" },
|
||||
"passkeys": { "label": "Passkeys", "keywords": "webauthn, fido, biometrie" },
|
||||
"sessions": { "label": "Aktive Sessions", "keywords": "logout, gerät, device" },
|
||||
"two_factor": { "label": "Zwei-Faktor (2FA)", "keywords": "totp, 2fa, mfa" },
|
||||
"vault": {
|
||||
"label": "Verschlüsselung",
|
||||
"keywords": "vault, encryption, aes, schlüssel, zero-knowledge"
|
||||
},
|
||||
"security_log": { "label": "Sicherheits-Log", "keywords": "audit, history, verlauf" },
|
||||
"privacy_overview": {
|
||||
"label": "Privatsphäre-Übersicht",
|
||||
"keywords": "public, öffentlich, unlisted, teilen, sharing, link"
|
||||
},
|
||||
"reset_all_private": {
|
||||
"label": "Alle auf privat zurücksetzen",
|
||||
"keywords": "kill-switch, kill switch, reset, privat, widerrufen"
|
||||
},
|
||||
"cloud_sync": { "label": "Cloud Sync", "keywords": "sync, geräte" },
|
||||
"data_export": { "label": "Daten exportieren", "keywords": "export, dsgvo, gdpr, json" },
|
||||
"auth_data": { "label": "Authentifizierung", "keywords": "sessions, 2fa, login" },
|
||||
"credits_data": { "label": "Credits & Transaktionen", "keywords": "guthaben, transaktionen" },
|
||||
"project_data": { "label": "Projektdaten", "keywords": "projekte, apps, statistik" },
|
||||
"retention": { "label": "Aufbewahrungsfristen", "keywords": "retention, dsgvo, fristen" },
|
||||
"backup": {
|
||||
"label": "Backup & Wiederherstellung",
|
||||
"keywords": "backup, restore, import, archiv, .mana"
|
||||
},
|
||||
"delete_account": { "label": "Konto löschen", "keywords": "delete, gdpr, dsgvo, gefahrenzone" }
|
||||
},
|
||||
"general": {
|
||||
"title": "Allgemein",
|
||||
"description": "Sprache, Wochenstart & Sounds",
|
||||
"language_title": "Anzeigesprache",
|
||||
"language_description": "Sprache der Benutzeroberfläche",
|
||||
"color_scheme_title": "Farbschema",
|
||||
"color_scheme_description": "Akzentfarbe der Oberfläche",
|
||||
"color_scheme_ocean": "Ozean",
|
||||
"color_scheme_nature": "Natur",
|
||||
"color_scheme_lume": "Lume",
|
||||
"color_scheme_stone": "Stein",
|
||||
"color_mode_title": "Farbmodus",
|
||||
"color_mode_description": "Hell, Dunkel oder automatisch",
|
||||
"color_mode_light": "Hell",
|
||||
"color_mode_dark": "Dunkel",
|
||||
"color_mode_system": "System",
|
||||
"week_start_title": "Wochenstart",
|
||||
"week_start_description": "Erster Tag der Woche in Kalendern",
|
||||
"week_start_monday": "Montag",
|
||||
"week_start_sunday": "Sonntag",
|
||||
"sounds_title": "Sounds",
|
||||
"sounds_description": "Sound-Effekte in allen Apps",
|
||||
"sounds_aria": "Sounds ein- oder ausschalten",
|
||||
"onboarding_title": "Onboarding erneut durchlaufen",
|
||||
"onboarding_description": "Name, Look und Module neu wählen",
|
||||
"onboarding_start": "Starten",
|
||||
"onboarding_starting": "Starte…"
|
||||
},
|
||||
"ai": {
|
||||
"title": "KI-Optionen",
|
||||
"description": "Wähle, welche KI-Schichten Mana verwenden darf — von gar keiner bis zu allen",
|
||||
"tier_local_title": "Lokal (ohne KI)",
|
||||
"tier_local_badge": "immer aktiv",
|
||||
"tier_local_subtitle": "Basis-Funktionen ohne jede KI",
|
||||
"tier_local_description": "Mana funktioniert auch ganz ohne KI: Datum-Erkennung, Suche und einfache Klassifikation laufen über klassische Algorithmen. Manche Funktionen sind dann begrenzt, dafür ist alles 100% offline und kostet nichts.",
|
||||
"tier_browser_title": "Auf deinem Gerät",
|
||||
"tier_browser_subtitle": "Gemma 4 E2B (Google) im Browser",
|
||||
"tier_browser_bullet_1": "100% lokal — Daten verlassen dein Gerät nicht",
|
||||
"tier_browser_bullet_2": "~500 MB einmaliger Download",
|
||||
"tier_browser_bullet_3": "Braucht WebGPU + 2 GB freien GPU-Speicher",
|
||||
"tier_browser_blocked": "WebGPU nicht verfügbar in deinem Browser. Funktioniert in Chrome/Edge 113+ oder Safari 18+.",
|
||||
"tier_browser_loaded": "✓ Modell geladen",
|
||||
"tier_browser_loading": "Lade {name} ({percent}%)…",
|
||||
"tier_browser_load_btn": "Modell laden (~{size} MB)",
|
||||
"tier_browser_load_btn_loading": "Lade…",
|
||||
"tier_mana_server_title": "Mana-Server",
|
||||
"tier_mana_server_subtitle": "Selbst-gehostet auf unserem Mac Mini",
|
||||
"tier_mana_server_bullet_1": "Schneller und stärker als Browser-LLM",
|
||||
"tier_mana_server_bullet_2": "Daten laufen verschlüsselt zu unserem Server",
|
||||
"tier_mana_server_bullet_3": "Keine Inhalte werden gespeichert",
|
||||
"tier_byok_title": "Eigener API-Key",
|
||||
"tier_byok_subtitle": "OpenAI, Anthropic, Google Gemini oder Mistral",
|
||||
"tier_byok_bullet_1": "Direkt aus dem Browser — keine Mana-Server-Zwischenstation",
|
||||
"tier_byok_bullet_2": "Du zahlst beim Provider, wir sehen nichts davon",
|
||||
"tier_byok_bullet_3": "Schlüssel werden verschlüsselt in deinem Vault gespeichert",
|
||||
"tier_cloud_title": "Google Gemini",
|
||||
"tier_cloud_subtitle": "Cloud-API über unseren Server-Proxy",
|
||||
"tier_cloud_bullet_1": "Stärkste Qualität für komplexe Aufgaben",
|
||||
"tier_cloud_bullet_2": "Schnellste Antworten",
|
||||
"tier_cloud_bullet_3": "Daten werden von Google verarbeitet",
|
||||
"tier_cloud_warning": "Nur empfehlenswert für nicht-sensitive Inhalte",
|
||||
"tier_cloud_consent_title": "Bestätigung erforderlich",
|
||||
"tier_cloud_consent_desc": "Cloud-Anfragen senden deine Inhalte an Google. Bitte bestätige, dass du das verstanden hast und akzeptierst.",
|
||||
"tier_cloud_consent_btn": "Verstanden, Cloud aktivieren",
|
||||
"tier_cloud_consent_ok": "✓ Cloud-Zustimmung erteilt",
|
||||
"tier_cloud_consent_revoke": "Zurücknehmen",
|
||||
"badge_active": "aktiv",
|
||||
"summary_label": "Aktuelle Reihenfolge:",
|
||||
"summary_local_only": "Nur lokal (ohne KI) — die meisten KI-Funktionen sind begrenzt.",
|
||||
"summary_chain_local_fallback": "Lokal (Fallback)",
|
||||
"fallback_title": "Bei Fehler auf „Lokal\" zurückfallen",
|
||||
"fallback_description": "Wenn die gewählte KI-Schicht eine Anfrage nicht beantworten kann, versucht Mana es mit der lokalen Variante (sofern verfügbar). Aus: zeigt stattdessen einen Fehler an.",
|
||||
"fallback_aria": "Fallback auf Lokal",
|
||||
"show_source_title": "Quelle bei jedem KI-Resultat anzeigen",
|
||||
"show_source_description": "Zeigt unter jeder KI-generierten Antwort eine kleine Markierung wie „Auf deinem Gerät\" oder „via Google Gemini\" — damit du immer siehst, wo deine Daten gerade verarbeitet wurden.",
|
||||
"show_source_aria": "Quelle anzeigen"
|
||||
},
|
||||
"byok": {
|
||||
"vault_locked": "Vault ist gesperrt — bitte zuerst anmelden um Keys zu verwalten.",
|
||||
"loading": "Lädt…",
|
||||
"add_first": "Ersten API-Key hinzufügen",
|
||||
"add_more": "Weiteren Key hinzufügen",
|
||||
"field_provider": "Provider",
|
||||
"field_label": "Label",
|
||||
"field_label_placeholder": "z.B. Privat OpenAI",
|
||||
"field_api_key": "API-Key",
|
||||
"field_api_key_placeholder_generic": "API-Key",
|
||||
"field_model": "Modell",
|
||||
"field_model_default": "Default ({model})",
|
||||
"field_default_short": "Default",
|
||||
"field_as_default": "Als Standard",
|
||||
"label_required": "Label und API-Key sind Pflicht",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern",
|
||||
"saving": "Speichern…",
|
||||
"confirm_delete": "Schlüssel wirklich löschen?",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"set_default": "Standard",
|
||||
"badge_default": "Standard",
|
||||
"meta_line": "{provider} · {model} · {count} Aufrufe · {cost}"
|
||||
},
|
||||
"security": {
|
||||
"title": "Sicherheit",
|
||||
"description": "Passkeys, 2FA, Verschlüsselung & Sitzungen"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privatsphäre-Übersicht",
|
||||
"description": "Alle Einträge, die du gerade öffentlich zeigst oder per Link teilst — mit ein-Klick-Rückzieher.",
|
||||
"summary_public": "öffentlich",
|
||||
"summary_unlisted": "per Link teilbar",
|
||||
"loading": "Lädt…",
|
||||
"empty": "Aktuell ist nichts öffentlich oder per Link geteilt — gut gemacht.",
|
||||
"badge_public": "Öffentlich",
|
||||
"badge_unlisted": "Per Link",
|
||||
"open": "Öffnen",
|
||||
"set_private": "Privat",
|
||||
"kill_all": "Alle auf privat zurücksetzen",
|
||||
"confirm_one": "{count} Eintrag wird auf \"Space\" zurückgesetzt. Aktive Share-Links werden widerrufen. Fortfahren?",
|
||||
"confirm_other": "{count} Einträge werden auf \"Space\" zurückgesetzt. Aktive Share-Links werden widerrufen. Fortfahren?",
|
||||
"cancel": "Abbrechen",
|
||||
"killing": "Setze zurück…",
|
||||
"confirm_kill": "Ja, alles zurücksetzen",
|
||||
"toast_set_private": "„{title}\" ist jetzt privat",
|
||||
"toast_set_private_failed": "Konnte „{title}\" nicht zurückstufen",
|
||||
"toast_kill_partial": "{flipped} Einträge auf privat — {failed} fehlgeschlagen",
|
||||
"toast_kill_done": "{flipped} Einträge auf privat zurückgesetzt",
|
||||
"toast_kill_failed": "Kill-Switch fehlgeschlagen"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Cloud Sync",
|
||||
"description": "Synchronisiere deine Daten über alle Geräte",
|
||||
"interval_monthly": "Monatlich",
|
||||
"interval_quarterly": "Quartalsweise",
|
||||
"interval_yearly": "Jährlich",
|
||||
"interval_short_monthly": "Monat",
|
||||
"interval_short_quarterly": "Quartal",
|
||||
"interval_short_yearly": "Jahr",
|
||||
"interval_hint_monthly": "jeden Monat abgerechnet · ~1 Credit/Tag",
|
||||
"interval_hint_quarterly": "alle 3 Monate abgerechnet",
|
||||
"interval_hint_yearly": "einmal jährlich abgerechnet",
|
||||
"toast_activated": "Cloud Sync aktiviert!",
|
||||
"toast_deactivated": "Cloud Sync deaktiviert",
|
||||
"toast_interval_changed": "Intervall auf {label} geändert",
|
||||
"deactivate_confirm": "Cloud Sync wirklich deaktivieren? Deine Daten bleiben lokal erhalten.",
|
||||
"err_activation_failed": "Aktivierung fehlgeschlagen",
|
||||
"err_deactivation_failed": "Deaktivierung fehlgeschlagen",
|
||||
"err_change_failed": "Änderung fehlgeschlagen",
|
||||
"status_label": "Status",
|
||||
"status_next_charge": "Nächste Abbuchung am {date}",
|
||||
"status_encrypted": "Synchronisiert verschlüsselt über alle Geräte",
|
||||
"status_insufficient": "Credits reichen nicht aus — lade Credits auf um fortzufahren",
|
||||
"status_local_only": "Deine Daten sind nur lokal auf diesem Gerät gespeichert",
|
||||
"badge_active": "Aktiv",
|
||||
"badge_paused": "Pausiert",
|
||||
"badge_inactive": "Inaktiv",
|
||||
"credits_available": "Verfügbare Credits",
|
||||
"credits_topup": "Credits aufladen",
|
||||
"interval_label": "Abrechnungsintervall",
|
||||
"interval_price_hint": "{price} Credits · {hint}",
|
||||
"changing": "Wird geändert…",
|
||||
"change_to": "Auf {label} wechseln",
|
||||
"change_hint": "Änderung gilt ab der nächsten Abbuchung",
|
||||
"deactivating": "Wird deaktiviert…",
|
||||
"deactivate": "Cloud Sync deaktivieren",
|
||||
"activating": "Wird aktiviert…",
|
||||
"activate": "Aktivieren · {price} Credits",
|
||||
"insufficient_warn_pre": "Nicht genügend Credits.",
|
||||
"insufficient_topup": "Aufladen",
|
||||
"footnote": "Cloud Sync synchronisiert deine Daten verschlüsselt über alle Geräte. Lokale Daten bleiben immer erhalten — auch wenn Sync pausiert oder deaktiviert wird."
|
||||
},
|
||||
"mydata": {
|
||||
"title": "Meine Daten (DSGVO)",
|
||||
"description": "Übersicht über alle deine gespeicherten Daten",
|
||||
"qr_export": "Als QR-Code exportieren",
|
||||
"qr_short": "QR",
|
||||
"export": "Exportieren",
|
||||
"exporting": "Exportiere…",
|
||||
"retry": "Erneut versuchen",
|
||||
"no_name": "Kein Name",
|
||||
"verified": "verifiziert",
|
||||
"not_verified": "nicht verifiziert",
|
||||
"registered_at": "Registriert am",
|
||||
"total_entities": "Gesamt-Entitäten",
|
||||
"total_entities_desc": "Datensätze über alle Apps hinweg",
|
||||
"projects_with_data": "Projekte mit Daten",
|
||||
"footnote_pre": "Keine Tracking-Cookies — anonyme Analyse via Umami. Details in der ",
|
||||
"footnote_link": "Datenschutzerklärung",
|
||||
"footnote_post": ".",
|
||||
"auth_title": "Authentifizierung",
|
||||
"auth_description": "Sessions, Accounts & 2FA",
|
||||
"auth_sessions": "Aktive Sessions",
|
||||
"auth_accounts": "Verknüpfte Accounts",
|
||||
"auth_two_fa": "Zwei-Faktor (2FA)",
|
||||
"auth_two_fa_active": "Aktiviert",
|
||||
"auth_two_fa_inactive": "Deaktiviert",
|
||||
"auth_last_login": "Letzter Login",
|
||||
"credits_title": "Credits",
|
||||
"credits_description": "Guthaben & Transaktionen",
|
||||
"credits_balance": "Aktueller Stand",
|
||||
"credits_total_earned": "Gesamt verdient",
|
||||
"credits_total_spent": "Gesamt ausgegeben",
|
||||
"credits_transactions": "Transaktionen",
|
||||
"project_title": "Projektdaten",
|
||||
"project_description": "Datensätze pro App",
|
||||
"col_app": "App",
|
||||
"col_count": "Einträge",
|
||||
"col_time": "Letzte Aktivität",
|
||||
"project_status_active": "Aktiv",
|
||||
"project_status_empty": "Keine Daten",
|
||||
"project_status_unavailable": "Nicht verfügbar",
|
||||
"project_unreachable": "nicht erreichbar",
|
||||
"relative_just_now": "gerade eben",
|
||||
"relative_minutes_ago": "vor {n} Min",
|
||||
"relative_hours_ago": "vor {n} Std",
|
||||
"relative_days_ago": "vor {n} Tagen",
|
||||
"retention_title": "Aufbewahrungsfristen",
|
||||
"retention_description": "Wie lange wir deine Daten speichern",
|
||||
"retention_account": "Benutzerkonto & Profil",
|
||||
"retention_account_value": "Bis zur Löschung",
|
||||
"retention_sessions": "Sessions & Login-Historie",
|
||||
"retention_sessions_value": "90 Tage nach Ablauf",
|
||||
"retention_credit": "Credit-Transaktionen",
|
||||
"retention_credit_value": "10 Jahre (gesetzlich)",
|
||||
"retention_security": "Security-Logs",
|
||||
"retention_security_value": "1 Jahr",
|
||||
"retention_project": "Projektdaten (Chat, Todo, …)",
|
||||
"retention_project_value": "Bis zur Löschung",
|
||||
"danger_title": "Gefahrenzone",
|
||||
"danger_description": "Unwiderrufliche Aktionen",
|
||||
"danger_delete_title": "Alle meine Daten löschen",
|
||||
"danger_delete_desc": "Löscht dein Konto und alle verbundenen Daten dauerhaft aus allen Projekten. Kann nicht rückgängig gemacht werden.",
|
||||
"danger_delete_btn": "Daten löschen"
|
||||
},
|
||||
"tag_presets": {
|
||||
"title": "Tag-Presets",
|
||||
"hint": "Gespeicherte Tag-Sets, die du beim Anlegen eines neuen Space als Start-Vorlage wählen kannst. Änderungen am Preset berühren bereits erstellte Spaces nicht — es ist eine Einweg-Kopie.",
|
||||
"name_required": "Bitte einen Namen eingeben",
|
||||
"no_active_space": "Kein aktiver Space",
|
||||
"name_placeholder": "Preset-Name (z.B. „Mein Standard-Set\")",
|
||||
"create_from": "Aus „{name}\" erstellen",
|
||||
"creating": "Erstelle …",
|
||||
"loading_space": "Lade Space …",
|
||||
"delete_confirm": "Preset „{name}\" löschen?",
|
||||
"empty": "Noch keine Presets. Lege das erste an, indem du dem aktuellen Tag-Set einen Namen gibst — es wird dann automatisch als Default für neue Spaces verwendet.",
|
||||
"badge_default": "Default",
|
||||
"tag_count_one": "{count} Tag",
|
||||
"tag_count_other": "{count} Tags",
|
||||
"with_groups": "mit Gruppen",
|
||||
"aria_set_default": "Als Default setzen",
|
||||
"aria_delete": "Löschen"
|
||||
},
|
||||
"vault": {
|
||||
"title": "Verschlüsselung",
|
||||
"description": "Sensitive Felder werden mit AES-GCM-256 verschlüsselt, bevor sie in die lokale Datenbank geschrieben werden.",
|
||||
"badge_encrypted": "Verschlüsselt",
|
||||
"badge_locked": "Gesperrt",
|
||||
"badge_recovery_required": "Recovery-Code erforderlich",
|
||||
"badge_auth_required": "Anmeldung erforderlich",
|
||||
"badge_network_error": "Netzwerkfehler",
|
||||
"badge_server_error": "Server-Fehler",
|
||||
"badge_unknown_error": "Unbekannter Fehler",
|
||||
"status_unlocked_pre": "Dein persönlicher Schlüssel ist auf diesem Gerät geladen.",
|
||||
"status_unlocked_fields": "{count} Felder",
|
||||
"status_unlocked_tables": "{count} Tabellen",
|
||||
"status_unlocked_post": "werden verschlüsselt gespeichert.",
|
||||
"status_locked": "Dein Schlüssel ist nicht geladen. Verschlüsselte Inhalte können nicht gelesen werden, bis du dich erneut anmeldest oder den Schlüssel manuell lädst.",
|
||||
"status_load_now": "Schlüssel jetzt laden",
|
||||
"status_error": "Es gab ein Problem beim Laden deines Verschlüsselungsschlüssels. Bitte melde dich neu an oder prüfe deine Internetverbindung.",
|
||||
"status_retry": "Erneut versuchen",
|
||||
"toast_unlocked": "Verschlüsselungsschlüssel geladen",
|
||||
"toast_unlock_failed": "Schlüssel konnte nicht geladen werden: {status}",
|
||||
"toast_rotated": "Schlüssel rotiert. Neue Schreibvorgänge verwenden den neuen Schlüssel.",
|
||||
"toast_rotate_failed": "Rotation fehlgeschlagen: {status}",
|
||||
"toast_zk_enabled": "Zero-Knowledge-Modus aktiviert",
|
||||
"toast_zk_disabled": "Zero-Knowledge-Modus deaktiviert",
|
||||
"toast_recovery_cleared": "Recovery-Code entfernt",
|
||||
"toast_clipboard_ok": "Code in die Zwischenablage kopiert",
|
||||
"toast_clipboard_failed": "Konnte Code nicht kopieren",
|
||||
"err_code_mismatch": "Der eingegebene Code stimmt nicht mit dem angezeigten überein.",
|
||||
"threat_title": "Was Mana sehen kann",
|
||||
"threat_never_label": "Was Mana nie sieht:",
|
||||
"threat_never_body": "deine verschlüsselten Inhalte (Chat, Notizen, Träume, Memos, Kontaktdetails, Zyklus-Notizen, Transaktionsbeschreibungen, …). Sie verlassen dein Gerät nur als unleserlicher Blob.",
|
||||
"threat_could_label": "Was Mana technisch entschlüsseln könnte:",
|
||||
"threat_could_body": "deinen Master-Key, falls ein Mitarbeiter mit Zugriff auf den Schlüsselverschlüsselungsschlüssel aktiv darauf zugreift. In der Praxis ist das gegen alle realistischen Bedrohungen außer einer gerichtlich erzwungenen Offenlegung gegen Mana selbst geschützt.",
|
||||
"threat_visible_label": "Was strukturell sichtbar bleibt:",
|
||||
"threat_visible_body": "Anzahl deiner Notizen / Chats / Kontakte, Zeitstempel, Verbindungen zwischen Records. Die Inhalte selbst nicht.",
|
||||
"fields_title": "Verschlüsselte Felder",
|
||||
"fields_meta": "{fields} Felder · {tables} Tabellen",
|
||||
"fields_description": "Welche Spalten in welchen Tabellen verschlüsselt am Gerät liegen. Strukturelle Metadaten (IDs, Zeitstempel, Status-Flags) bleiben absichtlich im Klartext, damit Indizes, Sortierungen und Sync weiter funktionieren.",
|
||||
"show_more": "{count} weitere anzeigen",
|
||||
"show_less": "Weniger anzeigen",
|
||||
"zk_title": "Zero-Knowledge-Modus",
|
||||
"zk_state_active": "Aktiv",
|
||||
"zk_state_inactive": "Nicht aktiv",
|
||||
"zk_desc_optional": "Optional, fortgeschritten.",
|
||||
"zk_desc_body_pre": "Im Zero-Knowledge-Modus speichert Mana deinen Schlüssel ",
|
||||
"zk_desc_body_em": "nur noch in einer Form, die wir selbst nicht entschlüsseln können",
|
||||
"zk_desc_body_post": ". Du brauchst dann beim Login von einem neuen Gerät deinen Recovery-Code, um deine Daten freizuschalten.",
|
||||
"zk_pro_label": "Vorteil:",
|
||||
"zk_pro_body": "selbst ein Mana-Mitarbeiter mit Vollzugriff auf den Server kann deine Inhalte nicht mehr lesen.",
|
||||
"zk_con_label": "Risiko:",
|
||||
"zk_con_body": "wenn du den Recovery-Code verlierst, sind deine Daten unwiderruflich weg — wir haben dann keinen Backup-Schlüssel mehr.",
|
||||
"zk_existing_warn": "Du hast bereits einen Recovery-Code gespeichert, aber Zero-Knowledge ist noch nicht aktiv. Du kannst direkt aktivieren oder den Code zurücksetzen.",
|
||||
"zk_enable_now": "Zero-Knowledge jetzt aktivieren",
|
||||
"zk_enabling": "Aktiviere …",
|
||||
"zk_generate_new": "Neuen Recovery-Code generieren",
|
||||
"zk_clear": "Recovery-Code entfernen",
|
||||
"zk_clear_confirm": "Ja, entfernen",
|
||||
"zk_clearing": "Entferne …",
|
||||
"zk_setup": "Recovery-Code einrichten",
|
||||
"zk_generating": "Generiere …",
|
||||
"step_1_title": "Code sicher aufschreiben",
|
||||
"step_1_desc_pre": "Speichere diesen Code an einem sicheren Ort (Passwort-Manager, ausgedruckt im Tresor, …). ",
|
||||
"step_1_desc_strong": "Wir zeigen ihn dir nur ein einziges Mal.",
|
||||
"copy": "Kopieren",
|
||||
"step_1_continue": "Ich habe den Code gesichert →",
|
||||
"step_2_title": "Code zurück eintippen",
|
||||
"step_2_desc": "Tippe (oder paste) den Code, den du gerade gespeichert hast. So stellen wir sicher, dass der Backup wirklich vollständig ist.",
|
||||
"step_2_placeholder": "1A2B-3C4D-5E6F-...",
|
||||
"step_2_confirm": "Code bestätigen",
|
||||
"step_3_title": "Zero-Knowledge-Modus aktivieren",
|
||||
"step_3_desc_pre": "Wenn du jetzt aktivierst, löscht der Server seine Kopie deines Schlüssels. Ab sofort kannst du ",
|
||||
"step_3_desc_strong": "nur noch mit dem Recovery-Code",
|
||||
"step_3_desc_post": " auf deine verschlüsselten Daten zugreifen.",
|
||||
"step_3_warn": "Diese Aktion ist nicht rückgängig zu machen ohne den Recovery-Code. Wenn du deinen Code verlegst, sind deine Inhalte verloren.",
|
||||
"step_3_confirm": "Ja, Zero-Knowledge-Modus aktivieren",
|
||||
"cancel": "Abbrechen",
|
||||
"zk_active_note": "Der Server kann deine Daten ab sofort nicht mehr entschlüsseln. Beim nächsten Login auf einem neuen Gerät wirst du nach deinem Recovery-Code gefragt.",
|
||||
"rotate_step_title": "Neuer Recovery-Code",
|
||||
"rotate_step_strong": "Dein alter Code ist ab sofort ungültig.",
|
||||
"rotate_step_desc": " Speichere den neuen Code an einem sicheren Ort, bevor du diese Seite verlässt — wir zeigen ihn dir nur ein einziges Mal.",
|
||||
"rotate_step_save": "Ich habe den neuen Code gesichert",
|
||||
"rotate_btn": "Recovery-Code rotieren",
|
||||
"rotate_busy": "Rotiere …",
|
||||
"zk_disable_btn": "Zero-Knowledge-Modus deaktivieren …",
|
||||
"zk_disable_desc": "Damit wir den Server-Schlüssel wiederherstellen können, brauchen wir deinen aktuell geladenen Master-Key. Der ist gerade in deinem Browser — wir senden ihn einmal an den Server, der ihn dann mit dem KEK neu wrappt.",
|
||||
"zk_disable_confirm": "Ja, deaktivieren",
|
||||
"zk_disabling": "Deaktiviere …",
|
||||
"rotate_section_title": "Schlüssel rotieren",
|
||||
"rotate_section_warn_strong": "Vorsicht:",
|
||||
"rotate_section_warn_body_pre": " Beim Rotieren wird ein neuer Schlüssel generiert. Daten, die mit dem alten Schlüssel verschlüsselt wurden, sind danach nicht mehr lesbar — es sei denn, sie wurden vorher entschlüsselt und mit dem neuen Schlüssel neu geschrieben. Mana führt diesen Re-Encrypt-Schritt aktuell ",
|
||||
"rotate_section_warn_em": "nicht automatisch",
|
||||
"rotate_section_warn_post": " durch.",
|
||||
"rotate_section_when": "Wann verwenden? Wenn du den Verdacht hast, dass dein Gerät kompromittiert wurde, oder als regelmäßige Sicherheitspraxis nach einem Login von einem unbekannten Ort.",
|
||||
"rotate_section_btn": "Schlüssel rotieren …",
|
||||
"rotate_section_confirm": "Ja, jetzt rotieren",
|
||||
"rotate_section_busy": "Rotiere …"
|
||||
}
|
||||
}
|
||||
405
apps/mana/apps/web/src/lib/i18n/locales/settings/en.json
Normal file
405
apps/mana/apps/web/src/lib/i18n/locales/settings/en.json
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
{
|
||||
"categories": {
|
||||
"general": { "label": "General", "description": "Theme, language, notifications" },
|
||||
"ai": { "label": "AI", "description": "Compute backend & models" },
|
||||
"security": { "label": "Security", "description": "Passkeys, 2FA, encryption & sessions" },
|
||||
"privacy": {
|
||||
"label": "Privacy",
|
||||
"description": "What is currently public or shared via link — with kill switch."
|
||||
},
|
||||
"data": { "label": "Data & Sync", "description": "Cloud sync, export, backup & GDPR" },
|
||||
"tag_presets": { "label": "Tag presets", "description": "Tag sets for new spaces" }
|
||||
},
|
||||
"sidebar": {
|
||||
"aria_categories": "Settings categories",
|
||||
"search_placeholder": "Search settings…",
|
||||
"aria_search": "Search settings",
|
||||
"aria_clear": "Clear search",
|
||||
"no_results": "No matches for \"{query}\""
|
||||
},
|
||||
"search": {
|
||||
"theme": { "label": "Theme", "keywords": "dark, light, color, design" },
|
||||
"language": { "label": "Language", "keywords": "language, i18n, german, english" },
|
||||
"notifications": { "label": "Notifications", "keywords": "notification, sound" },
|
||||
"ai_options": { "label": "AI options", "keywords": "llm, ai, compute" },
|
||||
"browser_model": {
|
||||
"label": "Browser model (Gemma)",
|
||||
"keywords": "gemma, webgpu, local, offline"
|
||||
},
|
||||
"mana_server": { "label": "Mana server (AI)", "keywords": "server, self-hosted" },
|
||||
"cloud_ai": { "label": "Cloud AI (Gemini)", "keywords": "google, cloud, gemini" },
|
||||
"passkeys": { "label": "Passkeys", "keywords": "webauthn, fido, biometric" },
|
||||
"sessions": { "label": "Active sessions", "keywords": "logout, device" },
|
||||
"two_factor": { "label": "Two-factor (2FA)", "keywords": "totp, 2fa, mfa" },
|
||||
"vault": { "label": "Encryption", "keywords": "vault, encryption, aes, key, zero-knowledge" },
|
||||
"security_log": { "label": "Security log", "keywords": "audit, history" },
|
||||
"privacy_overview": {
|
||||
"label": "Privacy overview",
|
||||
"keywords": "public, unlisted, share, sharing, link"
|
||||
},
|
||||
"reset_all_private": {
|
||||
"label": "Reset all to private",
|
||||
"keywords": "kill-switch, kill switch, reset, private, revoke"
|
||||
},
|
||||
"cloud_sync": { "label": "Cloud Sync", "keywords": "sync, devices" },
|
||||
"data_export": { "label": "Export data", "keywords": "export, dsgvo, gdpr, json" },
|
||||
"auth_data": { "label": "Authentication", "keywords": "sessions, 2fa, login" },
|
||||
"credits_data": { "label": "Credits & transactions", "keywords": "balance, transactions" },
|
||||
"project_data": { "label": "Project data", "keywords": "projects, apps, statistics" },
|
||||
"retention": { "label": "Retention periods", "keywords": "retention, gdpr, periods" },
|
||||
"backup": {
|
||||
"label": "Backup & restore",
|
||||
"keywords": "backup, restore, import, archive, .mana"
|
||||
},
|
||||
"delete_account": { "label": "Delete account", "keywords": "delete, gdpr, danger zone" }
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"description": "Language, week start & sounds",
|
||||
"language_title": "Display language",
|
||||
"language_description": "Language of the user interface",
|
||||
"color_scheme_title": "Color scheme",
|
||||
"color_scheme_description": "Accent color of the interface",
|
||||
"color_scheme_ocean": "Ocean",
|
||||
"color_scheme_nature": "Nature",
|
||||
"color_scheme_lume": "Lume",
|
||||
"color_scheme_stone": "Stone",
|
||||
"color_mode_title": "Color mode",
|
||||
"color_mode_description": "Light, dark or automatic",
|
||||
"color_mode_light": "Light",
|
||||
"color_mode_dark": "Dark",
|
||||
"color_mode_system": "System",
|
||||
"week_start_title": "Week start",
|
||||
"week_start_description": "First day of the week in calendars",
|
||||
"week_start_monday": "Monday",
|
||||
"week_start_sunday": "Sunday",
|
||||
"sounds_title": "Sounds",
|
||||
"sounds_description": "Sound effects across all apps",
|
||||
"sounds_aria": "Toggle sounds on or off",
|
||||
"onboarding_title": "Run onboarding again",
|
||||
"onboarding_description": "Pick name, look and modules again",
|
||||
"onboarding_start": "Start",
|
||||
"onboarding_starting": "Starting…"
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI options",
|
||||
"description": "Choose which AI layers Mana is allowed to use — from none to all",
|
||||
"tier_local_title": "Local (no AI)",
|
||||
"tier_local_badge": "always on",
|
||||
"tier_local_subtitle": "Basic functions without any AI",
|
||||
"tier_local_description": "Mana also works completely without AI: date detection, search and simple classification run via classic algorithms. Some functions are limited then, but everything is 100% offline and free.",
|
||||
"tier_browser_title": "On your device",
|
||||
"tier_browser_subtitle": "Gemma 4 E2B (Google) in the browser",
|
||||
"tier_browser_bullet_1": "100% local — data never leaves your device",
|
||||
"tier_browser_bullet_2": "~500 MB one-time download",
|
||||
"tier_browser_bullet_3": "Needs WebGPU + 2 GB free GPU memory",
|
||||
"tier_browser_blocked": "WebGPU not available in your browser. Works in Chrome/Edge 113+ or Safari 18+.",
|
||||
"tier_browser_loaded": "✓ Model loaded",
|
||||
"tier_browser_loading": "Loading {name} ({percent}%)…",
|
||||
"tier_browser_load_btn": "Load model (~{size} MB)",
|
||||
"tier_browser_load_btn_loading": "Loading…",
|
||||
"tier_mana_server_title": "Mana server",
|
||||
"tier_mana_server_subtitle": "Self-hosted on our Mac Mini",
|
||||
"tier_mana_server_bullet_1": "Faster and stronger than browser LLM",
|
||||
"tier_mana_server_bullet_2": "Data flows encrypted to our server",
|
||||
"tier_mana_server_bullet_3": "No content is stored",
|
||||
"tier_byok_title": "Your own API key",
|
||||
"tier_byok_subtitle": "OpenAI, Anthropic, Google Gemini or Mistral",
|
||||
"tier_byok_bullet_1": "Direct from the browser — no Mana server in between",
|
||||
"tier_byok_bullet_2": "You pay the provider, we see nothing",
|
||||
"tier_byok_bullet_3": "Keys are stored encrypted in your vault",
|
||||
"tier_cloud_title": "Google Gemini",
|
||||
"tier_cloud_subtitle": "Cloud API via our server proxy",
|
||||
"tier_cloud_bullet_1": "Best quality for complex tasks",
|
||||
"tier_cloud_bullet_2": "Fastest answers",
|
||||
"tier_cloud_bullet_3": "Data is processed by Google",
|
||||
"tier_cloud_warning": "Only recommended for non-sensitive content",
|
||||
"tier_cloud_consent_title": "Confirmation required",
|
||||
"tier_cloud_consent_desc": "Cloud requests send your content to Google. Please confirm that you understand and accept this.",
|
||||
"tier_cloud_consent_btn": "Understood, enable cloud",
|
||||
"tier_cloud_consent_ok": "✓ Cloud consent granted",
|
||||
"tier_cloud_consent_revoke": "Revoke",
|
||||
"badge_active": "active",
|
||||
"summary_label": "Current order:",
|
||||
"summary_local_only": "Only local (no AI) — most AI features are limited.",
|
||||
"summary_chain_local_fallback": "Local (fallback)",
|
||||
"fallback_title": "Fall back to \"Local\" on error",
|
||||
"fallback_description": "If the chosen AI layer can't answer a request, Mana will try the local variant (if available). Off: shows an error instead.",
|
||||
"fallback_aria": "Fallback to local",
|
||||
"show_source_title": "Show source on every AI result",
|
||||
"show_source_description": "Shows a small marker under every AI-generated answer like \"On your device\" or \"via Google Gemini\" — so you always see where your data was processed.",
|
||||
"show_source_aria": "Show source"
|
||||
},
|
||||
"byok": {
|
||||
"vault_locked": "Vault is locked — please sign in first to manage keys.",
|
||||
"loading": "Loading…",
|
||||
"add_first": "Add first API key",
|
||||
"add_more": "Add another key",
|
||||
"field_provider": "Provider",
|
||||
"field_label": "Label",
|
||||
"field_label_placeholder": "e.g. Personal OpenAI",
|
||||
"field_api_key": "API key",
|
||||
"field_api_key_placeholder_generic": "API key",
|
||||
"field_model": "Model",
|
||||
"field_model_default": "Default ({model})",
|
||||
"field_default_short": "Default",
|
||||
"field_as_default": "As default",
|
||||
"label_required": "Label and API key are required",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
"confirm_delete": "Really delete key?",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"set_default": "Default",
|
||||
"badge_default": "Default",
|
||||
"meta_line": "{provider} · {model} · {count} calls · {cost}"
|
||||
},
|
||||
"security": {
|
||||
"title": "Security",
|
||||
"description": "Passkeys, 2FA, encryption & sessions"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy overview",
|
||||
"description": "All entries you currently show publicly or share via link — with one-click rollback.",
|
||||
"summary_public": "public",
|
||||
"summary_unlisted": "shareable via link",
|
||||
"loading": "Loading…",
|
||||
"empty": "Nothing is currently public or shared via link — well done.",
|
||||
"badge_public": "Public",
|
||||
"badge_unlisted": "Via link",
|
||||
"open": "Open",
|
||||
"set_private": "Private",
|
||||
"kill_all": "Reset all to private",
|
||||
"confirm_one": "{count} entry will be reset to \"Space\". Active share links will be revoked. Continue?",
|
||||
"confirm_other": "{count} entries will be reset to \"Space\". Active share links will be revoked. Continue?",
|
||||
"cancel": "Cancel",
|
||||
"killing": "Resetting…",
|
||||
"confirm_kill": "Yes, reset all",
|
||||
"toast_set_private": "\"{title}\" is now private",
|
||||
"toast_set_private_failed": "Could not downgrade \"{title}\"",
|
||||
"toast_kill_partial": "{flipped} entries set to private — {failed} failed",
|
||||
"toast_kill_done": "{flipped} entries reset to private",
|
||||
"toast_kill_failed": "Kill switch failed"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Cloud Sync",
|
||||
"description": "Sync your data across all devices",
|
||||
"interval_monthly": "Monthly",
|
||||
"interval_quarterly": "Quarterly",
|
||||
"interval_yearly": "Yearly",
|
||||
"interval_short_monthly": "Month",
|
||||
"interval_short_quarterly": "Quarter",
|
||||
"interval_short_yearly": "Year",
|
||||
"interval_hint_monthly": "billed every month · ~1 credit/day",
|
||||
"interval_hint_quarterly": "billed every 3 months",
|
||||
"interval_hint_yearly": "billed once per year",
|
||||
"toast_activated": "Cloud Sync activated!",
|
||||
"toast_deactivated": "Cloud Sync deactivated",
|
||||
"toast_interval_changed": "Interval changed to {label}",
|
||||
"deactivate_confirm": "Really deactivate Cloud Sync? Your data stays available locally.",
|
||||
"err_activation_failed": "Activation failed",
|
||||
"err_deactivation_failed": "Deactivation failed",
|
||||
"err_change_failed": "Change failed",
|
||||
"status_label": "Status",
|
||||
"status_next_charge": "Next charge on {date}",
|
||||
"status_encrypted": "Synced encrypted across all devices",
|
||||
"status_insufficient": "Not enough credits — top up to continue",
|
||||
"status_local_only": "Your data is only stored locally on this device",
|
||||
"badge_active": "Active",
|
||||
"badge_paused": "Paused",
|
||||
"badge_inactive": "Inactive",
|
||||
"credits_available": "Available credits",
|
||||
"credits_topup": "Top up credits",
|
||||
"interval_label": "Billing interval",
|
||||
"interval_price_hint": "{price} credits · {hint}",
|
||||
"changing": "Changing…",
|
||||
"change_to": "Switch to {label}",
|
||||
"change_hint": "Change applies from the next charge",
|
||||
"deactivating": "Deactivating…",
|
||||
"deactivate": "Deactivate Cloud Sync",
|
||||
"activating": "Activating…",
|
||||
"activate": "Activate · {price} credits",
|
||||
"insufficient_warn_pre": "Not enough credits.",
|
||||
"insufficient_topup": "Top up",
|
||||
"footnote": "Cloud Sync syncs your data encrypted across all devices. Local data always stays — even when sync is paused or deactivated."
|
||||
},
|
||||
"mydata": {
|
||||
"title": "My data (GDPR)",
|
||||
"description": "Overview of all the data we store about you",
|
||||
"qr_export": "Export as QR code",
|
||||
"qr_short": "QR",
|
||||
"export": "Export",
|
||||
"exporting": "Exporting…",
|
||||
"retry": "Try again",
|
||||
"no_name": "No name",
|
||||
"verified": "verified",
|
||||
"not_verified": "not verified",
|
||||
"registered_at": "Registered on",
|
||||
"total_entities": "Total entities",
|
||||
"total_entities_desc": "Records across all apps",
|
||||
"projects_with_data": "Projects with data",
|
||||
"footnote_pre": "No tracking cookies — anonymous analytics via Umami. Details in the ",
|
||||
"footnote_link": "privacy policy",
|
||||
"footnote_post": ".",
|
||||
"auth_title": "Authentication",
|
||||
"auth_description": "Sessions, accounts & 2FA",
|
||||
"auth_sessions": "Active sessions",
|
||||
"auth_accounts": "Linked accounts",
|
||||
"auth_two_fa": "Two-factor (2FA)",
|
||||
"auth_two_fa_active": "Enabled",
|
||||
"auth_two_fa_inactive": "Disabled",
|
||||
"auth_last_login": "Last login",
|
||||
"credits_title": "Credits",
|
||||
"credits_description": "Balance & transactions",
|
||||
"credits_balance": "Current balance",
|
||||
"credits_total_earned": "Total earned",
|
||||
"credits_total_spent": "Total spent",
|
||||
"credits_transactions": "Transactions",
|
||||
"project_title": "Project data",
|
||||
"project_description": "Records per app",
|
||||
"col_app": "App",
|
||||
"col_count": "Entries",
|
||||
"col_time": "Last activity",
|
||||
"project_status_active": "Active",
|
||||
"project_status_empty": "No data",
|
||||
"project_status_unavailable": "Unavailable",
|
||||
"project_unreachable": "unreachable",
|
||||
"relative_just_now": "just now",
|
||||
"relative_minutes_ago": "{n} min ago",
|
||||
"relative_hours_ago": "{n} hr ago",
|
||||
"relative_days_ago": "{n} d ago",
|
||||
"retention_title": "Retention periods",
|
||||
"retention_description": "How long we keep your data",
|
||||
"retention_account": "User account & profile",
|
||||
"retention_account_value": "Until deletion",
|
||||
"retention_sessions": "Sessions & login history",
|
||||
"retention_sessions_value": "90 days after expiry",
|
||||
"retention_credit": "Credit transactions",
|
||||
"retention_credit_value": "10 years (legal)",
|
||||
"retention_security": "Security logs",
|
||||
"retention_security_value": "1 year",
|
||||
"retention_project": "Project data (chat, todo, …)",
|
||||
"retention_project_value": "Until deletion",
|
||||
"danger_title": "Danger zone",
|
||||
"danger_description": "Irreversible actions",
|
||||
"danger_delete_title": "Delete all my data",
|
||||
"danger_delete_desc": "Permanently deletes your account and all related data across all projects. Cannot be undone.",
|
||||
"danger_delete_btn": "Delete data"
|
||||
},
|
||||
"tag_presets": {
|
||||
"title": "Tag presets",
|
||||
"hint": "Saved tag sets you can pick as a starter template when creating a new space. Changes to a preset don't affect already-created spaces — it's a one-way copy.",
|
||||
"name_required": "Please enter a name",
|
||||
"no_active_space": "No active space",
|
||||
"name_placeholder": "Preset name (e.g. \"My standard set\")",
|
||||
"create_from": "Create from \"{name}\"",
|
||||
"creating": "Creating …",
|
||||
"loading_space": "Loading space …",
|
||||
"delete_confirm": "Delete preset \"{name}\"?",
|
||||
"empty": "No presets yet. Create the first one by giving the current tag set a name — it will then be used automatically as the default for new spaces.",
|
||||
"badge_default": "Default",
|
||||
"tag_count_one": "{count} tag",
|
||||
"tag_count_other": "{count} tags",
|
||||
"with_groups": "with groups",
|
||||
"aria_set_default": "Set as default",
|
||||
"aria_delete": "Delete"
|
||||
},
|
||||
"vault": {
|
||||
"title": "Encryption",
|
||||
"description": "Sensitive fields are encrypted with AES-GCM-256 before being written to the local database.",
|
||||
"badge_encrypted": "Encrypted",
|
||||
"badge_locked": "Locked",
|
||||
"badge_recovery_required": "Recovery code required",
|
||||
"badge_auth_required": "Sign-in required",
|
||||
"badge_network_error": "Network error",
|
||||
"badge_server_error": "Server error",
|
||||
"badge_unknown_error": "Unknown error",
|
||||
"status_unlocked_pre": "Your personal key is loaded on this device.",
|
||||
"status_unlocked_fields": "{count} fields",
|
||||
"status_unlocked_tables": "{count} tables",
|
||||
"status_unlocked_post": "are stored encrypted.",
|
||||
"status_locked": "Your key isn't loaded. Encrypted content can't be read until you sign in again or load the key manually.",
|
||||
"status_load_now": "Load key now",
|
||||
"status_error": "There was a problem loading your encryption key. Please sign in again or check your internet connection.",
|
||||
"status_retry": "Try again",
|
||||
"toast_unlocked": "Encryption key loaded",
|
||||
"toast_unlock_failed": "Key could not be loaded: {status}",
|
||||
"toast_rotated": "Key rotated. New writes will use the new key.",
|
||||
"toast_rotate_failed": "Rotation failed: {status}",
|
||||
"toast_zk_enabled": "Zero-knowledge mode enabled",
|
||||
"toast_zk_disabled": "Zero-knowledge mode disabled",
|
||||
"toast_recovery_cleared": "Recovery code removed",
|
||||
"toast_clipboard_ok": "Code copied to clipboard",
|
||||
"toast_clipboard_failed": "Could not copy code",
|
||||
"err_code_mismatch": "The entered code doesn't match the one shown.",
|
||||
"threat_title": "What Mana can see",
|
||||
"threat_never_label": "What Mana never sees:",
|
||||
"threat_never_body": "your encrypted content (chat, notes, dreams, memos, contact details, cycle notes, transaction descriptions, …). It only leaves your device as an unreadable blob.",
|
||||
"threat_could_label": "What Mana could technically decrypt:",
|
||||
"threat_could_body": "your master key, if an employee with access to the key-encryption key actively reaches for it. In practice this is protected against all realistic threats except a court-ordered disclosure against Mana itself.",
|
||||
"threat_visible_label": "What stays structurally visible:",
|
||||
"threat_visible_body": "the count of your notes / chats / contacts, timestamps, links between records. Not the content itself.",
|
||||
"fields_title": "Encrypted fields",
|
||||
"fields_meta": "{fields} fields · {tables} tables",
|
||||
"fields_description": "Which columns in which tables are stored encrypted on the device. Structural metadata (IDs, timestamps, status flags) intentionally stays in plaintext so indexes, sorting and sync keep working.",
|
||||
"show_more": "Show {count} more",
|
||||
"show_less": "Show less",
|
||||
"zk_title": "Zero-knowledge mode",
|
||||
"zk_state_active": "Active",
|
||||
"zk_state_inactive": "Not active",
|
||||
"zk_desc_optional": "Optional, advanced.",
|
||||
"zk_desc_body_pre": " In zero-knowledge mode Mana stores your key ",
|
||||
"zk_desc_body_em": "only in a form we ourselves can't decrypt",
|
||||
"zk_desc_body_post": ". When signing in from a new device you'll need your recovery code to unlock your data.",
|
||||
"zk_pro_label": "Pro:",
|
||||
"zk_pro_body": " even a Mana employee with full server access can no longer read your content.",
|
||||
"zk_con_label": "Risk:",
|
||||
"zk_con_body": " if you lose the recovery code, your data is gone for good — we no longer have a backup key.",
|
||||
"zk_existing_warn": "You already have a recovery code stored, but zero-knowledge mode isn't active yet. You can enable it directly or reset the code.",
|
||||
"zk_enable_now": "Enable zero-knowledge now",
|
||||
"zk_enabling": "Enabling …",
|
||||
"zk_generate_new": "Generate new recovery code",
|
||||
"zk_clear": "Remove recovery code",
|
||||
"zk_clear_confirm": "Yes, remove",
|
||||
"zk_clearing": "Removing …",
|
||||
"zk_setup": "Set up recovery code",
|
||||
"zk_generating": "Generating …",
|
||||
"step_1_title": "Save the code safely",
|
||||
"step_1_desc_pre": "Store this code in a safe place (password manager, printed in a vault, …). ",
|
||||
"step_1_desc_strong": "We only show it once.",
|
||||
"copy": "Copy",
|
||||
"step_1_continue": "I saved the code →",
|
||||
"step_2_title": "Type the code back",
|
||||
"step_2_desc": "Type (or paste) the code you just saved. This way we make sure the backup is really complete.",
|
||||
"step_2_placeholder": "1A2B-3C4D-5E6F-...",
|
||||
"step_2_confirm": "Confirm code",
|
||||
"step_3_title": "Enable zero-knowledge mode",
|
||||
"step_3_desc_pre": "If you enable now, the server deletes its copy of your key. From this point on you can ",
|
||||
"step_3_desc_strong": "only access your encrypted data with the recovery code",
|
||||
"step_3_desc_post": ".",
|
||||
"step_3_warn": "This action cannot be undone without the recovery code. If you misplace your code, your content is lost.",
|
||||
"step_3_confirm": "Yes, enable zero-knowledge mode",
|
||||
"cancel": "Cancel",
|
||||
"zk_active_note": "The server can no longer decrypt your data. The next time you sign in on a new device, you'll be asked for your recovery code.",
|
||||
"rotate_step_title": "New recovery code",
|
||||
"rotate_step_strong": "Your old code is no longer valid.",
|
||||
"rotate_step_desc": " Save the new code in a safe place before leaving this page — we only show it once.",
|
||||
"rotate_step_save": "I saved the new code",
|
||||
"rotate_btn": "Rotate recovery code",
|
||||
"rotate_busy": "Rotating …",
|
||||
"zk_disable_btn": "Disable zero-knowledge mode …",
|
||||
"zk_disable_desc": "To restore the server-side key, we need your currently loaded master key. It's currently in your browser — we'll send it to the server once, which will re-wrap it with the KEK.",
|
||||
"zk_disable_confirm": "Yes, disable",
|
||||
"zk_disabling": "Disabling …",
|
||||
"rotate_section_title": "Rotate key",
|
||||
"rotate_section_warn_strong": "Caution:",
|
||||
"rotate_section_warn_body_pre": " When rotating, a new key is generated. Data encrypted with the old key won't be readable afterwards — unless it was decrypted and re-written with the new key. Mana currently does ",
|
||||
"rotate_section_warn_em": "not perform this re-encrypt step automatically",
|
||||
"rotate_section_warn_post": ".",
|
||||
"rotate_section_when": "When to use? If you suspect your device was compromised, or as a regular security practice after signing in from an unknown location.",
|
||||
"rotate_section_btn": "Rotate key …",
|
||||
"rotate_section_confirm": "Yes, rotate now",
|
||||
"rotate_section_busy": "Rotating …"
|
||||
}
|
||||
}
|
||||
414
apps/mana/apps/web/src/lib/i18n/locales/settings/es.json
Normal file
414
apps/mana/apps/web/src/lib/i18n/locales/settings/es.json
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
{
|
||||
"categories": {
|
||||
"general": { "label": "General", "description": "Tema, idioma, notificaciones" },
|
||||
"ai": { "label": "IA", "description": "Backend de cómputo y modelos" },
|
||||
"security": { "label": "Seguridad", "description": "Passkeys, 2FA, cifrado y sesiones" },
|
||||
"privacy": {
|
||||
"label": "Privacidad",
|
||||
"description": "Qué es público o compartido por enlace ahora mismo — con interruptor de emergencia."
|
||||
},
|
||||
"data": {
|
||||
"label": "Datos y sincronización",
|
||||
"description": "Cloud sync, exportación, copia de seguridad y RGPD"
|
||||
},
|
||||
"tag_presets": {
|
||||
"label": "Plantillas de etiquetas",
|
||||
"description": "Conjuntos de etiquetas para nuevos espacios"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"aria_categories": "Categorías de ajustes",
|
||||
"search_placeholder": "Buscar ajustes…",
|
||||
"aria_search": "Buscar ajustes",
|
||||
"aria_clear": "Limpiar búsqueda",
|
||||
"no_results": "Sin resultados para «{query}»"
|
||||
},
|
||||
"search": {
|
||||
"theme": { "label": "Tema", "keywords": "oscuro, claro, color, diseño" },
|
||||
"language": { "label": "Idioma", "keywords": "language, i18n, alemán, inglés" },
|
||||
"notifications": { "label": "Notificaciones", "keywords": "notification, sonido" },
|
||||
"ai_options": { "label": "Opciones de IA", "keywords": "llm, ia, ai, compute" },
|
||||
"browser_model": {
|
||||
"label": "Modelo del navegador (Gemma)",
|
||||
"keywords": "gemma, webgpu, local, offline"
|
||||
},
|
||||
"mana_server": { "label": "Servidor Mana (IA)", "keywords": "servidor, self-hosted" },
|
||||
"cloud_ai": { "label": "IA en la nube (Gemini)", "keywords": "google, cloud, gemini" },
|
||||
"passkeys": { "label": "Passkeys", "keywords": "webauthn, fido, biometría" },
|
||||
"sessions": { "label": "Sesiones activas", "keywords": "logout, dispositivo" },
|
||||
"two_factor": { "label": "Doble factor (2FA)", "keywords": "totp, 2fa, mfa" },
|
||||
"vault": { "label": "Cifrado", "keywords": "vault, cifrado, aes, clave, zero-knowledge" },
|
||||
"security_log": { "label": "Registro de seguridad", "keywords": "audit, historial" },
|
||||
"privacy_overview": {
|
||||
"label": "Resumen de privacidad",
|
||||
"keywords": "público, unlisted, compartir, enlace"
|
||||
},
|
||||
"reset_all_private": {
|
||||
"label": "Restablecer todo a privado",
|
||||
"keywords": "kill-switch, reset, privado, revocar"
|
||||
},
|
||||
"cloud_sync": { "label": "Cloud Sync", "keywords": "sync, dispositivos" },
|
||||
"data_export": { "label": "Exportar datos", "keywords": "export, rgpd, gdpr, json" },
|
||||
"auth_data": { "label": "Autenticación", "keywords": "sesiones, 2fa, login" },
|
||||
"credits_data": { "label": "Créditos y transacciones", "keywords": "saldo, transacciones" },
|
||||
"project_data": { "label": "Datos de proyectos", "keywords": "proyectos, apps, estadística" },
|
||||
"retention": { "label": "Plazos de conservación", "keywords": "retention, rgpd, plazos" },
|
||||
"backup": {
|
||||
"label": "Copia y restauración",
|
||||
"keywords": "backup, restore, import, archivo, .mana"
|
||||
},
|
||||
"delete_account": {
|
||||
"label": "Eliminar cuenta",
|
||||
"keywords": "delete, gdpr, rgpd, zona de peligro"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"description": "Idioma, inicio de semana y sonidos",
|
||||
"language_title": "Idioma de la interfaz",
|
||||
"language_description": "Idioma de la interfaz de usuario",
|
||||
"color_scheme_title": "Esquema de color",
|
||||
"color_scheme_description": "Color de acento de la interfaz",
|
||||
"color_scheme_ocean": "Océano",
|
||||
"color_scheme_nature": "Naturaleza",
|
||||
"color_scheme_lume": "Lume",
|
||||
"color_scheme_stone": "Piedra",
|
||||
"color_mode_title": "Modo de color",
|
||||
"color_mode_description": "Claro, oscuro o automático",
|
||||
"color_mode_light": "Claro",
|
||||
"color_mode_dark": "Oscuro",
|
||||
"color_mode_system": "Sistema",
|
||||
"week_start_title": "Inicio de semana",
|
||||
"week_start_description": "Primer día de la semana en los calendarios",
|
||||
"week_start_monday": "Lunes",
|
||||
"week_start_sunday": "Domingo",
|
||||
"sounds_title": "Sonidos",
|
||||
"sounds_description": "Efectos de sonido en todas las apps",
|
||||
"sounds_aria": "Activar o desactivar sonidos",
|
||||
"onboarding_title": "Repetir el onboarding",
|
||||
"onboarding_description": "Volver a elegir nombre, apariencia y módulos",
|
||||
"onboarding_start": "Empezar",
|
||||
"onboarding_starting": "Iniciando…"
|
||||
},
|
||||
"ai": {
|
||||
"title": "Opciones de IA",
|
||||
"description": "Elige qué capas de IA puede usar Mana — desde ninguna hasta todas",
|
||||
"tier_local_title": "Local (sin IA)",
|
||||
"tier_local_badge": "siempre activo",
|
||||
"tier_local_subtitle": "Funciones básicas sin ninguna IA",
|
||||
"tier_local_description": "Mana también funciona sin IA: detección de fechas, búsqueda y clasificación simple usan algoritmos clásicos. Algunas funciones quedan limitadas, pero todo es 100 % offline y gratuito.",
|
||||
"tier_browser_title": "En tu dispositivo",
|
||||
"tier_browser_subtitle": "Gemma 4 E2B (Google) en el navegador",
|
||||
"tier_browser_bullet_1": "100 % local — los datos no salen de tu dispositivo",
|
||||
"tier_browser_bullet_2": "~500 MB de descarga única",
|
||||
"tier_browser_bullet_3": "Necesita WebGPU + 2 GB de memoria GPU libre",
|
||||
"tier_browser_blocked": "WebGPU no está disponible en tu navegador. Funciona en Chrome/Edge 113+ o Safari 18+.",
|
||||
"tier_browser_loaded": "✓ Modelo cargado",
|
||||
"tier_browser_loading": "Cargando {name} ({percent} %)…",
|
||||
"tier_browser_load_btn": "Cargar modelo (~{size} MB)",
|
||||
"tier_browser_load_btn_loading": "Cargando…",
|
||||
"tier_mana_server_title": "Servidor Mana",
|
||||
"tier_mana_server_subtitle": "Auto-alojado en nuestro Mac Mini",
|
||||
"tier_mana_server_bullet_1": "Más rápido y potente que el LLM del navegador",
|
||||
"tier_mana_server_bullet_2": "Los datos viajan cifrados a nuestro servidor",
|
||||
"tier_mana_server_bullet_3": "No se almacena ningún contenido",
|
||||
"tier_byok_title": "Tu propia API key",
|
||||
"tier_byok_subtitle": "OpenAI, Anthropic, Google Gemini o Mistral",
|
||||
"tier_byok_bullet_1": "Directo desde el navegador — sin escala en el servidor de Mana",
|
||||
"tier_byok_bullet_2": "Pagas al proveedor, nosotros no vemos nada",
|
||||
"tier_byok_bullet_3": "Las claves se guardan cifradas en tu vault",
|
||||
"tier_cloud_title": "Google Gemini",
|
||||
"tier_cloud_subtitle": "API en la nube vía nuestro proxy",
|
||||
"tier_cloud_bullet_1": "Mejor calidad para tareas complejas",
|
||||
"tier_cloud_bullet_2": "Las respuestas más rápidas",
|
||||
"tier_cloud_bullet_3": "Los datos los procesa Google",
|
||||
"tier_cloud_warning": "Solo recomendado para contenido no sensible",
|
||||
"tier_cloud_consent_title": "Confirmación necesaria",
|
||||
"tier_cloud_consent_desc": "Las solicitudes a la nube envían tu contenido a Google. Confirma que lo entiendes y aceptas.",
|
||||
"tier_cloud_consent_btn": "Entendido, activar nube",
|
||||
"tier_cloud_consent_ok": "✓ Consentimiento de nube concedido",
|
||||
"tier_cloud_consent_revoke": "Retirar",
|
||||
"badge_active": "activo",
|
||||
"summary_label": "Orden actual:",
|
||||
"summary_local_only": "Solo local (sin IA) — la mayoría de funciones de IA están limitadas.",
|
||||
"summary_chain_local_fallback": "Local (alternativo)",
|
||||
"fallback_title": "Recurrir a «Local» en caso de error",
|
||||
"fallback_description": "Si la capa de IA elegida no puede responder, Mana intenta la variante local (si está disponible). Apagado: muestra un error en su lugar.",
|
||||
"fallback_aria": "Recurrir a local",
|
||||
"show_source_title": "Mostrar la fuente en cada resultado de IA",
|
||||
"show_source_description": "Muestra una marca pequeña bajo cada respuesta generada por IA, como «En tu dispositivo» o «vía Google Gemini» — para que veas dónde se procesaron tus datos.",
|
||||
"show_source_aria": "Mostrar fuente"
|
||||
},
|
||||
"byok": {
|
||||
"vault_locked": "El vault está bloqueado — inicia sesión primero para gestionar las claves.",
|
||||
"loading": "Cargando…",
|
||||
"add_first": "Añadir primera API key",
|
||||
"add_more": "Añadir otra clave",
|
||||
"field_provider": "Proveedor",
|
||||
"field_label": "Etiqueta",
|
||||
"field_label_placeholder": "p. ej. OpenAI personal",
|
||||
"field_api_key": "API key",
|
||||
"field_api_key_placeholder_generic": "API key",
|
||||
"field_model": "Modelo",
|
||||
"field_model_default": "Por defecto ({model})",
|
||||
"field_default_short": "Por defecto",
|
||||
"field_as_default": "Como predeterminada",
|
||||
"label_required": "La etiqueta y la API key son obligatorias",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Guardar",
|
||||
"saving": "Guardando…",
|
||||
"confirm_delete": "¿Eliminar la clave de verdad?",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"set_default": "Predeterminada",
|
||||
"badge_default": "Predeterminada",
|
||||
"meta_line": "{provider} · {model} · {count} llamadas · {cost}"
|
||||
},
|
||||
"security": {
|
||||
"title": "Seguridad",
|
||||
"description": "Passkeys, 2FA, cifrado y sesiones"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Resumen de privacidad",
|
||||
"description": "Todas las entradas que ahora muestras públicamente o compartes por enlace — con vuelta atrás en un clic.",
|
||||
"summary_public": "público",
|
||||
"summary_unlisted": "compartible por enlace",
|
||||
"loading": "Cargando…",
|
||||
"empty": "Ahora mismo no hay nada público ni compartido por enlace — bien hecho.",
|
||||
"badge_public": "Público",
|
||||
"badge_unlisted": "Por enlace",
|
||||
"open": "Abrir",
|
||||
"set_private": "Privado",
|
||||
"kill_all": "Restablecer todo a privado",
|
||||
"confirm_one": "{count} entrada se restablecerá a «Espacio». Los enlaces de compartición activos se revocarán. ¿Continuar?",
|
||||
"confirm_other": "{count} entradas se restablecerán a «Espacio». Los enlaces de compartición activos se revocarán. ¿Continuar?",
|
||||
"cancel": "Cancelar",
|
||||
"killing": "Restableciendo…",
|
||||
"confirm_kill": "Sí, restablecer todo",
|
||||
"toast_set_private": "«{title}» ahora es privado",
|
||||
"toast_set_private_failed": "No se pudo bajar «{title}»",
|
||||
"toast_kill_partial": "{flipped} entradas a privado — {failed} fallaron",
|
||||
"toast_kill_done": "{flipped} entradas restablecidas a privado",
|
||||
"toast_kill_failed": "El interruptor de emergencia falló"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Cloud Sync",
|
||||
"description": "Sincroniza tus datos en todos tus dispositivos",
|
||||
"interval_monthly": "Mensual",
|
||||
"interval_quarterly": "Trimestral",
|
||||
"interval_yearly": "Anual",
|
||||
"interval_short_monthly": "Mes",
|
||||
"interval_short_quarterly": "Trimestre",
|
||||
"interval_short_yearly": "Año",
|
||||
"interval_hint_monthly": "facturado cada mes · ~1 crédito/día",
|
||||
"interval_hint_quarterly": "facturado cada 3 meses",
|
||||
"interval_hint_yearly": "facturado una vez al año",
|
||||
"toast_activated": "¡Cloud Sync activado!",
|
||||
"toast_deactivated": "Cloud Sync desactivado",
|
||||
"toast_interval_changed": "Intervalo cambiado a {label}",
|
||||
"deactivate_confirm": "¿Desactivar Cloud Sync de verdad? Tus datos se mantienen localmente.",
|
||||
"err_activation_failed": "Activación fallida",
|
||||
"err_deactivation_failed": "Desactivación fallida",
|
||||
"err_change_failed": "Cambio fallido",
|
||||
"status_label": "Estado",
|
||||
"status_next_charge": "Próximo cobro el {date}",
|
||||
"status_encrypted": "Sincronizado cifrado en todos los dispositivos",
|
||||
"status_insufficient": "No hay créditos suficientes — recarga para continuar",
|
||||
"status_local_only": "Tus datos solo se guardan localmente en este dispositivo",
|
||||
"badge_active": "Activo",
|
||||
"badge_paused": "Pausado",
|
||||
"badge_inactive": "Inactivo",
|
||||
"credits_available": "Créditos disponibles",
|
||||
"credits_topup": "Recargar créditos",
|
||||
"interval_label": "Intervalo de facturación",
|
||||
"interval_price_hint": "{price} créditos · {hint}",
|
||||
"changing": "Cambiando…",
|
||||
"change_to": "Cambiar a {label}",
|
||||
"change_hint": "El cambio se aplica desde el próximo cobro",
|
||||
"deactivating": "Desactivando…",
|
||||
"deactivate": "Desactivar Cloud Sync",
|
||||
"activating": "Activando…",
|
||||
"activate": "Activar · {price} créditos",
|
||||
"insufficient_warn_pre": "Créditos insuficientes.",
|
||||
"insufficient_topup": "Recargar",
|
||||
"footnote": "Cloud Sync sincroniza tus datos cifrados en todos tus dispositivos. Los datos locales siempre se mantienen — incluso cuando la sincronización está pausada o desactivada."
|
||||
},
|
||||
"mydata": {
|
||||
"title": "Mis datos (RGPD)",
|
||||
"description": "Resumen de todos los datos almacenados sobre ti",
|
||||
"qr_export": "Exportar como QR",
|
||||
"qr_short": "QR",
|
||||
"export": "Exportar",
|
||||
"exporting": "Exportando…",
|
||||
"retry": "Reintentar",
|
||||
"no_name": "Sin nombre",
|
||||
"verified": "verificado",
|
||||
"not_verified": "no verificado",
|
||||
"registered_at": "Registrado el",
|
||||
"total_entities": "Entidades totales",
|
||||
"total_entities_desc": "Registros en todas las apps",
|
||||
"projects_with_data": "Proyectos con datos",
|
||||
"footnote_pre": "Sin cookies de seguimiento — analítica anónima vía Umami. Detalles en la ",
|
||||
"footnote_link": "política de privacidad",
|
||||
"footnote_post": ".",
|
||||
"auth_title": "Autenticación",
|
||||
"auth_description": "Sesiones, cuentas y 2FA",
|
||||
"auth_sessions": "Sesiones activas",
|
||||
"auth_accounts": "Cuentas vinculadas",
|
||||
"auth_two_fa": "Doble factor (2FA)",
|
||||
"auth_two_fa_active": "Activado",
|
||||
"auth_two_fa_inactive": "Desactivado",
|
||||
"auth_last_login": "Último inicio de sesión",
|
||||
"credits_title": "Créditos",
|
||||
"credits_description": "Saldo y transacciones",
|
||||
"credits_balance": "Saldo actual",
|
||||
"credits_total_earned": "Total ganado",
|
||||
"credits_total_spent": "Total gastado",
|
||||
"credits_transactions": "Transacciones",
|
||||
"project_title": "Datos de proyectos",
|
||||
"project_description": "Registros por app",
|
||||
"col_app": "App",
|
||||
"col_count": "Entradas",
|
||||
"col_time": "Última actividad",
|
||||
"project_status_active": "Activo",
|
||||
"project_status_empty": "Sin datos",
|
||||
"project_status_unavailable": "No disponible",
|
||||
"project_unreachable": "no accesible",
|
||||
"relative_just_now": "ahora mismo",
|
||||
"relative_minutes_ago": "hace {n} min",
|
||||
"relative_hours_ago": "hace {n} h",
|
||||
"relative_days_ago": "hace {n} d",
|
||||
"retention_title": "Plazos de conservación",
|
||||
"retention_description": "Cuánto tiempo guardamos tus datos",
|
||||
"retention_account": "Cuenta de usuario y perfil",
|
||||
"retention_account_value": "Hasta su eliminación",
|
||||
"retention_sessions": "Sesiones e historial de inicio",
|
||||
"retention_sessions_value": "90 días tras la expiración",
|
||||
"retention_credit": "Transacciones de créditos",
|
||||
"retention_credit_value": "10 años (legal)",
|
||||
"retention_security": "Registros de seguridad",
|
||||
"retention_security_value": "1 año",
|
||||
"retention_project": "Datos de proyectos (chat, todo, …)",
|
||||
"retention_project_value": "Hasta su eliminación",
|
||||
"danger_title": "Zona de peligro",
|
||||
"danger_description": "Acciones irreversibles",
|
||||
"danger_delete_title": "Eliminar todos mis datos",
|
||||
"danger_delete_desc": "Elimina permanentemente tu cuenta y todos los datos relacionados en todos los proyectos. No se puede deshacer.",
|
||||
"danger_delete_btn": "Eliminar datos"
|
||||
},
|
||||
"tag_presets": {
|
||||
"title": "Plantillas de etiquetas",
|
||||
"hint": "Conjuntos de etiquetas guardados que puedes elegir como plantilla inicial al crear un nuevo espacio. Los cambios en una plantilla no afectan a los espacios ya creados — es una copia de un solo sentido.",
|
||||
"name_required": "Introduce un nombre",
|
||||
"no_active_space": "Ningún espacio activo",
|
||||
"name_placeholder": "Nombre de la plantilla (p. ej. «Mi conjunto estándar»)",
|
||||
"create_from": "Crear a partir de «{name}»",
|
||||
"creating": "Creando …",
|
||||
"loading_space": "Cargando espacio …",
|
||||
"delete_confirm": "¿Eliminar la plantilla «{name}»?",
|
||||
"empty": "Aún no hay plantillas. Crea la primera dándole un nombre al conjunto de etiquetas actual — se usará automáticamente como predeterminada para nuevos espacios.",
|
||||
"badge_default": "Predeterminada",
|
||||
"tag_count_one": "{count} etiqueta",
|
||||
"tag_count_other": "{count} etiquetas",
|
||||
"with_groups": "con grupos",
|
||||
"aria_set_default": "Establecer como predeterminada",
|
||||
"aria_delete": "Eliminar"
|
||||
},
|
||||
"vault": {
|
||||
"title": "Cifrado",
|
||||
"description": "Los campos sensibles se cifran con AES-GCM-256 antes de escribirse en la base de datos local.",
|
||||
"badge_encrypted": "Cifrado",
|
||||
"badge_locked": "Bloqueado",
|
||||
"badge_recovery_required": "Código de recuperación necesario",
|
||||
"badge_auth_required": "Inicio de sesión necesario",
|
||||
"badge_network_error": "Error de red",
|
||||
"badge_server_error": "Error del servidor",
|
||||
"badge_unknown_error": "Error desconocido",
|
||||
"status_unlocked_pre": "Tu clave personal está cargada en este dispositivo.",
|
||||
"status_unlocked_fields": "{count} campos",
|
||||
"status_unlocked_tables": "{count} tablas",
|
||||
"status_unlocked_post": "se guardan cifrados.",
|
||||
"status_locked": "Tu clave no está cargada. El contenido cifrado no se puede leer hasta que vuelvas a iniciar sesión o cargues la clave manualmente.",
|
||||
"status_load_now": "Cargar la clave ahora",
|
||||
"status_error": "Hubo un problema al cargar tu clave de cifrado. Vuelve a iniciar sesión o comprueba tu conexión a internet.",
|
||||
"status_retry": "Reintentar",
|
||||
"toast_unlocked": "Clave de cifrado cargada",
|
||||
"toast_unlock_failed": "No se pudo cargar la clave: {status}",
|
||||
"toast_rotated": "Clave rotada. Las nuevas escrituras usarán la clave nueva.",
|
||||
"toast_rotate_failed": "Rotación fallida: {status}",
|
||||
"toast_zk_enabled": "Modo zero-knowledge activado",
|
||||
"toast_zk_disabled": "Modo zero-knowledge desactivado",
|
||||
"toast_recovery_cleared": "Código de recuperación eliminado",
|
||||
"toast_clipboard_ok": "Código copiado al portapapeles",
|
||||
"toast_clipboard_failed": "No se pudo copiar el código",
|
||||
"err_code_mismatch": "El código introducido no coincide con el mostrado.",
|
||||
"threat_title": "Qué puede ver Mana",
|
||||
"threat_never_label": "Lo que Mana nunca ve:",
|
||||
"threat_never_body": "tu contenido cifrado (chat, notas, sueños, memos, datos de contacto, notas de ciclo, descripciones de transacciones, …). Solo sale de tu dispositivo como un blob ilegible.",
|
||||
"threat_could_label": "Lo que Mana técnicamente podría descifrar:",
|
||||
"threat_could_body": "tu master key, si un empleado con acceso a la clave de cifrado de claves accede a ella activamente. En la práctica está protegido frente a todas las amenazas realistas, salvo una orden judicial contra Mana misma.",
|
||||
"threat_visible_label": "Lo que sigue siendo visible estructuralmente:",
|
||||
"threat_visible_body": "el número de tus notas / chats / contactos, marcas temporales, conexiones entre registros. El contenido en sí no.",
|
||||
"fields_title": "Campos cifrados",
|
||||
"fields_meta": "{fields} campos · {tables} tablas",
|
||||
"fields_description": "Qué columnas en qué tablas se guardan cifradas en el dispositivo. Los metadatos estructurales (IDs, marcas temporales, banderas de estado) se mantienen en texto plano para que los índices, la ordenación y la sincronización sigan funcionando.",
|
||||
"show_more": "Mostrar {count} más",
|
||||
"show_less": "Mostrar menos",
|
||||
"zk_title": "Modo zero-knowledge",
|
||||
"zk_state_active": "Activo",
|
||||
"zk_state_inactive": "No activo",
|
||||
"zk_desc_optional": "Opcional, avanzado.",
|
||||
"zk_desc_body_pre": " En modo zero-knowledge, Mana guarda tu clave ",
|
||||
"zk_desc_body_em": "solo en una forma que nosotros mismos no podemos descifrar",
|
||||
"zk_desc_body_post": ". Al iniciar sesión desde un nuevo dispositivo necesitarás tu código de recuperación para desbloquear tus datos.",
|
||||
"zk_pro_label": "Ventaja:",
|
||||
"zk_pro_body": " ni siquiera un empleado de Mana con acceso total al servidor puede leer tu contenido.",
|
||||
"zk_con_label": "Riesgo:",
|
||||
"zk_con_body": " si pierdes el código de recuperación, tus datos se pierden de forma irreversible — ya no tenemos clave de respaldo.",
|
||||
"zk_existing_warn": "Ya tienes un código de recuperación guardado, pero el modo zero-knowledge aún no está activo. Puedes activarlo directamente o restablecer el código.",
|
||||
"zk_enable_now": "Activar zero-knowledge ahora",
|
||||
"zk_enabling": "Activando …",
|
||||
"zk_generate_new": "Generar nuevo código de recuperación",
|
||||
"zk_clear": "Eliminar código de recuperación",
|
||||
"zk_clear_confirm": "Sí, eliminar",
|
||||
"zk_clearing": "Eliminando …",
|
||||
"zk_setup": "Configurar código de recuperación",
|
||||
"zk_generating": "Generando …",
|
||||
"step_1_title": "Guarda el código de forma segura",
|
||||
"step_1_desc_pre": "Guarda este código en un lugar seguro (gestor de contraseñas, impreso en una caja fuerte, …). ",
|
||||
"step_1_desc_strong": "Solo lo mostramos una vez.",
|
||||
"copy": "Copiar",
|
||||
"step_1_continue": "He guardado el código →",
|
||||
"step_2_title": "Vuelve a teclear el código",
|
||||
"step_2_desc": "Teclea (o pega) el código que acabas de guardar. Así nos aseguramos de que la copia es realmente completa.",
|
||||
"step_2_placeholder": "1A2B-3C4D-5E6F-...",
|
||||
"step_2_confirm": "Confirmar código",
|
||||
"step_3_title": "Activar modo zero-knowledge",
|
||||
"step_3_desc_pre": "Si activas ahora, el servidor borra su copia de tu clave. A partir de ese momento ",
|
||||
"step_3_desc_strong": "solo podrás acceder con el código de recuperación",
|
||||
"step_3_desc_post": " a tus datos cifrados.",
|
||||
"step_3_warn": "Esta acción no se puede deshacer sin el código de recuperación. Si pierdes el código, tu contenido queda perdido.",
|
||||
"step_3_confirm": "Sí, activar el modo zero-knowledge",
|
||||
"cancel": "Cancelar",
|
||||
"zk_active_note": "El servidor ya no puede descifrar tus datos. La próxima vez que inicies sesión en un nuevo dispositivo, te pediremos tu código de recuperación.",
|
||||
"rotate_step_title": "Nuevo código de recuperación",
|
||||
"rotate_step_strong": "Tu código antiguo ya no es válido.",
|
||||
"rotate_step_desc": " Guarda el nuevo código en un lugar seguro antes de salir de esta página — solo lo mostramos una vez.",
|
||||
"rotate_step_save": "He guardado el nuevo código",
|
||||
"rotate_btn": "Rotar código de recuperación",
|
||||
"rotate_busy": "Rotando …",
|
||||
"zk_disable_btn": "Desactivar modo zero-knowledge …",
|
||||
"zk_disable_desc": "Para restaurar la clave del servidor necesitamos tu master key cargada actualmente. Está ahora mismo en tu navegador — la enviamos una vez al servidor, que la vuelve a envolver con la KEK.",
|
||||
"zk_disable_confirm": "Sí, desactivar",
|
||||
"zk_disabling": "Desactivando …",
|
||||
"rotate_section_title": "Rotar clave",
|
||||
"rotate_section_warn_strong": "Atención:",
|
||||
"rotate_section_warn_body_pre": " Al rotar se genera una nueva clave. Los datos cifrados con la clave antigua dejarán de poder leerse — salvo que se hayan descifrado y reescrito antes con la nueva clave. Mana actualmente ",
|
||||
"rotate_section_warn_em": "no realiza este re-cifrado automáticamente",
|
||||
"rotate_section_warn_post": ".",
|
||||
"rotate_section_when": "¿Cuándo usarlo? Si sospechas que tu dispositivo se ha visto comprometido, o como práctica regular tras un inicio de sesión desde un lugar desconocido.",
|
||||
"rotate_section_btn": "Rotar clave …",
|
||||
"rotate_section_confirm": "Sí, rotar ahora",
|
||||
"rotate_section_busy": "Rotando …"
|
||||
}
|
||||
}
|
||||
414
apps/mana/apps/web/src/lib/i18n/locales/settings/fr.json
Normal file
414
apps/mana/apps/web/src/lib/i18n/locales/settings/fr.json
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
{
|
||||
"categories": {
|
||||
"general": { "label": "Général", "description": "Thème, langue, notifications" },
|
||||
"ai": { "label": "IA", "description": "Backend de calcul et modèles" },
|
||||
"security": { "label": "Sécurité", "description": "Passkeys, 2FA, chiffrement et sessions" },
|
||||
"privacy": {
|
||||
"label": "Confidentialité",
|
||||
"description": "Ce qui est public ou partagé par lien actuellement — avec interrupteur d'urgence."
|
||||
},
|
||||
"data": {
|
||||
"label": "Données et synchro",
|
||||
"description": "Cloud sync, export, sauvegarde et RGPD"
|
||||
},
|
||||
"tag_presets": {
|
||||
"label": "Modèles d'étiquettes",
|
||||
"description": "Jeux d'étiquettes pour de nouveaux espaces"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"aria_categories": "Catégories de réglages",
|
||||
"search_placeholder": "Rechercher dans les réglages…",
|
||||
"aria_search": "Rechercher dans les réglages",
|
||||
"aria_clear": "Effacer la recherche",
|
||||
"no_results": "Aucun résultat pour « {query} »"
|
||||
},
|
||||
"search": {
|
||||
"theme": { "label": "Thème", "keywords": "sombre, clair, couleur, design" },
|
||||
"language": { "label": "Langue", "keywords": "language, i18n, allemand, anglais" },
|
||||
"notifications": { "label": "Notifications", "keywords": "notification, son" },
|
||||
"ai_options": { "label": "Options d'IA", "keywords": "llm, ia, ai, compute" },
|
||||
"browser_model": {
|
||||
"label": "Modèle navigateur (Gemma)",
|
||||
"keywords": "gemma, webgpu, local, hors ligne"
|
||||
},
|
||||
"mana_server": { "label": "Serveur Mana (IA)", "keywords": "serveur, self-hosted" },
|
||||
"cloud_ai": { "label": "IA cloud (Gemini)", "keywords": "google, cloud, gemini" },
|
||||
"passkeys": { "label": "Passkeys", "keywords": "webauthn, fido, biométrie" },
|
||||
"sessions": { "label": "Sessions actives", "keywords": "logout, appareil" },
|
||||
"two_factor": { "label": "Double facteur (2FA)", "keywords": "totp, 2fa, mfa" },
|
||||
"vault": { "label": "Chiffrement", "keywords": "vault, chiffrement, aes, clé, zero-knowledge" },
|
||||
"security_log": { "label": "Journal de sécurité", "keywords": "audit, historique" },
|
||||
"privacy_overview": {
|
||||
"label": "Aperçu de la confidentialité",
|
||||
"keywords": "public, unlisted, partage, lien"
|
||||
},
|
||||
"reset_all_private": {
|
||||
"label": "Tout repasser en privé",
|
||||
"keywords": "kill-switch, reset, privé, révoquer"
|
||||
},
|
||||
"cloud_sync": { "label": "Cloud Sync", "keywords": "sync, appareils" },
|
||||
"data_export": { "label": "Exporter les données", "keywords": "export, rgpd, gdpr, json" },
|
||||
"auth_data": { "label": "Authentification", "keywords": "sessions, 2fa, login" },
|
||||
"credits_data": { "label": "Crédits et transactions", "keywords": "solde, transactions" },
|
||||
"project_data": { "label": "Données projet", "keywords": "projets, apps, statistiques" },
|
||||
"retention": { "label": "Durées de conservation", "keywords": "retention, rgpd, durées" },
|
||||
"backup": {
|
||||
"label": "Sauvegarde et restauration",
|
||||
"keywords": "backup, restore, import, archive, .mana"
|
||||
},
|
||||
"delete_account": {
|
||||
"label": "Supprimer le compte",
|
||||
"keywords": "delete, gdpr, rgpd, zone dangereuse"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "Général",
|
||||
"description": "Langue, début de semaine et sons",
|
||||
"language_title": "Langue d'affichage",
|
||||
"language_description": "Langue de l'interface utilisateur",
|
||||
"color_scheme_title": "Schéma de couleurs",
|
||||
"color_scheme_description": "Couleur d'accentuation de l'interface",
|
||||
"color_scheme_ocean": "Océan",
|
||||
"color_scheme_nature": "Nature",
|
||||
"color_scheme_lume": "Lume",
|
||||
"color_scheme_stone": "Pierre",
|
||||
"color_mode_title": "Mode couleur",
|
||||
"color_mode_description": "Clair, sombre ou automatique",
|
||||
"color_mode_light": "Clair",
|
||||
"color_mode_dark": "Sombre",
|
||||
"color_mode_system": "Système",
|
||||
"week_start_title": "Début de semaine",
|
||||
"week_start_description": "Premier jour de la semaine dans les calendriers",
|
||||
"week_start_monday": "Lundi",
|
||||
"week_start_sunday": "Dimanche",
|
||||
"sounds_title": "Sons",
|
||||
"sounds_description": "Effets sonores dans toutes les apps",
|
||||
"sounds_aria": "Activer ou désactiver les sons",
|
||||
"onboarding_title": "Refaire l'onboarding",
|
||||
"onboarding_description": "Reprendre nom, apparence et modules",
|
||||
"onboarding_start": "Démarrer",
|
||||
"onboarding_starting": "Démarrage…"
|
||||
},
|
||||
"ai": {
|
||||
"title": "Options d'IA",
|
||||
"description": "Choisis quelles couches d'IA Mana peut utiliser — d'aucune à toutes",
|
||||
"tier_local_title": "Local (sans IA)",
|
||||
"tier_local_badge": "toujours actif",
|
||||
"tier_local_subtitle": "Fonctions de base sans aucune IA",
|
||||
"tier_local_description": "Mana fonctionne aussi totalement sans IA : détection de dates, recherche et classification simple utilisent des algorithmes classiques. Certaines fonctions sont alors limitées, mais tout est 100 % hors ligne et gratuit.",
|
||||
"tier_browser_title": "Sur ton appareil",
|
||||
"tier_browser_subtitle": "Gemma 4 E2B (Google) dans le navigateur",
|
||||
"tier_browser_bullet_1": "100 % local — les données ne quittent pas ton appareil",
|
||||
"tier_browser_bullet_2": "~500 Mo de téléchargement unique",
|
||||
"tier_browser_bullet_3": "Nécessite WebGPU + 2 Go de mémoire GPU libre",
|
||||
"tier_browser_blocked": "WebGPU n'est pas disponible dans ton navigateur. Fonctionne dans Chrome/Edge 113+ ou Safari 18+.",
|
||||
"tier_browser_loaded": "✓ Modèle chargé",
|
||||
"tier_browser_loading": "Chargement de {name} ({percent} %)…",
|
||||
"tier_browser_load_btn": "Charger le modèle (~{size} Mo)",
|
||||
"tier_browser_load_btn_loading": "Chargement…",
|
||||
"tier_mana_server_title": "Serveur Mana",
|
||||
"tier_mana_server_subtitle": "Auto-hébergé sur notre Mac Mini",
|
||||
"tier_mana_server_bullet_1": "Plus rapide et plus fort que le LLM dans le navigateur",
|
||||
"tier_mana_server_bullet_2": "Les données circulent chiffrées vers notre serveur",
|
||||
"tier_mana_server_bullet_3": "Aucun contenu n'est stocké",
|
||||
"tier_byok_title": "Ta propre clé API",
|
||||
"tier_byok_subtitle": "OpenAI, Anthropic, Google Gemini ou Mistral",
|
||||
"tier_byok_bullet_1": "Direct depuis le navigateur — sans escale par le serveur Mana",
|
||||
"tier_byok_bullet_2": "Tu paies le fournisseur, nous ne voyons rien",
|
||||
"tier_byok_bullet_3": "Les clés sont stockées chiffrées dans ton vault",
|
||||
"tier_cloud_title": "Google Gemini",
|
||||
"tier_cloud_subtitle": "API cloud via notre proxy serveur",
|
||||
"tier_cloud_bullet_1": "Meilleure qualité pour les tâches complexes",
|
||||
"tier_cloud_bullet_2": "Réponses les plus rapides",
|
||||
"tier_cloud_bullet_3": "Les données sont traitées par Google",
|
||||
"tier_cloud_warning": "Recommandé uniquement pour des contenus non sensibles",
|
||||
"tier_cloud_consent_title": "Confirmation requise",
|
||||
"tier_cloud_consent_desc": "Les requêtes cloud envoient ton contenu à Google. Confirme que tu as compris et que tu acceptes.",
|
||||
"tier_cloud_consent_btn": "Compris, activer le cloud",
|
||||
"tier_cloud_consent_ok": "✓ Consentement cloud accordé",
|
||||
"tier_cloud_consent_revoke": "Retirer",
|
||||
"badge_active": "actif",
|
||||
"summary_label": "Ordre actuel :",
|
||||
"summary_local_only": "Uniquement local (sans IA) — la plupart des fonctions IA sont limitées.",
|
||||
"summary_chain_local_fallback": "Local (repli)",
|
||||
"fallback_title": "Replier sur « Local » en cas d'erreur",
|
||||
"fallback_description": "Si la couche IA choisie ne peut pas répondre, Mana essaie la variante locale (si disponible). Désactivé : affiche une erreur à la place.",
|
||||
"fallback_aria": "Repli sur local",
|
||||
"show_source_title": "Afficher la source à chaque résultat IA",
|
||||
"show_source_description": "Affiche un petit marqueur sous chaque réponse générée par IA, comme « Sur ton appareil » ou « via Google Gemini » — pour que tu voies toujours où tes données sont traitées.",
|
||||
"show_source_aria": "Afficher la source"
|
||||
},
|
||||
"byok": {
|
||||
"vault_locked": "Le vault est verrouillé — connecte-toi d'abord pour gérer les clés.",
|
||||
"loading": "Chargement…",
|
||||
"add_first": "Ajouter la première clé API",
|
||||
"add_more": "Ajouter une autre clé",
|
||||
"field_provider": "Fournisseur",
|
||||
"field_label": "Étiquette",
|
||||
"field_label_placeholder": "p. ex. OpenAI perso",
|
||||
"field_api_key": "Clé API",
|
||||
"field_api_key_placeholder_generic": "Clé API",
|
||||
"field_model": "Modèle",
|
||||
"field_model_default": "Par défaut ({model})",
|
||||
"field_default_short": "Par défaut",
|
||||
"field_as_default": "Par défaut",
|
||||
"label_required": "L'étiquette et la clé API sont obligatoires",
|
||||
"cancel": "Annuler",
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement…",
|
||||
"confirm_delete": "Supprimer la clé pour de bon ?",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"set_default": "Par défaut",
|
||||
"badge_default": "Par défaut",
|
||||
"meta_line": "{provider} · {model} · {count} appels · {cost}"
|
||||
},
|
||||
"security": {
|
||||
"title": "Sécurité",
|
||||
"description": "Passkeys, 2FA, chiffrement et sessions"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Aperçu de la confidentialité",
|
||||
"description": "Toutes les entrées que tu montres publiquement ou partages par lien — avec retour arrière en un clic.",
|
||||
"summary_public": "public",
|
||||
"summary_unlisted": "partageable par lien",
|
||||
"loading": "Chargement…",
|
||||
"empty": "Pour l'instant rien n'est public ni partagé par lien — bien joué.",
|
||||
"badge_public": "Public",
|
||||
"badge_unlisted": "Par lien",
|
||||
"open": "Ouvrir",
|
||||
"set_private": "Privé",
|
||||
"kill_all": "Tout repasser en privé",
|
||||
"confirm_one": "{count} entrée sera réinitialisée à « Espace ». Les liens de partage actifs seront révoqués. Continuer ?",
|
||||
"confirm_other": "{count} entrées seront réinitialisées à « Espace ». Les liens de partage actifs seront révoqués. Continuer ?",
|
||||
"cancel": "Annuler",
|
||||
"killing": "Réinitialisation…",
|
||||
"confirm_kill": "Oui, tout réinitialiser",
|
||||
"toast_set_private": "« {title} » est désormais privé",
|
||||
"toast_set_private_failed": "Impossible de rétrograder « {title} »",
|
||||
"toast_kill_partial": "{flipped} entrées en privé — {failed} échecs",
|
||||
"toast_kill_done": "{flipped} entrées remises en privé",
|
||||
"toast_kill_failed": "L'interrupteur d'urgence a échoué"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Cloud Sync",
|
||||
"description": "Synchronise tes données sur tous tes appareils",
|
||||
"interval_monthly": "Mensuel",
|
||||
"interval_quarterly": "Trimestriel",
|
||||
"interval_yearly": "Annuel",
|
||||
"interval_short_monthly": "Mois",
|
||||
"interval_short_quarterly": "Trimestre",
|
||||
"interval_short_yearly": "Année",
|
||||
"interval_hint_monthly": "facturé chaque mois · ~1 crédit/jour",
|
||||
"interval_hint_quarterly": "facturé tous les 3 mois",
|
||||
"interval_hint_yearly": "facturé une fois par an",
|
||||
"toast_activated": "Cloud Sync activé !",
|
||||
"toast_deactivated": "Cloud Sync désactivé",
|
||||
"toast_interval_changed": "Intervalle changé en {label}",
|
||||
"deactivate_confirm": "Vraiment désactiver Cloud Sync ? Tes données restent localement.",
|
||||
"err_activation_failed": "Activation échouée",
|
||||
"err_deactivation_failed": "Désactivation échouée",
|
||||
"err_change_failed": "Changement échoué",
|
||||
"status_label": "Statut",
|
||||
"status_next_charge": "Prochaine facturation le {date}",
|
||||
"status_encrypted": "Synchronisé chiffré sur tous les appareils",
|
||||
"status_insufficient": "Crédits insuffisants — recharge pour continuer",
|
||||
"status_local_only": "Tes données ne sont stockées que localement sur cet appareil",
|
||||
"badge_active": "Actif",
|
||||
"badge_paused": "En pause",
|
||||
"badge_inactive": "Inactif",
|
||||
"credits_available": "Crédits disponibles",
|
||||
"credits_topup": "Recharger les crédits",
|
||||
"interval_label": "Intervalle de facturation",
|
||||
"interval_price_hint": "{price} crédits · {hint}",
|
||||
"changing": "Changement…",
|
||||
"change_to": "Passer à {label}",
|
||||
"change_hint": "Le changement s'applique à la prochaine facturation",
|
||||
"deactivating": "Désactivation…",
|
||||
"deactivate": "Désactiver Cloud Sync",
|
||||
"activating": "Activation…",
|
||||
"activate": "Activer · {price} crédits",
|
||||
"insufficient_warn_pre": "Crédits insuffisants.",
|
||||
"insufficient_topup": "Recharger",
|
||||
"footnote": "Cloud Sync synchronise tes données chiffrées sur tous tes appareils. Les données locales sont toujours conservées — même quand la synchro est en pause ou désactivée."
|
||||
},
|
||||
"mydata": {
|
||||
"title": "Mes données (RGPD)",
|
||||
"description": "Aperçu de toutes les données que nous stockons sur toi",
|
||||
"qr_export": "Exporter en QR code",
|
||||
"qr_short": "QR",
|
||||
"export": "Exporter",
|
||||
"exporting": "Export…",
|
||||
"retry": "Réessayer",
|
||||
"no_name": "Sans nom",
|
||||
"verified": "vérifié",
|
||||
"not_verified": "non vérifié",
|
||||
"registered_at": "Inscrit le",
|
||||
"total_entities": "Entités totales",
|
||||
"total_entities_desc": "Enregistrements toutes apps confondues",
|
||||
"projects_with_data": "Projets avec données",
|
||||
"footnote_pre": "Pas de cookies de suivi — analyse anonyme via Umami. Détails dans la ",
|
||||
"footnote_link": "politique de confidentialité",
|
||||
"footnote_post": ".",
|
||||
"auth_title": "Authentification",
|
||||
"auth_description": "Sessions, comptes et 2FA",
|
||||
"auth_sessions": "Sessions actives",
|
||||
"auth_accounts": "Comptes liés",
|
||||
"auth_two_fa": "Double facteur (2FA)",
|
||||
"auth_two_fa_active": "Activé",
|
||||
"auth_two_fa_inactive": "Désactivé",
|
||||
"auth_last_login": "Dernière connexion",
|
||||
"credits_title": "Crédits",
|
||||
"credits_description": "Solde et transactions",
|
||||
"credits_balance": "Solde actuel",
|
||||
"credits_total_earned": "Total gagné",
|
||||
"credits_total_spent": "Total dépensé",
|
||||
"credits_transactions": "Transactions",
|
||||
"project_title": "Données de projet",
|
||||
"project_description": "Enregistrements par app",
|
||||
"col_app": "App",
|
||||
"col_count": "Entrées",
|
||||
"col_time": "Dernière activité",
|
||||
"project_status_active": "Actif",
|
||||
"project_status_empty": "Pas de données",
|
||||
"project_status_unavailable": "Indisponible",
|
||||
"project_unreachable": "inaccessible",
|
||||
"relative_just_now": "à l'instant",
|
||||
"relative_minutes_ago": "il y a {n} min",
|
||||
"relative_hours_ago": "il y a {n} h",
|
||||
"relative_days_ago": "il y a {n} j",
|
||||
"retention_title": "Durées de conservation",
|
||||
"retention_description": "Combien de temps nous gardons tes données",
|
||||
"retention_account": "Compte utilisateur et profil",
|
||||
"retention_account_value": "Jusqu'à la suppression",
|
||||
"retention_sessions": "Sessions et historique de connexion",
|
||||
"retention_sessions_value": "90 jours après expiration",
|
||||
"retention_credit": "Transactions de crédits",
|
||||
"retention_credit_value": "10 ans (légal)",
|
||||
"retention_security": "Journaux de sécurité",
|
||||
"retention_security_value": "1 an",
|
||||
"retention_project": "Données de projet (chat, todo, …)",
|
||||
"retention_project_value": "Jusqu'à la suppression",
|
||||
"danger_title": "Zone dangereuse",
|
||||
"danger_description": "Actions irréversibles",
|
||||
"danger_delete_title": "Supprimer toutes mes données",
|
||||
"danger_delete_desc": "Supprime définitivement ton compte et toutes les données liées dans tous les projets. Impossible à annuler.",
|
||||
"danger_delete_btn": "Supprimer les données"
|
||||
},
|
||||
"tag_presets": {
|
||||
"title": "Modèles d'étiquettes",
|
||||
"hint": "Jeux d'étiquettes enregistrés que tu peux choisir comme modèle de départ lors de la création d'un nouvel espace. Modifier un modèle n'affecte pas les espaces déjà créés — c'est une copie à sens unique.",
|
||||
"name_required": "Saisis un nom",
|
||||
"no_active_space": "Aucun espace actif",
|
||||
"name_placeholder": "Nom du modèle (p. ex. « Mon set standard »)",
|
||||
"create_from": "Créer depuis « {name} »",
|
||||
"creating": "Création …",
|
||||
"loading_space": "Chargement de l'espace …",
|
||||
"delete_confirm": "Supprimer le modèle « {name} » ?",
|
||||
"empty": "Pas encore de modèles. Crée le premier en donnant un nom au jeu d'étiquettes actuel — il sera automatiquement utilisé par défaut pour les nouveaux espaces.",
|
||||
"badge_default": "Par défaut",
|
||||
"tag_count_one": "{count} étiquette",
|
||||
"tag_count_other": "{count} étiquettes",
|
||||
"with_groups": "avec groupes",
|
||||
"aria_set_default": "Définir comme défaut",
|
||||
"aria_delete": "Supprimer"
|
||||
},
|
||||
"vault": {
|
||||
"title": "Chiffrement",
|
||||
"description": "Les champs sensibles sont chiffrés avec AES-GCM-256 avant d'être écrits dans la base locale.",
|
||||
"badge_encrypted": "Chiffré",
|
||||
"badge_locked": "Verrouillé",
|
||||
"badge_recovery_required": "Code de récupération requis",
|
||||
"badge_auth_required": "Connexion requise",
|
||||
"badge_network_error": "Erreur réseau",
|
||||
"badge_server_error": "Erreur serveur",
|
||||
"badge_unknown_error": "Erreur inconnue",
|
||||
"status_unlocked_pre": "Ta clé personnelle est chargée sur cet appareil.",
|
||||
"status_unlocked_fields": "{count} champs",
|
||||
"status_unlocked_tables": "{count} tables",
|
||||
"status_unlocked_post": "sont stockés chiffrés.",
|
||||
"status_locked": "Ta clé n'est pas chargée. Le contenu chiffré ne peut pas être lu tant que tu ne te reconnectes pas ou ne charges pas la clé manuellement.",
|
||||
"status_load_now": "Charger la clé maintenant",
|
||||
"status_error": "Un problème est survenu lors du chargement de ta clé de chiffrement. Reconnecte-toi ou vérifie ta connexion internet.",
|
||||
"status_retry": "Réessayer",
|
||||
"toast_unlocked": "Clé de chiffrement chargée",
|
||||
"toast_unlock_failed": "Impossible de charger la clé : {status}",
|
||||
"toast_rotated": "Clé tournée. Les nouvelles écritures utilisent la nouvelle clé.",
|
||||
"toast_rotate_failed": "Rotation échouée : {status}",
|
||||
"toast_zk_enabled": "Mode zero-knowledge activé",
|
||||
"toast_zk_disabled": "Mode zero-knowledge désactivé",
|
||||
"toast_recovery_cleared": "Code de récupération supprimé",
|
||||
"toast_clipboard_ok": "Code copié dans le presse-papiers",
|
||||
"toast_clipboard_failed": "Impossible de copier le code",
|
||||
"err_code_mismatch": "Le code saisi ne correspond pas à celui affiché.",
|
||||
"threat_title": "Ce que Mana peut voir",
|
||||
"threat_never_label": "Ce que Mana ne voit jamais :",
|
||||
"threat_never_body": "ton contenu chiffré (chat, notes, rêves, mémos, détails de contacts, notes de cycle, descriptions de transactions, …). Il ne quitte ton appareil que sous forme de blob illisible.",
|
||||
"threat_could_label": "Ce que Mana pourrait techniquement déchiffrer :",
|
||||
"threat_could_body": "ta master key, si un employé ayant accès à la clé de chiffrement de clés y accède activement. En pratique, c'est protégé contre toutes les menaces réalistes hormis une divulgation imposée par décision de justice contre Mana.",
|
||||
"threat_visible_label": "Ce qui reste visible structurellement :",
|
||||
"threat_visible_body": "le nombre de tes notes / chats / contacts, les horodatages, les liens entre enregistrements. Pas le contenu lui-même.",
|
||||
"fields_title": "Champs chiffrés",
|
||||
"fields_meta": "{fields} champs · {tables} tables",
|
||||
"fields_description": "Quelles colonnes dans quelles tables sont stockées chiffrées sur l'appareil. Les métadonnées structurelles (IDs, horodatages, drapeaux d'état) restent volontairement en clair afin que les index, le tri et la synchro continuent de fonctionner.",
|
||||
"show_more": "Afficher {count} de plus",
|
||||
"show_less": "Afficher moins",
|
||||
"zk_title": "Mode zero-knowledge",
|
||||
"zk_state_active": "Actif",
|
||||
"zk_state_inactive": "Inactif",
|
||||
"zk_desc_optional": "Optionnel, avancé.",
|
||||
"zk_desc_body_pre": " En mode zero-knowledge, Mana stocke ta clé ",
|
||||
"zk_desc_body_em": "uniquement sous une forme que nous-mêmes ne pouvons pas déchiffrer",
|
||||
"zk_desc_body_post": ". Lors de la connexion depuis un nouvel appareil, tu auras besoin de ton code de récupération pour déverrouiller tes données.",
|
||||
"zk_pro_label": "Avantage :",
|
||||
"zk_pro_body": " même un employé Mana avec accès complet au serveur ne peut plus lire ton contenu.",
|
||||
"zk_con_label": "Risque :",
|
||||
"zk_con_body": " si tu perds le code de récupération, tes données sont irrémédiablement perdues — nous n'avons plus de clé de secours.",
|
||||
"zk_existing_warn": "Tu as déjà un code de récupération enregistré, mais le mode zero-knowledge n'est pas encore actif. Tu peux l'activer directement ou réinitialiser le code.",
|
||||
"zk_enable_now": "Activer le zero-knowledge maintenant",
|
||||
"zk_enabling": "Activation …",
|
||||
"zk_generate_new": "Générer un nouveau code de récupération",
|
||||
"zk_clear": "Supprimer le code de récupération",
|
||||
"zk_clear_confirm": "Oui, supprimer",
|
||||
"zk_clearing": "Suppression …",
|
||||
"zk_setup": "Configurer le code de récupération",
|
||||
"zk_generating": "Génération …",
|
||||
"step_1_title": "Note le code en lieu sûr",
|
||||
"step_1_desc_pre": "Stocke ce code en lieu sûr (gestionnaire de mots de passe, imprimé dans un coffre, …). ",
|
||||
"step_1_desc_strong": "Nous ne le montrons qu'une seule fois.",
|
||||
"copy": "Copier",
|
||||
"step_1_continue": "J'ai sauvegardé le code →",
|
||||
"step_2_title": "Retape le code",
|
||||
"step_2_desc": "Tape (ou colle) le code que tu viens de sauvegarder. Ainsi nous nous assurons que la sauvegarde est vraiment complète.",
|
||||
"step_2_placeholder": "1A2B-3C4D-5E6F-...",
|
||||
"step_2_confirm": "Confirmer le code",
|
||||
"step_3_title": "Activer le mode zero-knowledge",
|
||||
"step_3_desc_pre": "Si tu actives maintenant, le serveur supprime sa copie de ta clé. À partir de maintenant tu pourras ",
|
||||
"step_3_desc_strong": "uniquement accéder à tes données chiffrées avec le code de récupération",
|
||||
"step_3_desc_post": ".",
|
||||
"step_3_warn": "Cette action est irréversible sans le code de récupération. Si tu le perds, ton contenu est perdu.",
|
||||
"step_3_confirm": "Oui, activer le mode zero-knowledge",
|
||||
"cancel": "Annuler",
|
||||
"zk_active_note": "Le serveur ne peut plus déchiffrer tes données. Lors de ta prochaine connexion sur un nouvel appareil, on te demandera ton code de récupération.",
|
||||
"rotate_step_title": "Nouveau code de récupération",
|
||||
"rotate_step_strong": "Ton ancien code n'est plus valide.",
|
||||
"rotate_step_desc": " Sauvegarde le nouveau code en lieu sûr avant de quitter cette page — nous ne le montrons qu'une seule fois.",
|
||||
"rotate_step_save": "J'ai sauvegardé le nouveau code",
|
||||
"rotate_btn": "Roter le code de récupération",
|
||||
"rotate_busy": "Rotation …",
|
||||
"zk_disable_btn": "Désactiver le mode zero-knowledge …",
|
||||
"zk_disable_desc": "Pour restaurer la clé serveur, nous avons besoin de ta master key actuellement chargée. Elle est dans ton navigateur — nous l'envoyons une fois au serveur, qui la ré-enveloppe avec le KEK.",
|
||||
"zk_disable_confirm": "Oui, désactiver",
|
||||
"zk_disabling": "Désactivation …",
|
||||
"rotate_section_title": "Roter la clé",
|
||||
"rotate_section_warn_strong": "Attention :",
|
||||
"rotate_section_warn_body_pre": " Lors de la rotation, une nouvelle clé est générée. Les données chiffrées avec l'ancienne clé ne seront plus lisibles — sauf si elles ont été déchiffrées et réécrites au préalable avec la nouvelle clé. Mana ne réalise actuellement ",
|
||||
"rotate_section_warn_em": "pas ce ré-chiffrement automatiquement",
|
||||
"rotate_section_warn_post": ".",
|
||||
"rotate_section_when": "Quand l'utiliser ? Si tu suspectes que ton appareil est compromis, ou comme pratique régulière de sécurité après une connexion depuis un endroit inconnu.",
|
||||
"rotate_section_btn": "Roter la clé …",
|
||||
"rotate_section_confirm": "Oui, roter maintenant",
|
||||
"rotate_section_busy": "Rotation …"
|
||||
}
|
||||
}
|
||||
408
apps/mana/apps/web/src/lib/i18n/locales/settings/it.json
Normal file
408
apps/mana/apps/web/src/lib/i18n/locales/settings/it.json
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
{
|
||||
"categories": {
|
||||
"general": { "label": "Generale", "description": "Tema, lingua, notifiche" },
|
||||
"ai": { "label": "IA", "description": "Backend di calcolo e modelli" },
|
||||
"security": { "label": "Sicurezza", "description": "Passkey, 2FA, cifratura e sessioni" },
|
||||
"privacy": {
|
||||
"label": "Privacy",
|
||||
"description": "Cosa è pubblico o condiviso tramite link adesso — con interruttore di emergenza."
|
||||
},
|
||||
"data": {
|
||||
"label": "Dati e sincronizzazione",
|
||||
"description": "Cloud sync, esportazione, backup e GDPR"
|
||||
},
|
||||
"tag_presets": { "label": "Preset di tag", "description": "Set di tag per nuovi spazi" }
|
||||
},
|
||||
"sidebar": {
|
||||
"aria_categories": "Categorie delle impostazioni",
|
||||
"search_placeholder": "Cerca nelle impostazioni…",
|
||||
"aria_search": "Cerca nelle impostazioni",
|
||||
"aria_clear": "Cancella la ricerca",
|
||||
"no_results": "Nessun risultato per «{query}»"
|
||||
},
|
||||
"search": {
|
||||
"theme": { "label": "Tema", "keywords": "scuro, chiaro, colore, design" },
|
||||
"language": { "label": "Lingua", "keywords": "language, i18n, tedesco, inglese" },
|
||||
"notifications": { "label": "Notifiche", "keywords": "notification, suono" },
|
||||
"ai_options": { "label": "Opzioni IA", "keywords": "llm, ia, ai, compute" },
|
||||
"browser_model": {
|
||||
"label": "Modello del browser (Gemma)",
|
||||
"keywords": "gemma, webgpu, locale, offline"
|
||||
},
|
||||
"mana_server": { "label": "Server Mana (IA)", "keywords": "server, self-hosted" },
|
||||
"cloud_ai": { "label": "IA cloud (Gemini)", "keywords": "google, cloud, gemini" },
|
||||
"passkeys": { "label": "Passkey", "keywords": "webauthn, fido, biometria" },
|
||||
"sessions": { "label": "Sessioni attive", "keywords": "logout, dispositivo" },
|
||||
"two_factor": { "label": "Due fattori (2FA)", "keywords": "totp, 2fa, mfa" },
|
||||
"vault": { "label": "Cifratura", "keywords": "vault, cifratura, aes, chiave, zero-knowledge" },
|
||||
"security_log": { "label": "Registro di sicurezza", "keywords": "audit, cronologia" },
|
||||
"privacy_overview": {
|
||||
"label": "Panoramica privacy",
|
||||
"keywords": "pubblico, unlisted, condividere, link"
|
||||
},
|
||||
"reset_all_private": {
|
||||
"label": "Riporta tutto a privato",
|
||||
"keywords": "kill-switch, reset, privato, revocare"
|
||||
},
|
||||
"cloud_sync": { "label": "Cloud Sync", "keywords": "sync, dispositivi" },
|
||||
"data_export": { "label": "Esporta i dati", "keywords": "export, gdpr, json" },
|
||||
"auth_data": { "label": "Autenticazione", "keywords": "sessioni, 2fa, login" },
|
||||
"credits_data": { "label": "Crediti e transazioni", "keywords": "saldo, transazioni" },
|
||||
"project_data": { "label": "Dati di progetto", "keywords": "progetti, app, statistiche" },
|
||||
"retention": { "label": "Periodi di conservazione", "keywords": "retention, gdpr, periodi" },
|
||||
"backup": {
|
||||
"label": "Backup e ripristino",
|
||||
"keywords": "backup, restore, import, archivio, .mana"
|
||||
},
|
||||
"delete_account": { "label": "Elimina account", "keywords": "delete, gdpr, zona di pericolo" }
|
||||
},
|
||||
"general": {
|
||||
"title": "Generale",
|
||||
"description": "Lingua, inizio settimana e suoni",
|
||||
"language_title": "Lingua di visualizzazione",
|
||||
"language_description": "Lingua dell'interfaccia utente",
|
||||
"color_scheme_title": "Schema di colori",
|
||||
"color_scheme_description": "Colore di accento dell'interfaccia",
|
||||
"color_scheme_ocean": "Oceano",
|
||||
"color_scheme_nature": "Natura",
|
||||
"color_scheme_lume": "Lume",
|
||||
"color_scheme_stone": "Pietra",
|
||||
"color_mode_title": "Modalità colore",
|
||||
"color_mode_description": "Chiaro, scuro o automatico",
|
||||
"color_mode_light": "Chiaro",
|
||||
"color_mode_dark": "Scuro",
|
||||
"color_mode_system": "Sistema",
|
||||
"week_start_title": "Inizio settimana",
|
||||
"week_start_description": "Primo giorno della settimana nei calendari",
|
||||
"week_start_monday": "Lunedì",
|
||||
"week_start_sunday": "Domenica",
|
||||
"sounds_title": "Suoni",
|
||||
"sounds_description": "Effetti sonori in tutte le app",
|
||||
"sounds_aria": "Attiva o disattiva i suoni",
|
||||
"onboarding_title": "Ripeti l'onboarding",
|
||||
"onboarding_description": "Scegli di nuovo nome, aspetto e moduli",
|
||||
"onboarding_start": "Avvia",
|
||||
"onboarding_starting": "Avvio…"
|
||||
},
|
||||
"ai": {
|
||||
"title": "Opzioni IA",
|
||||
"description": "Scegli quali livelli di IA Mana può usare — da nessuno a tutti",
|
||||
"tier_local_title": "Locale (senza IA)",
|
||||
"tier_local_badge": "sempre attivo",
|
||||
"tier_local_subtitle": "Funzioni di base senza alcuna IA",
|
||||
"tier_local_description": "Mana funziona anche completamente senza IA: rilevamento date, ricerca e classificazione semplice usano algoritmi classici. Alcune funzioni risultano limitate, ma tutto è 100 % offline e gratuito.",
|
||||
"tier_browser_title": "Sul tuo dispositivo",
|
||||
"tier_browser_subtitle": "Gemma 4 E2B (Google) nel browser",
|
||||
"tier_browser_bullet_1": "100 % locale — i dati non lasciano il tuo dispositivo",
|
||||
"tier_browser_bullet_2": "~500 MB di download una tantum",
|
||||
"tier_browser_bullet_3": "Richiede WebGPU + 2 GB di memoria GPU libera",
|
||||
"tier_browser_blocked": "WebGPU non disponibile nel tuo browser. Funziona in Chrome/Edge 113+ o Safari 18+.",
|
||||
"tier_browser_loaded": "✓ Modello caricato",
|
||||
"tier_browser_loading": "Caricamento di {name} ({percent} %)…",
|
||||
"tier_browser_load_btn": "Carica modello (~{size} MB)",
|
||||
"tier_browser_load_btn_loading": "Caricamento…",
|
||||
"tier_mana_server_title": "Server Mana",
|
||||
"tier_mana_server_subtitle": "Self-hosted sul nostro Mac Mini",
|
||||
"tier_mana_server_bullet_1": "Più veloce e potente del LLM nel browser",
|
||||
"tier_mana_server_bullet_2": "I dati viaggiano cifrati verso il nostro server",
|
||||
"tier_mana_server_bullet_3": "Nessun contenuto viene memorizzato",
|
||||
"tier_byok_title": "La tua API key",
|
||||
"tier_byok_subtitle": "OpenAI, Anthropic, Google Gemini o Mistral",
|
||||
"tier_byok_bullet_1": "Direttamente dal browser — niente server Mana di mezzo",
|
||||
"tier_byok_bullet_2": "Paghi tu il fornitore, noi non vediamo nulla",
|
||||
"tier_byok_bullet_3": "Le chiavi sono salvate cifrate nel tuo vault",
|
||||
"tier_cloud_title": "Google Gemini",
|
||||
"tier_cloud_subtitle": "API cloud tramite il nostro proxy",
|
||||
"tier_cloud_bullet_1": "Massima qualità per i compiti complessi",
|
||||
"tier_cloud_bullet_2": "Risposte più veloci",
|
||||
"tier_cloud_bullet_3": "I dati vengono elaborati da Google",
|
||||
"tier_cloud_warning": "Consigliato solo per contenuti non sensibili",
|
||||
"tier_cloud_consent_title": "Conferma necessaria",
|
||||
"tier_cloud_consent_desc": "Le richieste cloud inviano i tuoi contenuti a Google. Conferma di averlo capito e accettato.",
|
||||
"tier_cloud_consent_btn": "Capito, attiva il cloud",
|
||||
"tier_cloud_consent_ok": "✓ Consenso cloud concesso",
|
||||
"tier_cloud_consent_revoke": "Ritira",
|
||||
"badge_active": "attivo",
|
||||
"summary_label": "Ordine attuale:",
|
||||
"summary_local_only": "Solo locale (senza IA) — la maggior parte delle funzioni IA è limitata.",
|
||||
"summary_chain_local_fallback": "Locale (fallback)",
|
||||
"fallback_title": "Ripiegare su «Locale» in caso di errore",
|
||||
"fallback_description": "Se il livello IA scelto non riesce a rispondere, Mana prova la variante locale (se disponibile). Disattivato: mostra invece un errore.",
|
||||
"fallback_aria": "Fallback su locale",
|
||||
"show_source_title": "Mostra la fonte di ogni risultato IA",
|
||||
"show_source_description": "Mostra una piccola etichetta sotto ogni risposta generata dall'IA, come «Sul tuo dispositivo» o «via Google Gemini» — così vedi sempre dove sono stati elaborati i tuoi dati.",
|
||||
"show_source_aria": "Mostra fonte"
|
||||
},
|
||||
"byok": {
|
||||
"vault_locked": "Il vault è bloccato — accedi prima per gestire le chiavi.",
|
||||
"loading": "Caricamento…",
|
||||
"add_first": "Aggiungi prima API key",
|
||||
"add_more": "Aggiungi un'altra chiave",
|
||||
"field_provider": "Fornitore",
|
||||
"field_label": "Etichetta",
|
||||
"field_label_placeholder": "es. OpenAI privata",
|
||||
"field_api_key": "API key",
|
||||
"field_api_key_placeholder_generic": "API key",
|
||||
"field_model": "Modello",
|
||||
"field_model_default": "Predefinito ({model})",
|
||||
"field_default_short": "Predefinito",
|
||||
"field_as_default": "Come predefinita",
|
||||
"label_required": "Etichetta e API key sono obbligatorie",
|
||||
"cancel": "Annulla",
|
||||
"save": "Salva",
|
||||
"saving": "Salvataggio…",
|
||||
"confirm_delete": "Eliminare davvero la chiave?",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
"set_default": "Predefinita",
|
||||
"badge_default": "Predefinita",
|
||||
"meta_line": "{provider} · {model} · {count} chiamate · {cost}"
|
||||
},
|
||||
"security": {
|
||||
"title": "Sicurezza",
|
||||
"description": "Passkey, 2FA, cifratura e sessioni"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Panoramica privacy",
|
||||
"description": "Tutte le voci che mostri pubblicamente o condividi tramite link — con annullamento in un clic.",
|
||||
"summary_public": "pubblico",
|
||||
"summary_unlisted": "condivisibile via link",
|
||||
"loading": "Caricamento…",
|
||||
"empty": "Al momento nulla è pubblico o condiviso tramite link — ottimo lavoro.",
|
||||
"badge_public": "Pubblico",
|
||||
"badge_unlisted": "Via link",
|
||||
"open": "Apri",
|
||||
"set_private": "Privato",
|
||||
"kill_all": "Riporta tutto a privato",
|
||||
"confirm_one": "{count} voce verrà riportata a «Spazio». I link di condivisione attivi saranno revocati. Continuare?",
|
||||
"confirm_other": "{count} voci verranno riportate a «Spazio». I link di condivisione attivi saranno revocati. Continuare?",
|
||||
"cancel": "Annulla",
|
||||
"killing": "Ripristino in corso…",
|
||||
"confirm_kill": "Sì, ripristina tutto",
|
||||
"toast_set_private": "«{title}» è ora privato",
|
||||
"toast_set_private_failed": "Impossibile riportare «{title}» a privato",
|
||||
"toast_kill_partial": "{flipped} voci a privato — {failed} non riuscite",
|
||||
"toast_kill_done": "{flipped} voci riportate a privato",
|
||||
"toast_kill_failed": "Interruttore di emergenza fallito"
|
||||
},
|
||||
"sync": {
|
||||
"title": "Cloud Sync",
|
||||
"description": "Sincronizza i tuoi dati su tutti i dispositivi",
|
||||
"interval_monthly": "Mensile",
|
||||
"interval_quarterly": "Trimestrale",
|
||||
"interval_yearly": "Annuale",
|
||||
"interval_short_monthly": "Mese",
|
||||
"interval_short_quarterly": "Trimestre",
|
||||
"interval_short_yearly": "Anno",
|
||||
"interval_hint_monthly": "fatturato ogni mese · ~1 credito/giorno",
|
||||
"interval_hint_quarterly": "fatturato ogni 3 mesi",
|
||||
"interval_hint_yearly": "fatturato una volta l'anno",
|
||||
"toast_activated": "Cloud Sync attivato!",
|
||||
"toast_deactivated": "Cloud Sync disattivato",
|
||||
"toast_interval_changed": "Intervallo cambiato in {label}",
|
||||
"deactivate_confirm": "Disattivare davvero Cloud Sync? I tuoi dati restano in locale.",
|
||||
"err_activation_failed": "Attivazione fallita",
|
||||
"err_deactivation_failed": "Disattivazione fallita",
|
||||
"err_change_failed": "Modifica fallita",
|
||||
"status_label": "Stato",
|
||||
"status_next_charge": "Prossimo addebito il {date}",
|
||||
"status_encrypted": "Sincronizzato cifrato su tutti i dispositivi",
|
||||
"status_insufficient": "Crediti insufficienti — ricarica per continuare",
|
||||
"status_local_only": "I tuoi dati sono salvati solo localmente su questo dispositivo",
|
||||
"badge_active": "Attivo",
|
||||
"badge_paused": "In pausa",
|
||||
"badge_inactive": "Inattivo",
|
||||
"credits_available": "Crediti disponibili",
|
||||
"credits_topup": "Ricarica crediti",
|
||||
"interval_label": "Intervallo di fatturazione",
|
||||
"interval_price_hint": "{price} crediti · {hint}",
|
||||
"changing": "Modifica in corso…",
|
||||
"change_to": "Passa a {label}",
|
||||
"change_hint": "La modifica si applica dal prossimo addebito",
|
||||
"deactivating": "Disattivazione…",
|
||||
"deactivate": "Disattiva Cloud Sync",
|
||||
"activating": "Attivazione…",
|
||||
"activate": "Attiva · {price} crediti",
|
||||
"insufficient_warn_pre": "Crediti insufficienti.",
|
||||
"insufficient_topup": "Ricarica",
|
||||
"footnote": "Cloud Sync sincronizza i tuoi dati cifrati su tutti i dispositivi. I dati locali restano sempre — anche quando la sincronizzazione è in pausa o disattivata."
|
||||
},
|
||||
"mydata": {
|
||||
"title": "I miei dati (GDPR)",
|
||||
"description": "Panoramica di tutti i dati che memorizziamo su di te",
|
||||
"qr_export": "Esporta come QR",
|
||||
"qr_short": "QR",
|
||||
"export": "Esporta",
|
||||
"exporting": "Esportazione…",
|
||||
"retry": "Riprova",
|
||||
"no_name": "Nessun nome",
|
||||
"verified": "verificato",
|
||||
"not_verified": "non verificato",
|
||||
"registered_at": "Registrato il",
|
||||
"total_entities": "Entità totali",
|
||||
"total_entities_desc": "Record su tutte le app",
|
||||
"projects_with_data": "Progetti con dati",
|
||||
"footnote_pre": "Niente cookie di tracciamento — analitica anonima tramite Umami. Dettagli nell'",
|
||||
"footnote_link": "informativa sulla privacy",
|
||||
"footnote_post": ".",
|
||||
"auth_title": "Autenticazione",
|
||||
"auth_description": "Sessioni, account e 2FA",
|
||||
"auth_sessions": "Sessioni attive",
|
||||
"auth_accounts": "Account collegati",
|
||||
"auth_two_fa": "Due fattori (2FA)",
|
||||
"auth_two_fa_active": "Attivata",
|
||||
"auth_two_fa_inactive": "Disattivata",
|
||||
"auth_last_login": "Ultimo accesso",
|
||||
"credits_title": "Crediti",
|
||||
"credits_description": "Saldo e transazioni",
|
||||
"credits_balance": "Saldo attuale",
|
||||
"credits_total_earned": "Totale guadagnato",
|
||||
"credits_total_spent": "Totale speso",
|
||||
"credits_transactions": "Transazioni",
|
||||
"project_title": "Dati di progetto",
|
||||
"project_description": "Record per app",
|
||||
"col_app": "App",
|
||||
"col_count": "Voci",
|
||||
"col_time": "Ultima attività",
|
||||
"project_status_active": "Attivo",
|
||||
"project_status_empty": "Nessun dato",
|
||||
"project_status_unavailable": "Non disponibile",
|
||||
"project_unreachable": "non raggiungibile",
|
||||
"relative_just_now": "proprio ora",
|
||||
"relative_minutes_ago": "{n} min fa",
|
||||
"relative_hours_ago": "{n} h fa",
|
||||
"relative_days_ago": "{n} g fa",
|
||||
"retention_title": "Periodi di conservazione",
|
||||
"retention_description": "Per quanto tempo conserviamo i tuoi dati",
|
||||
"retention_account": "Account utente e profilo",
|
||||
"retention_account_value": "Fino all'eliminazione",
|
||||
"retention_sessions": "Sessioni e cronologia accessi",
|
||||
"retention_sessions_value": "90 giorni dalla scadenza",
|
||||
"retention_credit": "Transazioni di crediti",
|
||||
"retention_credit_value": "10 anni (legge)",
|
||||
"retention_security": "Registri di sicurezza",
|
||||
"retention_security_value": "1 anno",
|
||||
"retention_project": "Dati di progetto (chat, todo, …)",
|
||||
"retention_project_value": "Fino all'eliminazione",
|
||||
"danger_title": "Zona di pericolo",
|
||||
"danger_description": "Azioni irreversibili",
|
||||
"danger_delete_title": "Elimina tutti i miei dati",
|
||||
"danger_delete_desc": "Elimina definitivamente il tuo account e tutti i dati collegati in tutti i progetti. Non si può annullare.",
|
||||
"danger_delete_btn": "Elimina i dati"
|
||||
},
|
||||
"tag_presets": {
|
||||
"title": "Preset di tag",
|
||||
"hint": "Set di tag salvati che puoi scegliere come template iniziale quando crei un nuovo spazio. Le modifiche al preset non toccano gli spazi già creati — è una copia a senso unico.",
|
||||
"name_required": "Inserisci un nome",
|
||||
"no_active_space": "Nessuno spazio attivo",
|
||||
"name_placeholder": "Nome del preset (es. «Il mio set standard»)",
|
||||
"create_from": "Crea da «{name}»",
|
||||
"creating": "Creazione …",
|
||||
"loading_space": "Caricamento spazio …",
|
||||
"delete_confirm": "Eliminare il preset «{name}»?",
|
||||
"empty": "Ancora nessun preset. Crea il primo dando un nome al set di tag attuale — verrà usato automaticamente come predefinito per i nuovi spazi.",
|
||||
"badge_default": "Predefinito",
|
||||
"tag_count_one": "{count} tag",
|
||||
"tag_count_other": "{count} tag",
|
||||
"with_groups": "con gruppi",
|
||||
"aria_set_default": "Imposta come predefinito",
|
||||
"aria_delete": "Elimina"
|
||||
},
|
||||
"vault": {
|
||||
"title": "Cifratura",
|
||||
"description": "I campi sensibili vengono cifrati con AES-GCM-256 prima di essere scritti nel database locale.",
|
||||
"badge_encrypted": "Cifrato",
|
||||
"badge_locked": "Bloccato",
|
||||
"badge_recovery_required": "Codice di ripristino richiesto",
|
||||
"badge_auth_required": "Accesso richiesto",
|
||||
"badge_network_error": "Errore di rete",
|
||||
"badge_server_error": "Errore del server",
|
||||
"badge_unknown_error": "Errore sconosciuto",
|
||||
"status_unlocked_pre": "La tua chiave personale è caricata su questo dispositivo.",
|
||||
"status_unlocked_fields": "{count} campi",
|
||||
"status_unlocked_tables": "{count} tabelle",
|
||||
"status_unlocked_post": "vengono salvati cifrati.",
|
||||
"status_locked": "La tua chiave non è caricata. I contenuti cifrati non possono essere letti finché non accedi di nuovo o carichi la chiave manualmente.",
|
||||
"status_load_now": "Carica la chiave ora",
|
||||
"status_error": "Si è verificato un problema durante il caricamento della chiave di cifratura. Accedi di nuovo o controlla la connessione internet.",
|
||||
"status_retry": "Riprova",
|
||||
"toast_unlocked": "Chiave di cifratura caricata",
|
||||
"toast_unlock_failed": "Impossibile caricare la chiave: {status}",
|
||||
"toast_rotated": "Chiave ruotata. Le nuove scritture useranno la nuova chiave.",
|
||||
"toast_rotate_failed": "Rotazione fallita: {status}",
|
||||
"toast_zk_enabled": "Modalità zero-knowledge attivata",
|
||||
"toast_zk_disabled": "Modalità zero-knowledge disattivata",
|
||||
"toast_recovery_cleared": "Codice di ripristino rimosso",
|
||||
"toast_clipboard_ok": "Codice copiato negli appunti",
|
||||
"toast_clipboard_failed": "Impossibile copiare il codice",
|
||||
"err_code_mismatch": "Il codice inserito non corrisponde a quello mostrato.",
|
||||
"threat_title": "Cosa può vedere Mana",
|
||||
"threat_never_label": "Cosa Mana non vede mai:",
|
||||
"threat_never_body": "i tuoi contenuti cifrati (chat, note, sogni, memo, dettagli dei contatti, note del ciclo, descrizioni delle transazioni, …). Lasciano il tuo dispositivo solo come blob illeggibile.",
|
||||
"threat_could_label": "Cosa Mana potrebbe tecnicamente decifrare:",
|
||||
"threat_could_body": "la tua master key, se un dipendente con accesso alla chiave di cifratura delle chiavi vi accede attivamente. In pratica è protetto contro tutte le minacce realistiche, eccetto un'imposizione giudiziaria contro Mana stessa.",
|
||||
"threat_visible_label": "Cosa resta visibile strutturalmente:",
|
||||
"threat_visible_body": "il numero di note / chat / contatti, i timestamp, i collegamenti tra record. Il contenuto in sé no.",
|
||||
"fields_title": "Campi cifrati",
|
||||
"fields_meta": "{fields} campi · {tables} tabelle",
|
||||
"fields_description": "Quali colonne in quali tabelle sono salvate cifrate sul dispositivo. I metadati strutturali (ID, timestamp, flag di stato) restano deliberatamente in chiaro affinché indici, ordinamento e sincronizzazione continuino a funzionare.",
|
||||
"show_more": "Mostra altri {count}",
|
||||
"show_less": "Mostra meno",
|
||||
"zk_title": "Modalità zero-knowledge",
|
||||
"zk_state_active": "Attiva",
|
||||
"zk_state_inactive": "Non attiva",
|
||||
"zk_desc_optional": "Opzionale, avanzata.",
|
||||
"zk_desc_body_pre": " In modalità zero-knowledge, Mana memorizza la tua chiave ",
|
||||
"zk_desc_body_em": "solo in una forma che neanche noi possiamo decifrare",
|
||||
"zk_desc_body_post": ". Al login da un nuovo dispositivo ti servirà il codice di ripristino per sbloccare i tuoi dati.",
|
||||
"zk_pro_label": "Vantaggio:",
|
||||
"zk_pro_body": " nemmeno un dipendente Mana con pieno accesso al server può più leggere i tuoi contenuti.",
|
||||
"zk_con_label": "Rischio:",
|
||||
"zk_con_body": " se perdi il codice di ripristino, i tuoi dati sono persi in modo irreversibile — non avremo più chiavi di backup.",
|
||||
"zk_existing_warn": "Hai già un codice di ripristino salvato, ma la modalità zero-knowledge non è ancora attiva. Puoi attivarla direttamente o resettare il codice.",
|
||||
"zk_enable_now": "Attiva zero-knowledge ora",
|
||||
"zk_enabling": "Attivazione …",
|
||||
"zk_generate_new": "Genera nuovo codice di ripristino",
|
||||
"zk_clear": "Rimuovi codice di ripristino",
|
||||
"zk_clear_confirm": "Sì, rimuovi",
|
||||
"zk_clearing": "Rimozione …",
|
||||
"zk_setup": "Configura codice di ripristino",
|
||||
"zk_generating": "Generazione …",
|
||||
"step_1_title": "Salva il codice in modo sicuro",
|
||||
"step_1_desc_pre": "Salva questo codice in un posto sicuro (password manager, stampato in cassaforte, …). ",
|
||||
"step_1_desc_strong": "Lo mostriamo solo una volta.",
|
||||
"copy": "Copia",
|
||||
"step_1_continue": "Ho salvato il codice →",
|
||||
"step_2_title": "Reinserisci il codice",
|
||||
"step_2_desc": "Digita (o incolla) il codice che hai appena salvato. Così ci assicuriamo che il backup sia davvero completo.",
|
||||
"step_2_placeholder": "1A2B-3C4D-5E6F-...",
|
||||
"step_2_confirm": "Conferma codice",
|
||||
"step_3_title": "Attiva la modalità zero-knowledge",
|
||||
"step_3_desc_pre": "Se attivi ora, il server cancella la sua copia della tua chiave. Da questo momento potrai ",
|
||||
"step_3_desc_strong": "accedere ai tuoi dati cifrati solo con il codice di ripristino",
|
||||
"step_3_desc_post": ".",
|
||||
"step_3_warn": "Questa azione non è annullabile senza il codice di ripristino. Se perdi il codice, i tuoi contenuti sono persi.",
|
||||
"step_3_confirm": "Sì, attiva la modalità zero-knowledge",
|
||||
"cancel": "Annulla",
|
||||
"zk_active_note": "Il server non può più decifrare i tuoi dati. Al prossimo login da un nuovo dispositivo ti chiederemo il codice di ripristino.",
|
||||
"rotate_step_title": "Nuovo codice di ripristino",
|
||||
"rotate_step_strong": "Il tuo vecchio codice non è più valido.",
|
||||
"rotate_step_desc": " Salva il nuovo codice in un posto sicuro prima di lasciare questa pagina — lo mostriamo solo una volta.",
|
||||
"rotate_step_save": "Ho salvato il nuovo codice",
|
||||
"rotate_btn": "Ruota codice di ripristino",
|
||||
"rotate_busy": "Rotazione …",
|
||||
"zk_disable_btn": "Disattiva la modalità zero-knowledge …",
|
||||
"zk_disable_desc": "Per ripristinare la chiave del server abbiamo bisogno della tua master key attualmente caricata. Si trova nel tuo browser — la inviamo una volta al server, che la riavvolge con il KEK.",
|
||||
"zk_disable_confirm": "Sì, disattiva",
|
||||
"zk_disabling": "Disattivazione …",
|
||||
"rotate_section_title": "Ruota la chiave",
|
||||
"rotate_section_warn_strong": "Attenzione:",
|
||||
"rotate_section_warn_body_pre": " Durante la rotazione viene generata una nuova chiave. I dati cifrati con la vecchia chiave non saranno più leggibili — a meno che non siano stati prima decifrati e riscritti con la nuova chiave. Mana attualmente ",
|
||||
"rotate_section_warn_em": "non esegue automaticamente questo ri-cifraggio",
|
||||
"rotate_section_warn_post": ".",
|
||||
"rotate_section_when": "Quando usarlo? Se sospetti che il tuo dispositivo sia stato compromesso, o come pratica di sicurezza regolare dopo un accesso da un luogo sconosciuto.",
|
||||
"rotate_section_btn": "Ruota la chiave …",
|
||||
"rotate_section_confirm": "Sì, ruota ora",
|
||||
"rotate_section_busy": "Rotazione …"
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
import { APP_VERSION } from '$lib/version';
|
||||
import SettingsSidebar from '$lib/components/settings/SettingsSidebar.svelte';
|
||||
import {
|
||||
categories,
|
||||
findCategoryByAnchor,
|
||||
type CategoryId,
|
||||
type SearchEntry,
|
||||
} from '$lib/components/settings/searchIndex';
|
||||
|
|
@ -23,8 +23,8 @@
|
|||
|
||||
function navigateToHash(hash: string) {
|
||||
if (!hash) return;
|
||||
const cat = categories.find((c) => c.anchors.includes(hash));
|
||||
if (cat) activeCategory = cat.id;
|
||||
const cat = findCategoryByAnchor(hash);
|
||||
if (cat) activeCategory = cat;
|
||||
void tick().then(() => {
|
||||
const el = document.getElementById(hash);
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue