feat: Add internationalization (i18n) support to frontend

This commit introduces internationalization (i18n) to the frontend using the `sveltekit-i18n` library, allowing the user interface to be translated into multiple languages.

Key changes:
- Added translation files for 10 languages (en, de, es, fr, etc.).
- Replaced hardcoded text strings throughout the frontend components and pages with translation keys.
- Added a language selector to the system settings page, allowing administrators to set the default application language.
- Updated the backend settings API to store and expose the new language configuration.
This commit is contained in:
Wayne
2025-08-28 19:22:21 +03:00
parent 159f7d8777
commit 67cef40e5c
38 changed files with 2791 additions and 266 deletions

View File

@@ -1,5 +1,6 @@
import type { Request, Response } from 'express';
import { SettingsService } from '../../services/SettingsService';
import { SystemSettings } from '@open-archiver/types';
const settingsService = new SettingsService();

View File

@@ -8,6 +8,9 @@ export const createSettingsRouter = (authService: AuthService): Router => {
const router = Router();
// Public route to get non-sensitive settings. settings read should not be scoped with a permission because all end users need the settings data in the frontend. However, for sensitive settings data, we need to add a new permission subject to limit access. So this route should only expose non-sensitive settings data.
/**
* @returns SystemSettings
*/
router.get('/', settingsController.getSettings);
// Protected route to update settings

View File

@@ -23,6 +23,7 @@
"lucide-svelte": "^0.525.0",
"postal-mime": "^2.4.4",
"svelte-persisted-store": "^0.12.0",
"sveltekit-i18n": "^2.4.2",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0"
},

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import PostalMime, { type Email } from 'postal-mime';
import type { Buffer } from 'buffer';
import { t } from '$lib/translations';
let {
raw,
@@ -51,13 +52,16 @@
<div class="mt-2 rounded-md border bg-white p-4">
{#if isLoading}
<p>Loading email preview...</p>
<p>{$t('components.email_preview.loading')}</p>
{:else if emailHtml}
<iframe title="Email Preview" srcdoc={emailHtml()} class="h-[600px] w-full border-none"
<iframe
title={$t('archive.email_preview')}
srcdoc={emailHtml()}
class="h-[600px] w-full border-none"
></iframe>
{:else if raw}
<p>Could not render email preview.</p>
<p>{$t('components.email_preview.render_error')}</p>
{:else}
<p class="text-gray-500">Raw .eml file not available for this email.</p>
<p class="text-gray-500">{$t('components.email_preview.not_available')}</p>
{/if}
</div>

View File

@@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import type { ArchivedEmail } from '@open-archiver/types';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { t } from '$lib/translations';
let {
thread,
@@ -47,16 +48,16 @@
goto(`/dashboard/archived-emails/${item.id}`, {
invalidateAll: true,
});
}}>{item.subject || 'No Subject'}</a
}}>{item.subject || $t('archive.no_subject')}</a
>
{:else}
{item.subject || 'No Subject'}
{item.subject || $t('archive.no_subject')}
{/if}
</h4>
<div
class="flex flex-col space-y-2 text-sm font-normal leading-none text-gray-400"
>
<span>From: {item.senderEmail}</span>
<span>{$t('archive.from')}: {item.senderEmail}</span>
<time class="">{new Date(item.sentAt).toLocaleString()}</time>
</div>
</div>

View File

@@ -1,12 +1,17 @@
<footer class=" bg-muted py-6 md:py-0">
<script lang="ts">
import { t } from '$lib/translations';
</script>
<footer class="bg-muted py-6 md:py-0">
<div
class="container mx-auto flex flex-col items-center justify-center gap-4 md:h-24 md:flex-row"
>
<div class="flex flex-col items-center gap-2">
<p class=" text-balance text-center text-xs font-medium leading-loose">
<p class="text-balance text-center text-xs font-medium leading-loose">
© {new Date().getFullYear()}
<a href="https://openarchiver.com/" target="_blank">Open Archiver</a>. All rights
reserved.
<a href="https://openarchiver.com/" target="_blank">Open Archiver</a>. {$t(
'app.components.footer.all_rights_reserved'
)}
</p>
</div>
</div>

View File

@@ -11,6 +11,7 @@
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import { api } from '$lib/api.client';
import { Loader2 } from 'lucide-svelte';
import { t } from '$lib/translations';
let {
source = null,
onSubmit,
@@ -20,11 +21,20 @@
} = $props();
const providerOptions = [
{ value: 'generic_imap', label: 'Generic IMAP' },
{ value: 'google_workspace', label: 'Google Workspace' },
{ value: 'microsoft_365', label: 'Microsoft 365' },
{ value: 'pst_import', label: 'PST Import' },
{ value: 'eml_import', label: 'EML Import' },
{
value: 'generic_imap',
label: $t('components.ingestion_source_form.provider_generic_imap'),
},
{
value: 'google_workspace',
label: $t('components.ingestion_source_form.provider_google_workspace'),
},
{
value: 'microsoft_365',
label: $t('components.ingestion_source_form.provider_microsoft_365'),
},
{ value: 'pst_import', label: $t('components.ingestion_source_form.provider_pst_import') },
{ value: 'eml_import', label: $t('components.ingestion_source_form.provider_eml_import') },
];
let formData: CreateIngestionSourceDto = $state({
@@ -42,7 +52,8 @@
});
const triggerContent = $derived(
providerOptions.find((p) => p.value === formData.provider)?.label ?? 'Select a provider'
providerOptions.find((p) => p.value === formData.provider)?.label ??
$t('components.ingestion_source_form.select_provider')
);
let isSubmitting = $state(false);
@@ -89,7 +100,7 @@
fileUploading = false;
setAlert({
type: 'error',
title: 'Upload Failed, please try again',
title: $t('components.ingestion_source_form.upload_failed'),
message: JSON.stringify(error),
duration: 5000,
show: true,
@@ -100,11 +111,11 @@
<form onsubmit={handleSubmit} class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-left">Name</Label>
<Label for="name" class="text-left">{$t('ingestions.name')}</Label>
<Input id="name" bind:value={formData.name} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="provider" class="text-left">Provider</Label>
<Label for="provider" class="text-left">{$t('ingestions.provider')}</Label>
<Select.Root name="provider" bind:value={formData.provider} type="single">
<Select.Trigger class="col-span-3">
{triggerContent}
@@ -119,16 +130,20 @@
{#if formData.provider === 'google_workspace'}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="serviceAccountKeyJson" class="text-left">Service Account Key (JSON)</Label>
<Label for="serviceAccountKeyJson" class="text-left"
>{$t('components.ingestion_source_form.service_account_key')}</Label
>
<Textarea
placeholder="Paste your service account key JSON content"
placeholder={$t('components.ingestion_source_form.service_account_key_placeholder')}
id="serviceAccountKeyJson"
bind:value={formData.providerConfig.serviceAccountKeyJson}
class="col-span-3 max-h-32"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="impersonatedAdminEmail" class="text-left">Impersonated Admin Email</Label>
<Label for="impersonatedAdminEmail" class="text-left"
>{$t('components.ingestion_source_form.impersonated_admin_email')}</Label
>
<Input
id="impersonatedAdminEmail"
bind:value={formData.providerConfig.impersonatedAdminEmail}
@@ -137,30 +152,38 @@
</div>
{:else if formData.provider === 'microsoft_365'}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="clientId" class="text-left">Application (Client) ID</Label>
<Label for="clientId" class="text-left"
>{$t('components.ingestion_source_form.client_id')}</Label
>
<Input id="clientId" bind:value={formData.providerConfig.clientId} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="clientSecret" class="text-left">Client Secret Value</Label>
<Label for="clientSecret" class="text-left"
>{$t('components.ingestion_source_form.client_secret')}</Label
>
<Input
id="clientSecret"
type="password"
placeholder="Enter the secret Value, not the Secret ID"
placeholder={$t('components.ingestion_source_form.client_secret_placeholder')}
bind:value={formData.providerConfig.clientSecret}
class="col-span-3"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="tenantId" class="text-left">Directory (Tenant) ID</Label>
<Label for="tenantId" class="text-left"
>{$t('components.ingestion_source_form.tenant_id')}</Label
>
<Input id="tenantId" bind:value={formData.providerConfig.tenantId} class="col-span-3" />
</div>
{:else if formData.provider === 'generic_imap'}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="host" class="text-left">Host</Label>
<Label for="host" class="text-left">{$t('components.ingestion_source_form.host')}</Label
>
<Input id="host" bind:value={formData.providerConfig.host} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="port" class="text-left">Port</Label>
<Label for="port" class="text-left">{$t('components.ingestion_source_form.port')}</Label
>
<Input
id="port"
type="number"
@@ -169,11 +192,13 @@
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="username" class="text-left">Username</Label>
<Label for="username" class="text-left"
>{$t('components.ingestion_source_form.username')}</Label
>
<Input id="username" bind:value={formData.providerConfig.username} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="password" class="text-left">Password</Label>
<Label for="password" class="text-left">{$t('auth.password')}</Label>
<Input
id="password"
type="password"
@@ -182,12 +207,16 @@
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="secure" class="text-left">Use TLS</Label>
<Label for="secure" class="text-left"
>{$t('components.ingestion_source_form.use_tls')}</Label
>
<Checkbox id="secure" bind:checked={formData.providerConfig.secure} />
</div>
{:else if formData.provider === 'pst_import'}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="pst-file" class="text-left">PST File</Label>
<Label for="pst-file" class="text-left"
>{$t('components.ingestion_source_form.pst_file')}</Label
>
<div class="col-span-3 flex flex-row items-center space-x-2">
<Input
id="pst-file"
@@ -203,7 +232,9 @@
</div>
{:else if formData.provider === 'eml_import'}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="eml-file" class="text-left">EML File</Label>
<Label for="eml-file" class="text-left"
>{$t('components.ingestion_source_form.eml_file')}</Label
>
<div class="col-span-3 flex flex-row items-center space-x-2">
<Input
id="eml-file"
@@ -220,12 +251,10 @@
{/if}
{#if formData.provider === 'google_workspace' || formData.provider === 'microsoft_365'}
<Alert.Root>
<Alert.Title>Heads up!</Alert.Title>
<Alert.Title>{$t('components.ingestion_source_form.heads_up')}</Alert.Title>
<Alert.Description>
<div class="my-1">
Please note that this is an organization-wide operation. This kind of ingestions
will import and index <b>all</b> email inboxes in your organization. If you want
to import only specific email inboxes, use the IMAP connector.
{@html $t('components.ingestion_source_form.org_wide_warning')}
</div>
</Alert.Description>
</Alert.Root>
@@ -233,9 +262,9 @@
<Dialog.Footer>
<Button type="submit" disabled={isSubmitting || fileUploading}>
{#if isSubmitting}
Submitting...
{$t('components.common.submitting')}
{:else}
Submit
{$t('components.common.submit')}
{/if}
</Button>
</Dialog.Footer>

View File

@@ -4,6 +4,7 @@
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label';
import { t } from '$lib/translations';
let { role, onSubmit }: { role: Role | null; onSubmit: (formData: Partial<Role>) => void } =
$props();
@@ -16,7 +17,7 @@
const parsedPolicies: CaslPolicy[] = JSON.parse(policies);
onSubmit({ name, policies: parsedPolicies });
} catch (error) {
alert('Invalid JSON format for policies.');
alert($t('components.role_form.invalid_json'));
}
};
</script>
@@ -29,11 +30,11 @@
class="grid gap-4 py-4"
>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="name" class="text-right">Name</Label>
<Label for="name" class="text-right">{$t('roles.name')}</Label>
<Input id="name" bind:value={name} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="policies" class="text-right">Policies (JSON)</Label>
<Label for="policies" class="text-right">{$t('components.role_form.policies_json')}</Label>
<Textarea
id="policies"
bind:value={policies}
@@ -42,6 +43,6 @@
/>
</div>
<div class="flex justify-end">
<Button type="submit">Save</Button>
<Button type="submit">{$t('components.common.save')}</Button>
</div>
</form>

View File

@@ -3,6 +3,7 @@
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Sun, Moon, Laptop } from 'lucide-svelte';
import { t } from '$lib/translations';
</script>
<DropdownMenu.Root>
@@ -14,21 +15,21 @@
<Moon
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">Toggle theme</span>
<span class="sr-only">{$t('app.components.theme_switcher.toggle_theme')}</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => ($theme = 'light')}>
<Sun class="mr-2 h-4 w-4" />
<span>Light</span>
<span>{$t('app.system_settings.light')}</span>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => ($theme = 'dark')}>
<Moon class="mr-2 h-4 w-4" />
<span>Dark</span>
<span>{$t('app.system_settings.dark')}</span>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => ($theme = 'system')}>
<Laptop class="mr-2 h-4 w-4" />
<span>System</span>
<span>{$t('app.system_settings.system')}</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -5,6 +5,7 @@
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import * as Dialog from '$lib/components/ui/dialog';
import { t } from '$lib/translations';
let {
user = null,
@@ -25,7 +26,7 @@
});
const triggerContent = $derived(
roles.find((r) => r.id === formData.roleId)?.name ?? 'Select a role'
roles.find((r) => r.id === formData.roleId)?.name ?? $t('components.user_form.select_role')
);
let isSubmitting = $state(false);
@@ -53,20 +54,20 @@
<form onsubmit={handleSubmit} class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="first_name" class="text-left">First Name</Label>
<Label for="first_name" class="text-left">{$t('setup.first_name')}</Label>
<Input id="first_name" bind:value={formData.first_name} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="last_name" class="text-left">Last Name</Label>
<Label for="last_name" class="text-left">{$t('setup.last_name')}</Label>
<Input id="last_name" bind:value={formData.last_name} class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="email" class="text-left">Email</Label>
<Label for="email" class="text-left">{$t('users.email')}</Label>
<Input id="email" type="email" bind:value={formData.email} class="col-span-3" />
</div>
{#if !user}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="password" class="text-left">Password</Label>
<Label for="password" class="text-left">{$t('auth.password')}</Label>
<Input
id="password"
type="password"
@@ -76,7 +77,7 @@
</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="role" class="text-left">Role</Label>
<Label for="role" class="text-left">{$t('users.role')}</Label>
<Select.Root name="role" bind:value={formData.roleId} type="single">
<Select.Trigger class="col-span-3">
{triggerContent}
@@ -92,9 +93,9 @@
<Dialog.Footer>
<Button type="submit" disabled={isSubmitting}>
{#if isSubmitting}
Submitting...
{$t('components.common.submitting')}
{:else}
Submit
{$t('components.common.submit')}
{/if}
</Button>
</Dialog.Footer>

View File

@@ -3,12 +3,13 @@
import { AreaChart } from 'layerchart';
import { curveMonotoneX } from 'd3-shape';
import type { ChartConfig } from '$lib/components/ui/chart';
import { t } from '$lib/translations';
export let data: { date: Date; count: number }[];
const chartConfig = {
count: {
label: 'Emails Ingested',
label: $t('app.components.charts.emails_ingested'),
color: 'var(--chart-1)',
},
} satisfies ChartConfig;

View File

@@ -4,12 +4,13 @@
import type { IngestionSourceStats } from '@open-archiver/types';
import type { ChartConfig } from '$lib/components/ui/chart';
import { formatBytes } from '$lib/utils';
import { t } from '$lib/translations';
export let data: IngestionSourceStats[];
const chartConfig = {
storageUsed: {
label: 'Storage Used',
label: $t('app.components.charts.storage_used'),
},
} satisfies ChartConfig;
</script>

View File

@@ -3,12 +3,13 @@
import { BarChart } from 'layerchart';
import type { TopSender } from '@open-archiver/types';
import type { ChartConfig } from '$lib/components/ui/chart';
import { t } from '$lib/translations';
export let data: TopSender[];
const chartConfig = {
count: {
label: 'Emails',
label: $t('app.components.charts.emails'),
},
} satisfies ChartConfig;
</script>

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Anmelden",
"login_tip": "Geben Sie unten Ihre E-Mail-Adresse ein, um sich bei Ihrem Konto anzumelden.",
"email": "Email",
"password": "Passwort"
},
"common": {
"working": "Arbeiten"
},
"archive": {
"title": "Archiv",
"no_subject": "Kein Betreff",
"from": "Von",
"sent": "Gesendet",
"recipients": "Empfänger",
"to": "An",
"meta_data": "Metadaten",
"folder": "Ordner",
"tags": "Tags",
"size": "Größe",
"email_preview": "E-Mail-Vorschau",
"attachments": "Anhänge",
"download": "Herunterladen",
"actions": "Aktionen",
"download_eml": "E-Mail herunterladen (.eml)",
"delete_email": "E-Mail löschen",
"email_thread": "E-Mail-Thread",
"delete_confirmation_title": "Möchten Sie diese E-Mail wirklich löschen?",
"delete_confirmation_description": "Diese Aktion kann nicht rückgängig gemacht werden und entfernt die E-Mail und ihre Anhänge dauerhaft.",
"deleting": "Löschen",
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"not_found": "E-Mail nicht gefunden."
},
"ingestions": {
"title": "Erfassungsquellen",
"ingestion_sources": "Erfassungsquellen",
"bulk_actions": "Massenaktionen",
"force_sync": "Synchronisierung erzwingen",
"delete": "Löschen",
"create_new": "Neu erstellen",
"name": "Name",
"provider": "Anbieter",
"status": "Status",
"active": "Aktiv",
"created_at": "Erstellt am",
"actions": "Aktionen",
"last_sync_message": "Letzte Synchronisierungsnachricht",
"empty": "Leer",
"open_menu": "Menü öffnen",
"edit": "Bearbeiten",
"create": "Erstellen",
"ingestion_source": "Erfassungsquelle",
"edit_description": "Nehmen Sie hier Änderungen an Ihrer Erfassungsquelle vor.",
"create_description": "Fügen Sie eine neue Erfassungsquelle hinzu, um mit der Archivierung von E-Mails zu beginnen.",
"read": "Lesen",
"docs_here": "Dokumente hier",
"delete_confirmation_title": "Möchten Sie diese Erfassung wirklich löschen?",
"delete_confirmation_description": "Dadurch werden alle archivierten E-Mails, Anhänge, Indizierungen und Dateien, die mit dieser Erfassung verknüpft sind, gelöscht. Wenn Sie nur die Synchronisierung neuer E-Mails beenden möchten, können Sie stattdessen die Erfassung anhalten.",
"deleting": "Löschen",
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"bulk_delete_confirmation_title": "Möchten Sie wirklich {{count}} ausgewählte Erfassungen löschen?",
"bulk_delete_confirmation_description": "Dadurch werden alle archivierten E-Mails, Anhänge, Indizierungen und Dateien, die mit diesen Erfassungen verknüpft sind, gelöscht. Wenn Sie nur die Synchronisierung neuer E-Mails beenden möchten, können Sie stattdessen die Erfassungen anhalten."
},
"search": {
"title": "Suche",
"description": "Suchen Sie nach archivierten E-Mails.",
"email_search": "E-Mail-Suche",
"placeholder": "Suche nach Stichwort, Absender, Empfänger...",
"search_button": "Suche",
"search_options": "Suchoptionen",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Wörtlich",
"strategy_frequency": "Frequenz",
"select_strategy": "Wählen Sie eine Strategie",
"error": "Fehler",
"found_results_in": "{{total}} Ergebnisse in {{seconds}}s gefunden",
"found_results": "{{total}} Ergebnisse gefunden",
"from": "Von",
"to": "An",
"in_email_body": "Im E-Mail-Text",
"in_attachment": "Im Anhang: {{filename}}",
"prev": "Zurück",
"next": "Weiter"
},
"roles": {
"title": "Rollenverwaltung",
"role_management": "Rollenverwaltung",
"create_new": "Neu erstellen",
"name": "Name",
"created_at": "Erstellt am",
"actions": "Aktionen",
"open_menu": "Menü öffnen",
"view_policy": "Richtlinie anzeigen",
"edit": "Bearbeiten",
"delete": "Löschen",
"no_roles_found": "Keine Rollen gefunden.",
"role_policy": "Rollenrichtlinie",
"viewing_policy_for_role": "Richtlinie für Rolle anzeigen: {{name}}",
"create": "Erstellen",
"role": "Rolle",
"edit_description": "Nehmen Sie hier Änderungen an der Rolle vor.",
"create_description": "Fügen Sie dem System eine neue Rolle hinzu.",
"delete_confirmation_title": "Möchten Sie diese Rolle wirklich löschen?",
"delete_confirmation_description": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird die Rolle dauerhaft gelöscht.",
"deleting": "Löschen",
"confirm": "Bestätigen",
"cancel": "Abbrechen"
},
"system_settings": {
"title": "Systemeinstellungen",
"system_settings": "Systemeinstellungen",
"description": "Globale Anwendungseinstellungen verwalten.",
"language": "Sprache",
"default_theme": "Standardthema",
"light": "Hell",
"dark": "Dunkel",
"system": "System",
"support_email": "Support-E-Mail",
"saving": "Speichern",
"save_changes": "Änderungen speichern"
},
"users": {
"title": "Benutzerverwaltung",
"user_management": "Benutzerverwaltung",
"create_new": "Neu erstellen",
"name": "Name",
"email": "Email",
"role": "Rolle",
"created_at": "Erstellt am",
"actions": "Aktionen",
"open_menu": "Menü öffnen",
"edit": "Bearbeiten",
"delete": "Löschen",
"no_users_found": "Keine Benutzer gefunden.",
"create": "Erstellen",
"user": "Benutzer",
"edit_description": "Nehmen Sie hier Änderungen am Benutzer vor.",
"create_description": "Fügen Sie dem System einen neuen Benutzer hinzu.",
"delete_confirmation_title": "Möchten Sie diesen Benutzer wirklich löschen?",
"delete_confirmation_description": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch wird der Benutzer dauerhaft gelöscht und seine Daten von unseren Servern entfernt.",
"deleting": "Löschen",
"confirm": "Bestätigen",
"cancel": "Abbrechen"
},
"setup": {
"title": "Einrichtung",
"description": "Richten Sie das anfängliche Administratorkonto für Open Archiver ein.",
"welcome": "Willkommen",
"create_admin_account": "Erstellen Sie das erste Administratorkonto, um loszulegen.",
"first_name": "Vorname",
"last_name": "Nachname",
"email": "Email",
"password": "Passwort",
"creating_account": "Konto wird erstellt",
"create_account": "Konto erstellen"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Erfassungen",
"archived_emails": "Archivierte E-Mails",
"search": "Suche",
"settings": "Einstellungen",
"system": "System",
"users": "Benutzer",
"roles": "Rollen",
"logout": "Abmelden"
},
"components": {
"charts": {
"emails_ingested": "E-Mails aufgenommen",
"storage_used": "Speicher verwendet",
"emails": "E-Mails"
},
"common": {
"submitting": "Übermittlung...",
"submit": "Übermitteln",
"save": "Speichern"
},
"email_preview": {
"loading": "E-Mail-Vorschau wird geladen...",
"render_error": "E-Mail-Vorschau konnte nicht gerendert werden.",
"not_available": "Rohe .eml-Datei für diese E-Mail nicht verfügbar."
},
"footer": {
"all_rights_reserved": "Alle Rechte vorbehalten."
},
"ingestion_source_form": {
"provider_generic_imap": "Generisches IMAP",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "PST-Import",
"provider_eml_import": "EML-Import",
"select_provider": "Wählen Sie einen Anbieter",
"service_account_key": "Dienstkontoschlüssel (JSON)",
"service_account_key_placeholder": "Fügen Sie den JSON-Inhalt Ihres Dienstkontoschlüssels ein",
"impersonated_admin_email": "Impersonierte Admin-E-Mail",
"client_id": "Anwendungs-(Client-)ID",
"client_secret": "Client-Geheimniswert",
"client_secret_placeholder": "Geben Sie den Geheimniswert ein, nicht die Geheimnis-ID",
"tenant_id": "Verzeichnis-(Mandanten-)ID",
"host": "Host",
"port": "Port",
"username": "Benutzername",
"use_tls": "TLS verwenden",
"pst_file": "PST-Datei",
"eml_file": "EML-Datei",
"heads_up": "Achtung!",
"org_wide_warning": "Bitte beachten Sie, dass dies ein organisationsweiter Vorgang ist. Diese Art von Erfassungen importiert und indiziert <b>alle</b> E-Mail-Postfächer in Ihrer Organisation. Wenn Sie nur bestimmte E-Mail-Postfächer importieren möchten, verwenden Sie den IMAP-Connector.",
"upload_failed": "Hochladen fehlgeschlagen, bitte versuchen Sie es erneut"
},
"role_form": {
"policies_json": "Richtlinien (JSON)",
"invalid_json": "Ungültiges JSON-Format für Richtlinien."
},
"theme_switcher": {
"toggle_theme": "Thema umschalten"
},
"user_form": {
"select_role": "Wählen Sie eine Rolle aus"
}
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Übersicht über Ihr E-Mail-Archiv.",
"header": "Dashboard",
"create_ingestion": "Erfassung erstellen",
"no_ingestion_header": "Sie haben keine Erfassungsquelle eingerichtet.",
"no_ingestion_text": "Fügen Sie eine Erfassungsquelle hinzu, um mit der Archivierung Ihrer Posteingänge zu beginnen.",
"total_emails_archived": "Insgesamt archivierte E-Mails",
"total_storage_used": "Insgesamt genutzter Speicherplatz",
"failed_ingestions": "Fehlgeschlagene Erfassungen (letzte 7 Tage)",
"ingestion_history": "Erfassungsverlauf",
"no_ingestion_history": "Kein Erfassungsverlauf verfügbar.",
"storage_by_source": "Speicher nach Erfassungsquelle",
"no_ingestion_sources": "Keine Erfassungsquellen verfügbar.",
"indexed_insights": "Indizierte Einblicke",
"top_10_senders": "Top 10 Absender",
"no_indexed_insights": "Keine indizierten Einblicke verfügbar."
}
}
}

View File

@@ -0,0 +1,260 @@
{
"app": {
"auth": {
"login": "Login",
"login_tip": "Enter your email below to login to your account.",
"email": "Email",
"password": "Password"
},
"common": {
"working": "Working"
},
"archive": {
"title": "Archive",
"no_subject": "No Subject",
"from": "From",
"sent": "Sent",
"recipients": "Recipients",
"to": "To",
"meta_data": "Meta Data",
"folder": "Folder",
"tags": "Tags",
"size": "Size",
"email_preview": "Email Preview",
"attachments": "Attachments",
"download": "Download",
"actions": "Actions",
"download_eml": "Download Email (.eml)",
"delete_email": "Delete Email",
"email_thread": "Email Thread",
"delete_confirmation_title": "Are you sure you want to delete this email?",
"delete_confirmation_description": "This action cannot be undone and will permanently remove the email and its attachments.",
"deleting": "Deleting",
"confirm": "Confirm",
"cancel": "Cancel",
"not_found": "Email not found."
},
"ingestions": {
"title": "Ingestion Sources",
"ingestion_sources": "Ingestion Sources",
"bulk_actions": "Bulk Actions",
"force_sync": "Force Sync",
"delete": "Delete",
"create_new": "Create New",
"name": "Name",
"provider": "Provider",
"status": "Status",
"active": "Active",
"created_at": "Created At",
"actions": "Actions",
"last_sync_message": "Last sync message",
"empty": "Empty",
"open_menu": "Open menu",
"edit": "Edit",
"create": "Create",
"ingestion_source": "Ingestion Source",
"edit_description": "Make changes to your ingestion source here.",
"create_description": "Add a new ingestion source to start archiving emails.",
"read": "Read",
"docs_here": "docs here",
"delete_confirmation_title": "Are you sure you want to delete this ingestion?",
"delete_confirmation_description": "This will delete all archived emails, attachments, indexing, and files associated with this ingestion. If you only want to stop syncing new emails, you can pause the ingestion instead.",
"deleting": "Deleting",
"confirm": "Confirm",
"cancel": "Cancel",
"bulk_delete_confirmation_title": "Are you sure you want to delete {{count}} selected ingestions?",
"bulk_delete_confirmation_description": "This will delete all archived emails, attachments, indexing, and files associated with these ingestions. If you only want to stop syncing new emails, you can pause the ingestions instead."
},
"search": {
"title": "Search",
"description": "Search for archived emails.",
"email_search": "Email Search",
"placeholder": "Search by keyword, sender, recipient...",
"search_button": "Search",
"search_options": "Search options",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Verbatim",
"strategy_frequency": "Frequency",
"select_strategy": "Select a strategy",
"error": "Error",
"found_results_in": "Found {{total}} results in {{seconds}}s",
"found_results": "Found {{total}} results",
"from": "From",
"to": "To",
"in_email_body": "In email body",
"in_attachment": "In attachment: {{filename}}",
"prev": "Prev",
"next": "Next"
},
"roles": {
"title": "Role Management",
"role_management": "Role Management",
"create_new": "Create New",
"name": "Name",
"created_at": "Created At",
"actions": "Actions",
"open_menu": "Open menu",
"view_policy": "View Policy",
"edit": "Edit",
"delete": "Delete",
"no_roles_found": "No roles found.",
"role_policy": "Role Policy",
"viewing_policy_for_role": "Viewing policy for role: {{name}}",
"create": "Create",
"role": "Role",
"edit_description": "Make changes to the role here.",
"create_description": "Add a new role to the system.",
"delete_confirmation_title": "Are you sure you want to delete this role?",
"delete_confirmation_description": "This action cannot be undone. This will permanently delete the role.",
"deleting": "Deleting",
"confirm": "Confirm",
"cancel": "Cancel"
},
"system_settings": {
"title": "System Settings",
"system_settings": "System Settings",
"description": "Manage global application settings.",
"language": "Language",
"default_theme": "Default theme",
"light": "Light",
"dark": "Dark",
"system": "System",
"support_email": "Support Email",
"saving": "Saving",
"save_changes": "Save Changes"
},
"users": {
"title": "User Management",
"user_management": "User Management",
"create_new": "Create New",
"name": "Name",
"email": "Email",
"role": "Role",
"created_at": "Created At",
"actions": "Actions",
"open_menu": "Open menu",
"edit": "Edit",
"delete": "Delete",
"no_users_found": "No users found.",
"create": "Create",
"user": "User",
"edit_description": "Make changes to the user here.",
"create_description": "Add a new user to the system.",
"delete_confirmation_title": "Are you sure you want to delete this user?",
"delete_confirmation_description": "This action cannot be undone. This will permanently delete the user and remove their data from our servers.",
"deleting": "Deleting",
"confirm": "Confirm",
"cancel": "Cancel"
},
"components": {
"charts": {
"emails_ingested": "Emails Ingested",
"storage_used": "Storage Used",
"emails": "Emails"
},
"common": {
"submitting": "Submitting...",
"submit": "Submit",
"save": "Save"
},
"email_preview": {
"loading": "Loading email preview...",
"render_error": "Could not render email preview.",
"not_available": "Raw .eml file not available for this email."
},
"footer": {
"all_rights_reserved": "All rights reserved."
},
"ingestion_source_form": {
"provider_generic_imap": "Generic IMAP",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "PST Import",
"provider_eml_import": "EML Import",
"select_provider": "Select a provider",
"service_account_key": "Service Account Key (JSON)",
"service_account_key_placeholder": "Paste your service account key JSON content",
"impersonated_admin_email": "Impersonated Admin Email",
"client_id": "Application (Client) ID",
"client_secret": "Client Secret Value",
"client_secret_placeholder": "Enter the secret Value, not the Secret ID",
"tenant_id": "Directory (Tenant) ID",
"host": "Host",
"port": "Port",
"username": "Username",
"use_tls": "Use TLS",
"pst_file": "PST File",
"eml_file": "EML File",
"heads_up": "Heads up!",
"org_wide_warning": "Please note that this is an organization-wide operation. This kind of ingestions will import and index <b>all</b> email inboxes in your organization. If you want to import only specific email inboxes, use the IMAP connector.",
"upload_failed": "Upload Failed, please try again"
},
"role_form": {
"policies_json": "Policies (JSON)",
"invalid_json": "Invalid JSON format for policies."
},
"theme_switcher": {
"toggle_theme": "Toggle theme"
},
"user_form": {
"select_role": "Select a role"
}
},
"setup": {
"title": "Setup",
"description": "Set up the initial administrator account for Open Archiver.",
"welcome": "Welcome",
"create_admin_account": "Create the first administrator account to get started.",
"first_name": "First name",
"last_name": "Last name",
"email": "Email",
"password": "Password",
"creating_account": "Creating Account",
"create_account": "Create Account"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Ingestions",
"archived_emails": "Archived emails",
"search": "Search",
"settings": "Settings",
"system": "System",
"users": "Users",
"roles": "Roles",
"logout": "Logout"
},
"archived_emails_page": {
"title": "Archived emails",
"header": "Archived Emails",
"select_ingestion_source": "Select an ingestion source",
"date": "Date",
"subject": "Subject",
"sender": "Sender",
"inbox": "Inbox",
"path": "Path",
"actions": "Actions",
"view": "View",
"no_emails_found": "No archived emails found.",
"prev": "Prev",
"next": "Next"
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Overview of your email archive.",
"header": "Dashboard",
"create_ingestion": "Create an ingestion",
"no_ingestion_header": "You don't have any ingestion source set up.",
"no_ingestion_text": "Add an ingestion source to start archiving your inboxes.",
"total_emails_archived": "Total Emails Archived",
"total_storage_used": "Total Storage Used",
"failed_ingestions": "Failed Ingestions (Last 7 Days)",
"ingestion_history": "Ingestion History",
"no_ingestion_history": "No ingestion history available.",
"storage_by_source": "Storage by Ingestion Source",
"no_ingestion_sources": "No ingestion sources available.",
"indexed_insights": "Indexed insights",
"top_10_senders": "Top 10 Senders",
"no_indexed_insights": "No indexed insights available."
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Iniciar sesión",
"login_tip": "Ingrese su correo electrónico a continuación para iniciar sesión en su cuenta.",
"email": "Correo electrónico",
"password": "Contraseña"
},
"common": {
"working": "Trabajando"
},
"archive": {
"title": "Archivo",
"no_subject": "Sin asunto",
"from": "De",
"sent": "Enviado",
"recipients": "Destinatarios",
"to": "Para",
"meta_data": "Metadatos",
"folder": "Carpeta",
"tags": "Etiquetas",
"size": "Tamaño",
"email_preview": "Vista previa del correo electrónico",
"attachments": "Archivos adjuntos",
"download": "Descargar",
"actions": "Acciones",
"download_eml": "Descargar correo electrónico (.eml)",
"delete_email": "Eliminar correo electrónico",
"email_thread": "Hilo de correo electrónico",
"delete_confirmation_title": "¿Está seguro de que desea eliminar este correo electrónico?",
"delete_confirmation_description": "Esta acción no se puede deshacer y eliminará permanentemente el correo electrónico y sus archivos adjuntos.",
"deleting": "Eliminando",
"confirm": "Confirmar",
"cancel": "Cancelar",
"not_found": "Correo electrónico no encontrado."
},
"ingestions": {
"title": "Fuentes de ingesta",
"ingestion_sources": "Fuentes de ingesta",
"bulk_actions": "Acciones masivas",
"force_sync": "Forzar sincronización",
"delete": "Eliminar",
"create_new": "Crear nuevo",
"name": "Nombre",
"provider": "Proveedor",
"status": "Estado",
"active": "Activo",
"created_at": "Creado el",
"actions": "Acciones",
"last_sync_message": "Último mensaje de sincronización",
"empty": "Vacío",
"open_menu": "Abrir menú",
"edit": "Editar",
"create": "Crear",
"ingestion_source": "Fuente de ingesta",
"edit_description": "Realice cambios en su fuente de ingesta aquí.",
"create_description": "Agregue una nueva fuente de ingesta para comenzar a archivar correos electrónicos.",
"read": "Leer",
"docs_here": "documentos aquí",
"delete_confirmation_title": "¿Está seguro de que desea eliminar esta ingesta?",
"delete_confirmation_description": "Esto eliminará todos los correos electrónicos archivados, archivos adjuntos, indexación y archivos asociados con esta ingesta. Si solo desea dejar de sincronizar nuevos correos electrónicos, puede pausar la ingesta en su lugar.",
"deleting": "Eliminando",
"confirm": "Confirmar",
"cancel": "Cancelar",
"bulk_delete_confirmation_title": "¿Está seguro de que desea eliminar {{count}} ingestas seleccionadas?",
"bulk_delete_confirmation_description": "Esto eliminará todos los correos electrónicos archivados, archivos adjuntos, indexación y archivos asociados con estas ingestas. Si solo desea dejar de sincronizar nuevos correos electrónicos, puede pausar las ingestas en su lugar."
},
"search": {
"title": "Buscar",
"description": "Buscar correos electrónicos archivados.",
"email_search": "Búsqueda de correo electrónico",
"placeholder": "Buscar por palabra clave, remitente, destinatario...",
"search_button": "Buscar",
"search_options": "Opciones de búsqueda",
"strategy_fuzzy": "Difuso",
"strategy_verbatim": "Literal",
"strategy_frequency": "Frecuencia",
"select_strategy": "Seleccione una estrategia",
"error": "Error",
"found_results_in": "Se encontraron {{total}} resultados en {{seconds}}s",
"found_results": "Se encontraron {{total}} resultados",
"from": "De",
"to": "Para",
"in_email_body": "En el cuerpo del correo electrónico",
"in_attachment": "En el archivo adjunto: {{filename}}",
"prev": "Anterior",
"next": "Siguiente"
},
"roles": {
"title": "Gestión de roles",
"role_management": "Gestión de roles",
"create_new": "Crear nuevo",
"name": "Nombre",
"created_at": "Creado el",
"actions": "Acciones",
"open_menu": "Abrir menú",
"view_policy": "Ver política",
"edit": "Editar",
"delete": "Eliminar",
"no_roles_found": "No se encontraron roles.",
"role_policy": "Política de roles",
"viewing_policy_for_role": "Viendo la política para el rol: {{name}}",
"create": "Crear",
"role": "Rol",
"edit_description": "Realice cambios en el rol aquí.",
"create_description": "Agregue un nuevo rol al sistema.",
"delete_confirmation_title": "¿Está seguro de que desea eliminar este rol?",
"delete_confirmation_description": "Esta acción no se puede deshacer. Esto eliminará permanentemente el rol.",
"deleting": "Eliminando",
"confirm": "Confirmar",
"cancel": "Cancelar"
},
"system_settings": {
"title": "Configuración del sistema",
"system_settings": "Configuración del sistema",
"description": "Administrar la configuración global de la aplicación.",
"language": "Idioma",
"default_theme": "Tema predeterminado",
"light": "Claro",
"dark": "Oscuro",
"system": "Sistema",
"support_email": "Correo electrónico de soporte",
"saving": "Guardando",
"save_changes": "Guardar cambios"
},
"users": {
"title": "Gestión de usuarios",
"user_management": "Gestión de usuarios",
"create_new": "Crear nuevo",
"name": "Nombre",
"email": "Correo electrónico",
"role": "Rol",
"created_at": "Creado el",
"actions": "Acciones",
"open_menu": "Abrir menú",
"edit": "Editar",
"delete": "Eliminar",
"no_users_found": "No se encontraron usuarios.",
"create": "Crear",
"user": "Usuario",
"edit_description": "Realice cambios en el usuario aquí.",
"create_description": "Agregue un nuevo usuario al sistema.",
"delete_confirmation_title": "¿Está seguro de que desea eliminar este usuario?",
"delete_confirmation_description": "Esta acción no se puede deshacer. Esto eliminará permanentemente al usuario y eliminará sus datos de nuestros servidores.",
"deleting": "Eliminando",
"confirm": "Confirmar",
"cancel": "Cancelar"
},
"setup": {
"title": "Configuración",
"description": "Configure la cuenta de administrador inicial para Open Archiver.",
"welcome": "Bienvenido",
"create_admin_account": "Cree la primera cuenta de administrador para comenzar.",
"first_name": "Nombre",
"last_name": "Apellido",
"email": "Correo electrónico",
"password": "Contraseña",
"creating_account": "Creando cuenta",
"create_account": "Crear cuenta"
},
"layout": {
"dashboard": "Tablero",
"ingestions": "Ingestas",
"archived_emails": "Correos electrónicos archivados",
"search": "Buscar",
"settings": "Configuración",
"system": "Sistema",
"users": "Usuarios",
"roles": "Roles",
"logout": "Cerrar sesión"
},
"components": {
"charts": {
"emails_ingested": "Correos electrónicos ingeridos",
"storage_used": "Almacenamiento utilizado",
"emails": "Correos electrónicos"
},
"common": {
"submitting": "Enviando...",
"submit": "Enviar",
"save": "Guardar"
},
"email_preview": {
"loading": "Cargando vista previa del correo electrónico...",
"render_error": "No se pudo renderizar la vista previa del correo electrónico.",
"not_available": "El archivo .eml sin procesar no está disponible para este correo electrónico."
},
"footer": {
"all_rights_reserved": "Todos los derechos reservados."
},
"ingestion_source_form": {
"provider_generic_imap": "IMAP genérico",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "Importación de PST",
"provider_eml_import": "Importación de EML",
"select_provider": "Seleccione un proveedor",
"service_account_key": "Clave de cuenta de servicio (JSON)",
"service_account_key_placeholder": "Pegue el contenido JSON de su clave de cuenta de servicio",
"impersonated_admin_email": "Correo electrónico de administrador suplantado",
"client_id": "ID de aplicación (cliente)",
"client_secret": "Valor secreto del cliente",
"client_secret_placeholder": "Ingrese el valor secreto, no el ID secreto",
"tenant_id": "ID de directorio (inquilino)",
"host": "Host",
"port": "Puerto",
"username": "Nombre de usuario",
"use_tls": "Usar TLS",
"pst_file": "Archivo PST",
"eml_file": "Archivo EML",
"heads_up": "¡Atención!",
"org_wide_warning": "Tenga en cuenta que esta es una operación para toda la organización. Este tipo de ingestas importará e indexará <b>todos</b> los buzones de correo electrónico de su organización. Si desea importar solo buzones de correo electrónico específicos, utilice el conector IMAP.",
"upload_failed": "Error al cargar, por favor intente de nuevo"
},
"role_form": {
"policies_json": "Políticas (JSON)",
"invalid_json": "Formato JSON no válido para las políticas."
},
"theme_switcher": {
"toggle_theme": "Cambiar tema"
},
"user_form": {
"select_role": "Seleccione un rol"
}
},
"dashboard_page": {
"title": "Tablero",
"meta_description": "Resumen de su archivo de correo electrónico.",
"header": "Tablero",
"create_ingestion": "Crear una ingesta",
"no_ingestion_header": "No tiene ninguna fuente de ingesta configurada.",
"no_ingestion_text": "Agregue una fuente de ingesta para comenzar a archivar sus bandejas de entrada.",
"total_emails_archived": "Total de correos electrónicos archivados",
"total_storage_used": "Almacenamiento total utilizado",
"failed_ingestions": "Ingestas fallidas (últimos 7 días)",
"ingestion_history": "Historial de ingesta",
"no_ingestion_history": "No hay historial de ingesta disponible.",
"storage_by_source": "Almacenamiento por fuente de ingesta",
"no_ingestion_sources": "No hay fuentes de ingesta disponibles.",
"indexed_insights": "Información indexada",
"top_10_senders": "Los 10 principales remitentes",
"no_indexed_insights": "No hay información indexada disponible."
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Logi sisse",
"login_tip": "Oma kontole sisselogimiseks sisestage allpool oma e-posti aadress.",
"email": "E-post",
"password": "Parool"
},
"common": {
"working": "Töötan"
},
"archive": {
"title": "Arhiiv",
"no_subject": "Teema puudub",
"from": "Kellelt",
"sent": "Saadetud",
"recipients": "Saajad",
"to": "Kellele",
"meta_data": "Metaandmed",
"folder": "Kaust",
"tags": "Sildid",
"size": "Suurus",
"email_preview": "E-kirja eelvaade",
"attachments": "Manused",
"download": "Laadi alla",
"actions": "Toimingud",
"download_eml": "Laadi alla e-kiri (.eml)",
"delete_email": "Kustuta e-kiri",
"email_thread": "E-kirja lõim",
"delete_confirmation_title": "Kas olete kindel, et soovite selle e-kirja kustutada?",
"delete_confirmation_description": "Seda toimingut ei saa tagasi võtta ja see eemaldab e-kirja ja selle manused jäädavalt.",
"deleting": "Kustutamine",
"confirm": "Kinnita",
"cancel": "Tühista",
"not_found": "E-kirja ei leitud."
},
"ingestions": {
"title": "Sissevõtuallikad",
"ingestion_sources": "Sissevõtuallikad",
"bulk_actions": "Hulgitoimingud",
"force_sync": "Sunni sünkroonimine",
"delete": "Kustuta",
"create_new": "Loo uus",
"name": "Nimi",
"provider": "Pakkuja",
"status": "Olek",
"active": "Aktiivne",
"created_at": "Loodud",
"actions": "Toimingud",
"last_sync_message": "Viimane sünkroonimissõnum",
"empty": "Tühi",
"open_menu": "Ava menüü",
"edit": "Muuda",
"create": "Loo",
"ingestion_source": "Sissevõtuallikas",
"edit_description": "Tehke siin oma sissevõtuallikas muudatusi.",
"create_description": "E-kirjade arhiveerimise alustamiseks lisage uus sissevõtuallikas.",
"read": "Loe",
"docs_here": "dokumendid siin",
"delete_confirmation_title": "Kas olete kindel, et soovite selle sissevõtu kustutada?",
"delete_confirmation_description": "See kustutab kõik selle sissevõtuga seotud arhiveeritud e-kirjad, manused, indekseerimise ja failid. Kui soovite ainult uute e-kirjade sünkroonimise peatada, saate sissevõtu peatada.",
"deleting": "Kustutamine",
"confirm": "Kinnita",
"cancel": "Tühista",
"bulk_delete_confirmation_title": "Kas olete kindel, et soovite kustutada {{count}} valitud sissevõttu?",
"bulk_delete_confirmation_description": "See kustutab kõik nende sissevõttudega seotud arhiveeritud e-kirjad, manused, indekseerimise ja failid. Kui soovite ainult uute e-kirjade sünkroonimise peatada, saate sissevõtud peatada."
},
"search": {
"title": "Otsing",
"description": "Otsige arhiveeritud e-kirju.",
"email_search": "E-kirja otsing",
"placeholder": "Otsige märksõna, saatja, saaja järgi...",
"search_button": "Otsi",
"search_options": "Otsinguvalikud",
"strategy_fuzzy": "Hägune",
"strategy_verbatim": "Sõnasõnaline",
"strategy_frequency": "Sagedus",
"select_strategy": "Valige strateegia",
"error": "Viga",
"found_results_in": "Leiti {{total}} tulemust {{seconds}} sekundiga",
"found_results": "Leiti {{total}} tulemust",
"from": "Kellelt",
"to": "Kellele",
"in_email_body": "E-kirja sisus",
"in_attachment": "Manuses: {{filename}}",
"prev": "Eelmine",
"next": "Järgmine"
},
"roles": {
"title": "Rollide haldamine",
"role_management": "Rollide haldamine",
"create_new": "Loo uus",
"name": "Nimi",
"created_at": "Loodud",
"actions": "Toimingud",
"open_menu": "Ava menüü",
"view_policy": "Vaata poliitikat",
"edit": "Muuda",
"delete": "Kustuta",
"no_roles_found": "Rolle ei leitud.",
"role_policy": "Rollipoliitika",
"viewing_policy_for_role": "Rolli poliitika vaatamine: {{name}}",
"create": "Loo",
"role": "Roll",
"edit_description": "Tehke siin rollis muudatusi.",
"create_description": "Lisage süsteemi uus roll.",
"delete_confirmation_title": "Kas olete kindel, et soovite selle rolli kustutada?",
"delete_confirmation_description": "Seda toimingut ei saa tagasi võtta. See kustutab rolli jäädavalt.",
"deleting": "Kustutamine",
"confirm": "Kinnita",
"cancel": "Tühista"
},
"system_settings": {
"title": "Süsteemi seaded",
"system_settings": "Süsteemi seaded",
"description": "Hallake globaalseid rakenduse seadeid.",
"language": "Keel",
"default_theme": "Vaiketeema",
"light": "Hele",
"dark": "Tume",
"system": "Süsteem",
"support_email": "Tugi e-post",
"saving": "Salvestamine",
"save_changes": "Salvesta muudatused"
},
"users": {
"title": "Kasutajate haldamine",
"user_management": "Kasutajate haldamine",
"create_new": "Loo uus",
"name": "Nimi",
"email": "E-post",
"role": "Roll",
"created_at": "Loodud",
"actions": "Toimingud",
"open_menu": "Ava menüü",
"edit": "Muuda",
"delete": "Kustuta",
"no_users_found": "Kasutajaid ei leitud.",
"create": "Loo",
"user": "Kasutaja",
"edit_description": "Tehke siin kasutajas muudatusi.",
"create_description": "Lisage süsteemi uus kasutaja.",
"delete_confirmation_title": "Kas olete kindel, et soovite selle kasutaja kustutada?",
"delete_confirmation_description": "Seda toimingut ei saa tagasi võtta. See kustutab kasutaja jäädavalt ja eemaldab tema andmed meie serveritest.",
"deleting": "Kustutamine",
"confirm": "Kinnita",
"cancel": "Tühista"
},
"setup": {
"title": "Seadistamine",
"description": "Seadistage Open Archiveri esialgne administraatorikonto.",
"welcome": "Tere tulemast",
"create_admin_account": "Alustamiseks looge esimene administraatorikonto.",
"first_name": "Eesnimi",
"last_name": "Perekonnanimi",
"email": "E-post",
"password": "Parool",
"creating_account": "Konto loomine",
"create_account": "Loo konto"
},
"layout": {
"dashboard": "Armatuurlaud",
"ingestions": "Sissevõtud",
"archived_emails": "Arhiveeritud e-kirjad",
"search": "Otsing",
"settings": "Seaded",
"system": "Süsteem",
"users": "Kasutajad",
"roles": "Rollid",
"logout": "Logi välja"
},
"components": {
"charts": {
"emails_ingested": "Sissevõetud e-kirjad",
"storage_used": "Kasutatud salvestusruum",
"emails": "E-kirjad"
},
"common": {
"submitting": "Esitamine...",
"submit": "Esita",
"save": "Salvesta"
},
"email_preview": {
"loading": "E-kirja eelvaate laadimine...",
"render_error": "E-kirja eelvaadet ei saanud renderdada.",
"not_available": "Selle e-kirja jaoks pole toores .eml-faili saadaval."
},
"footer": {
"all_rights_reserved": "Kõik õigused kaitstud."
},
"ingestion_source_form": {
"provider_generic_imap": "Üldine IMAP",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "PST import",
"provider_eml_import": "EML import",
"select_provider": "Valige pakkuja",
"service_account_key": "Teenusekonto võti (JSON)",
"service_account_key_placeholder": "Kleepige oma teenusekonto võtme JSON-sisu",
"impersonated_admin_email": "Impersoniseeritud administraatori e-post",
"client_id": "Rakenduse (kliendi) ID",
"client_secret": "Kliendi salajane väärtus",
"client_secret_placeholder": "Sisestage salajane väärtus, mitte salajane ID",
"tenant_id": "Kataloogi (üürniku) ID",
"host": "Host",
"port": "Port",
"username": "Kasutajanimi",
"use_tls": "Kasuta TLS-i",
"pst_file": "PST-fail",
"eml_file": "EML-fail",
"heads_up": "Tähelepanu!",
"org_wide_warning": "Pange tähele, et see on kogu organisatsiooni hõlmav toiming. Seda tüüpi sissevõtud impordivad ja indekseerivad <b>kõik</b> teie organisatsiooni e-posti postkastid. Kui soovite importida ainult konkreetseid e-posti postkaste, kasutage IMAP-konnektorit.",
"upload_failed": "Üleslaadimine ebaõnnestus, proovige uuesti"
},
"role_form": {
"policies_json": "Poliitikad (JSON)",
"invalid_json": "Poliitikate jaoks kehtetu JSON-vorming."
},
"theme_switcher": {
"toggle_theme": "Vaheta teemat"
},
"user_form": {
"select_role": "Valige roll"
}
},
"dashboard_page": {
"title": "Armatuurlaud",
"meta_description": "Ülevaade teie e-posti arhiivist.",
"header": "Armatuurlaud",
"create_ingestion": "Loo sissevõtt",
"no_ingestion_header": "Teil pole ühtegi sissevõtuallikat seadistatud.",
"no_ingestion_text": "Postkastide arhiveerimise alustamiseks lisage sissevõtuallikas.",
"total_emails_archived": "Arhiveeritud e-kirjade koguarv",
"total_storage_used": "Kasutatud salvestusruum kokku",
"failed_ingestions": "Ebaõnnestunud sissevõtud (viimased 7 päeva)",
"ingestion_history": "Sissevõtuajalugu",
"no_ingestion_history": "Sissevõtuajalugu pole saadaval.",
"storage_by_source": "Salvestusruum sissevõtuallika järgi",
"no_ingestion_sources": "Sissevõtuallikaid pole saadaval.",
"indexed_insights": "Indekseeritud ülevaated",
"top_10_senders": "Top 10 saatjat",
"no_indexed_insights": "Indekseeritud ülevaateid pole saadaval."
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Connexion",
"login_tip": "Entrez votre email ci-dessous pour vous connecter à votre compte.",
"email": "Email",
"password": "Mot de passe"
},
"common": {
"working": "Travail en cours"
},
"archive": {
"title": "Archive",
"no_subject": "Pas de sujet",
"from": "De",
"sent": "Envoyé",
"recipients": "Destinataires",
"to": "À",
"meta_data": "Métadonnées",
"folder": "Dossier",
"tags": "Tags",
"size": "Taille",
"email_preview": "Aperçu de l'email",
"attachments": "Pièces jointes",
"download": "Télécharger",
"actions": "Actions",
"download_eml": "Télécharger l'email (.eml)",
"delete_email": "Supprimer l'email",
"email_thread": "Fil de discussion",
"delete_confirmation_title": "Êtes-vous sûr de vouloir supprimer cet email ?",
"delete_confirmation_description": "Cette action est irréversible et supprimera définitivement l'email et ses pièces jointes.",
"deleting": "Suppression en cours",
"confirm": "Confirmer",
"cancel": "Annuler",
"not_found": "Email non trouvé."
},
"ingestions": {
"title": "Sources d'ingestion",
"ingestion_sources": "Sources d'ingestion",
"bulk_actions": "Actions en masse",
"force_sync": "Forcer la synchronisation",
"delete": "Supprimer",
"create_new": "Créer",
"name": "Nom",
"provider": "Fournisseur",
"status": "Statut",
"active": "Actif",
"created_at": "Créé le",
"actions": "Actions",
"last_sync_message": "Dernier message de synchronisation",
"empty": "Vide",
"open_menu": "Ouvrir le menu",
"edit": "Modifier",
"create": "Créer",
"ingestion_source": "Source d'ingestion",
"edit_description": "Modifiez votre source d'ingestion ici.",
"create_description": "Ajoutez une nouvelle source d'ingestion pour commencer à archiver les emails.",
"read": "Lire",
"docs_here": "docs ici",
"delete_confirmation_title": "Êtes-vous sûr de vouloir supprimer cette ingestion ?",
"delete_confirmation_description": "Cela supprimera tous les emails archivés, les pièces jointes, l'indexation et les fichiers associés à cette ingestion. Si vous souhaitez uniquement arrêter la synchronisation des nouveaux emails, vous pouvez suspendre l'ingestion à la place.",
"deleting": "Suppression en cours",
"confirm": "Confirmer",
"cancel": "Annuler",
"bulk_delete_confirmation_title": "Êtes-vous sûr de vouloir supprimer {{count}} ingestions sélectionnées ?",
"bulk_delete_confirmation_description": "Cela supprimera tous les emails archivés, les pièces jointes, l'indexation et les fichiers associés à ces ingestions. Si vous souhaitez uniquement arrêter la synchronisation des nouveaux emails, vous pouvez suspendre les ingestions à la place."
},
"search": {
"title": "Recherche",
"description": "Rechercher des emails archivés.",
"email_search": "Recherche d'emails",
"placeholder": "Rechercher par mot-clé, expéditeur, destinataire...",
"search_button": "Rechercher",
"search_options": "Options de recherche",
"strategy_fuzzy": "Floue",
"strategy_verbatim": "Textuelle",
"strategy_frequency": "Fréquence",
"select_strategy": "Sélectionnez une stratégie",
"error": "Erreur",
"found_results_in": "{{total}} résultats trouvés en {{seconds}}s",
"found_results": "{{total}} résultats trouvés",
"from": "De",
"to": "À",
"in_email_body": "Dans le corps de l'email",
"in_attachment": "Dans la pièce jointe : {{filename}}",
"prev": "Préc",
"next": "Suiv"
},
"roles": {
"title": "Gestion des rôles",
"role_management": "Gestion des rôles",
"create_new": "Créer",
"name": "Nom",
"created_at": "Créé le",
"actions": "Actions",
"open_menu": "Ouvrir le menu",
"view_policy": "Voir la politique",
"edit": "Modifier",
"delete": "Supprimer",
"no_roles_found": "Aucun rôle trouvé.",
"role_policy": "Politique de rôle",
"viewing_policy_for_role": "Affichage de la politique pour le rôle : {{name}}",
"create": "Créer",
"role": "Rôle",
"edit_description": "Modifiez le rôle ici.",
"create_description": "Ajoutez un nouveau rôle au système.",
"delete_confirmation_title": "Êtes-vous sûr de vouloir supprimer ce rôle ?",
"delete_confirmation_description": "Cette action est irréversible. Cela supprimera définitivement le rôle.",
"deleting": "Suppression en cours",
"confirm": "Confirmer",
"cancel": "Annuler"
},
"system_settings": {
"title": "Paramètres système",
"system_settings": "Paramètres système",
"description": "Gérer les paramètres globaux de l'application.",
"language": "Langue",
"default_theme": "Thème par défaut",
"light": "Clair",
"dark": "Sombre",
"system": "Système",
"support_email": "Email de support",
"saving": "Enregistrement",
"save_changes": "Enregistrer les modifications"
},
"users": {
"title": "Gestion des utilisateurs",
"user_management": "Gestion des utilisateurs",
"create_new": "Créer",
"name": "Nom",
"email": "Email",
"role": "Rôle",
"created_at": "Créé le",
"actions": "Actions",
"open_menu": "Ouvrir le menu",
"edit": "Modifier",
"delete": "Supprimer",
"no_users_found": "Aucun utilisateur trouvé.",
"create": "Créer",
"user": "Utilisateur",
"edit_description": "Modifiez l'utilisateur ici.",
"create_description": "Ajoutez un nouvel utilisateur au système.",
"delete_confirmation_title": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?",
"delete_confirmation_description": "Cette action est irréversible. Cela supprimera définitivement l'utilisateur et ses données de nos serveurs.",
"deleting": "Suppression en cours",
"confirm": "Confirmer",
"cancel": "Annuler"
},
"setup": {
"title": "Configuration",
"description": "Configurez le compte administrateur initial pour Open Archiver.",
"welcome": "Bienvenue",
"create_admin_account": "Créez le premier compte administrateur pour commencer.",
"first_name": "Prénom",
"last_name": "Nom de famille",
"email": "Email",
"password": "Mot de passe",
"creating_account": "Création du compte",
"create_account": "Créer un compte"
},
"layout": {
"dashboard": "Tableau de bord",
"ingestions": "Ingestions",
"archived_emails": "E-mails archivés",
"search": "Recherche",
"settings": "Paramètres",
"system": "Système",
"users": "Utilisateurs",
"roles": "Rôles",
"logout": "Déconnexion"
},
"components": {
"charts": {
"emails_ingested": "E-mails ingérés",
"storage_used": "Stockage utilisé",
"emails": "E-mails"
},
"common": {
"submitting": "Soumission...",
"submit": "Soumettre",
"save": "Enregistrer"
},
"email_preview": {
"loading": "Chargement de l'aperçu de l'email...",
"render_error": "Impossible de rendre l'aperçu de l'email.",
"not_available": "Le fichier .eml brut n'est pas disponible pour cet email."
},
"footer": {
"all_rights_reserved": "Tous droits réservés."
},
"ingestion_source_form": {
"provider_generic_imap": "IMAP générique",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "Importation PST",
"provider_eml_import": "Importation EML",
"select_provider": "Sélectionnez un fournisseur",
"service_account_key": "Clé de compte de service (JSON)",
"service_account_key_placeholder": "Collez le contenu JSON de votre clé de compte de service",
"impersonated_admin_email": "Email de l'administrateur impersonné",
"client_id": "ID de l'application (client)",
"client_secret": "Valeur secrète du client",
"client_secret_placeholder": "Entrez la valeur secrète, pas l'ID secret",
"tenant_id": "ID du répertoire (locataire)",
"host": "Hôte",
"port": "Port",
"username": "Nom d'utilisateur",
"use_tls": "Utiliser TLS",
"pst_file": "Fichier PST",
"eml_file": "Fichier EML",
"heads_up": "Attention !",
"org_wide_warning": "Veuillez noter qu'il s'agit d'une opération à l'échelle de l'organisation. Ce type d'ingestion importera et indexera <b>toutes</b> les boîtes de réception de votre organisation. Si vous souhaitez importer uniquement des boîtes de réception spécifiques, utilisez le connecteur IMAP.",
"upload_failed": "Échec du téléchargement, veuillez réessayer"
},
"role_form": {
"policies_json": "Politiques (JSON)",
"invalid_json": "Format JSON invalide pour les politiques."
},
"theme_switcher": {
"toggle_theme": "Changer de thème"
},
"user_form": {
"select_role": "Sélectionnez un rôle"
}
},
"dashboard_page": {
"title": "Tableau de bord",
"meta_description": "Aperçu de vos archives d'e-mails.",
"header": "Tableau de bord",
"create_ingestion": "Créer une ingestion",
"no_ingestion_header": "Vous n'avez aucune source d'ingestion configurée.",
"no_ingestion_text": "Ajoutez une source d'ingestion pour commencer à archiver vos boîtes de réception.",
"total_emails_archived": "Total des e-mails archivés",
"total_storage_used": "Stockage total utilisé",
"failed_ingestions": "Ingestions échouées (7 derniers jours)",
"ingestion_history": "Historique d'ingestion",
"no_ingestion_history": "Aucun historique d'ingestion disponible.",
"storage_by_source": "Stockage par source d'ingestion",
"no_ingestion_sources": "Aucune source d'ingestion disponible.",
"indexed_insights": "Informations indexées",
"top_10_senders": "Top 10 des expéditeurs",
"no_indexed_insights": "Aucune information indexée disponible."
}
}
}

View File

@@ -0,0 +1,123 @@
import i18n from 'sveltekit-i18n';
import type { Config } from 'sveltekit-i18n';
// Import your locales
import en from './en.json';
import de from './de.json';
import es from './es.json';
import fr from './fr.json';
import it from './it.json';
import pt from './pt.json';
import nl from './nl.json';
import ja from './ja.json';
import et from './et.json';
// This is your config object.
// It defines the languages and how to load them.
const config: Config = {
// Define the loaders for each language
loaders: [
// English 🇬🇧
{
locale: 'en',
key: 'app', // This key matches the top-level key in your en.json
loader: async () => en.app, // We return the nested 'app' object
},
// German 🇩🇪
{
locale: 'de',
key: 'app', // This key matches the top-level key in your en.json
loader: async () => de.app, // We return the nested 'app' object
},
// Spanish 🇪🇸
{
locale: 'es',
key: 'app',
loader: async () => es.app,
},
// French 🇫🇷
{
locale: 'fr',
key: 'app',
loader: async () => fr.app,
},
// Italian 🇮🇹
{
locale: 'it',
key: 'app',
loader: async () => it.app,
},
// Portuguese 🇵🇹
{
locale: 'pt',
key: 'app',
loader: async () => pt.app,
},
// Dutch 🇳🇱
{
locale: 'nl',
key: 'app',
loader: async () => nl.app,
},
// Japanese 🇯🇵
{
locale: 'ja',
key: 'app',
loader: async () => ja.app,
},
// Estonian 🇪🇪
{
locale: 'et',
key: 'app',
loader: async () => et.app,
},
],
fallbackLocale: 'en',
};
// Create the i18n instance.
// export const i18nInstance = new i18n(config);
export const { t, locale, locales, loading, loadTranslations } = new i18n(config);
// Export the t store for use in components
// export const t = i18n.t;
// import i18n from 'sveltekit-i18n';
// import type { Config } from 'sveltekit-i18n';
// const config: Config = ({
// loaders: [
// {
// locale: 'en',
// key: 'app',
// loader: async () => (
// await import('./en/app.json')
// ).default,
// },
// {
// locale: 'en',
// key: 'marketing',
// loader: async () => (
// await import('./en/marketing.json')
// ).default,
// },
// {
// locale: 'fr',
// key: 'app',
// loader: async () => (
// await import('./fr/app.json')
// ).default,
// },
// {
// locale: 'fr',
// key: 'marketing',
// loader: async () => (
// await import('./fr/marketing.json')
// ).default,
// },
// ],
// fallbackLocale: 'en'
// });
// export const { t, locale, locales, loading, loadTranslations } = new i18n(config);

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Accesso",
"login_tip": "Inserisci la tua email qui sotto per accedere al tuo account.",
"email": "Email",
"password": "Password"
},
"common": {
"working": "In lavorazione"
},
"archive": {
"title": "Archivio",
"no_subject": "Nessun oggetto",
"from": "Da",
"sent": "Inviato",
"recipients": "Destinatari",
"to": "A",
"meta_data": "Metadati",
"folder": "Cartella",
"tags": "Tag",
"size": "Dimensione",
"email_preview": "Anteprima email",
"attachments": "Allegati",
"download": "Scarica",
"actions": "Azioni",
"download_eml": "Scarica email (.eml)",
"delete_email": "Elimina email",
"email_thread": "Thread email",
"delete_confirmation_title": "Sei sicuro di voler eliminare questa email?",
"delete_confirmation_description": "Questa azione non può essere annullata ed eliminerà permanentemente l'email e i suoi allegati.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla",
"not_found": "Email non trovata."
},
"ingestions": {
"title": "Fonti di ingestione",
"ingestion_sources": "Fonti di ingestione",
"bulk_actions": "Azioni di massa",
"force_sync": "Forza sincronizzazione",
"delete": "Elimina",
"create_new": "Crea nuovo",
"name": "Nome",
"provider": "Provider",
"status": "Stato",
"active": "Attivo",
"created_at": "Creato il",
"actions": "Azioni",
"last_sync_message": "Ultimo messaggio di sincronizzazione",
"empty": "Vuoto",
"open_menu": "Apri menu",
"edit": "Modifica",
"create": "Crea",
"ingestion_source": "Fonte di ingestione",
"edit_description": "Apporta modifiche alla tua fonte di ingestione qui.",
"create_description": "Aggiungi una nuova fonte di ingestione per iniziare ad archiviare le email.",
"read": "Leggi",
"docs_here": "documenti qui",
"delete_confirmation_title": "Sei sicuro di voler eliminare questa ingestione?",
"delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a questa ingestione. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa l'ingestione.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla",
"bulk_delete_confirmation_title": "Sei sicuro di voler eliminare {{count}} ingestioni selezionate?",
"bulk_delete_confirmation_description": "Questo eliminerà tutte le email archiviate, gli allegati, l'indicizzazione e i file associati a queste ingestioni. Se desideri solo interrompere la sincronizzazione di nuove email, puoi invece mettere in pausa le ingestioni."
},
"search": {
"title": "Cerca",
"description": "Cerca email archiviate.",
"email_search": "Ricerca email",
"placeholder": "Cerca per parola chiave, mittente, destinatario...",
"search_button": "Cerca",
"search_options": "Opzioni di ricerca",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Verbatim",
"strategy_frequency": "Frequenza",
"select_strategy": "Seleziona una strategia",
"error": "Errore",
"found_results_in": "Trovati {{total}} risultati in {{seconds}}s",
"found_results": "Trovati {{total}} risultati",
"from": "Da",
"to": "A",
"in_email_body": "Nel corpo dell'email",
"in_attachment": "Nell'allegato: {{filename}}",
"prev": "Prec",
"next": "Succ"
},
"roles": {
"title": "Gestione ruoli",
"role_management": "Gestione ruoli",
"create_new": "Crea nuovo",
"name": "Nome",
"created_at": "Creato il",
"actions": "Azioni",
"open_menu": "Apri menu",
"view_policy": "Visualizza policy",
"edit": "Modifica",
"delete": "Elimina",
"no_roles_found": "Nessun ruolo trovato.",
"role_policy": "Policy ruolo",
"viewing_policy_for_role": "Visualizzazione policy per il ruolo: {{name}}",
"create": "Crea",
"role": "Ruolo",
"edit_description": "Apporta modifiche al ruolo qui.",
"create_description": "Aggiungi un nuovo ruolo al sistema.",
"delete_confirmation_title": "Sei sicuro di voler eliminare questo ruolo?",
"delete_confirmation_description": "Questa azione non può essere annullata. Questo eliminerà permanentemente il ruolo.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla"
},
"system_settings": {
"title": "Impostazioni di sistema",
"system_settings": "Impostazioni di sistema",
"description": "Gestisci le impostazioni globali dell'applicazione.",
"language": "Lingua",
"default_theme": "Tema predefinito",
"light": "Chiaro",
"dark": "Scuro",
"system": "Sistema",
"support_email": "Email di supporto",
"saving": "Salvataggio",
"save_changes": "Salva modifiche"
},
"users": {
"title": "Gestione utenti",
"user_management": "Gestione utenti",
"create_new": "Crea nuovo",
"name": "Nome",
"email": "Email",
"role": "Ruolo",
"created_at": "Creato il",
"actions": "Azioni",
"open_menu": "Apri menu",
"edit": "Modifica",
"delete": "Elimina",
"no_users_found": "Nessun utente trovato.",
"create": "Crea",
"user": "Utente",
"edit_description": "Apporta modifiche all'utente qui.",
"create_description": "Aggiungi un nuovo utente al sistema.",
"delete_confirmation_title": "Sei sicuro di voler eliminare questo utente?",
"delete_confirmation_description": "Questa azione non può essere annullata. Questo eliminerà permanentemente l'utente e rimuoverà i suoi dati dai nostri server.",
"deleting": "Eliminazione in corso",
"confirm": "Conferma",
"cancel": "Annulla"
},
"setup": {
"title": "Configurazione",
"description": "Configura l'account amministratore iniziale per Open Archiver.",
"welcome": "Benvenuto",
"create_admin_account": "Crea il primo account amministratore per iniziare.",
"first_name": "Nome",
"last_name": "Cognome",
"email": "Email",
"password": "Password",
"creating_account": "Creazione account",
"create_account": "Crea account"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Ingestioni",
"archived_emails": "Email archiviate",
"search": "Cerca",
"settings": "Impostazioni",
"system": "Sistema",
"users": "Utenti",
"roles": "Ruoli",
"logout": "Esci"
},
"components": {
"charts": {
"emails_ingested": "Email ingerite",
"storage_used": "Spazio di archiviazione utilizzato",
"emails": "Email"
},
"common": {
"submitting": "Invio in corso...",
"submit": "Invia",
"save": "Salva"
},
"email_preview": {
"loading": "Caricamento anteprima email...",
"render_error": "Impossibile visualizzare l'anteprima dell'email.",
"not_available": "File .eml non disponibile per questa email."
},
"footer": {
"all_rights_reserved": "Tutti i diritti riservati."
},
"ingestion_source_form": {
"provider_generic_imap": "IMAP generico",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "Importazione PST",
"provider_eml_import": "Importazione EML",
"select_provider": "Seleziona un provider",
"service_account_key": "Chiave account di servizio (JSON)",
"service_account_key_placeholder": "Incolla il contenuto JSON della tua chiave account di servizio",
"impersonated_admin_email": "Email amministratore impersonata",
"client_id": "ID applicazione (client)",
"client_secret": "Valore segreto client",
"client_secret_placeholder": "Inserisci il valore segreto, non l'ID segreto",
"tenant_id": "ID directory (tenant)",
"host": "Host",
"port": "Porta",
"username": "Nome utente",
"use_tls": "Usa TLS",
"pst_file": "File PST",
"eml_file": "File EML",
"heads_up": "Attenzione!",
"org_wide_warning": "Si prega di notare che questa è un'operazione a livello di organizzazione. Questo tipo di ingestione importerà e indicizzerà <b>tutte</b> le caselle di posta elettronica della tua organizzazione. Se desideri importare solo caselle di posta elettronica specifiche, utilizza il connettore IMAP.",
"upload_failed": "Caricamento non riuscito, riprova"
},
"role_form": {
"policies_json": "Policy (JSON)",
"invalid_json": "Formato JSON non valido per le policy."
},
"theme_switcher": {
"toggle_theme": "Cambia tema"
},
"user_form": {
"select_role": "Seleziona un ruolo"
}
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Panoramica del tuo archivio email.",
"header": "Dashboard",
"create_ingestion": "Crea un'ingestione",
"no_ingestion_header": "Non hai alcuna fonte di ingestione configurata.",
"no_ingestion_text": "Aggiungi una fonte di ingestione per iniziare ad archiviare le tue caselle di posta.",
"total_emails_archived": "Email totali archiviate",
"total_storage_used": "Spazio di archiviazione totale utilizzato",
"failed_ingestions": "Ingestioni non riuscite (ultimi 7 giorni)",
"ingestion_history": "Cronologia ingestioni",
"no_ingestion_history": "Nessuna cronologia di ingestione disponibile.",
"storage_by_source": "Archiviazione per fonte di ingestione",
"no_ingestion_sources": "Nessuna fonte di ingestione disponibile.",
"indexed_insights": "Approfondimenti indicizzati",
"top_10_senders": "Top 10 mittenti",
"no_indexed_insights": "Nessun approfondimento indicizzato disponibile."
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "ログイン",
"login_tip": "アカウントにログインするには、以下にメールアドレスを入力してください。",
"email": "メール",
"password": "パスワード"
},
"common": {
"working": "作業中"
},
"archive": {
"title": "アーカイブ",
"no_subject": "件名なし",
"from": "差出人",
"sent": "送信日時",
"recipients": "受信者",
"to": "宛先",
"meta_data": "メタデータ",
"folder": "フォルダー",
"tags": "タグ",
"size": "サイズ",
"email_preview": "メールプレビュー",
"attachments": "添付ファイル",
"download": "ダウンロード",
"actions": "アクション",
"download_eml": "メールをダウンロード (.eml)",
"delete_email": "メールを削除",
"email_thread": "メールのスレッド",
"delete_confirmation_title": "このメールを削除してもよろしいですか?",
"delete_confirmation_description": "この操作は元に戻せません。メールと添付ファイルが完全に削除されます。",
"deleting": "削除中",
"confirm": "確認",
"cancel": "キャンセル",
"not_found": "メールが見つかりません。"
},
"ingestions": {
"title": "取り込み元",
"ingestion_sources": "取り込み元",
"bulk_actions": "一括操作",
"force_sync": "強制同期",
"delete": "削除",
"create_new": "新規作成",
"name": "名前",
"provider": "プロバイダー",
"status": "ステータス",
"active": "アクティブ",
"created_at": "作成日",
"actions": "アクション",
"last_sync_message": "最終同期メッセージ",
"empty": "空",
"open_menu": "メニューを開く",
"edit": "編集",
"create": "作成",
"ingestion_source": "取り込み元",
"edit_description": "ここで取り込み元を変更します。",
"create_description": "メールのアーカイブを開始するために、新しい取り込み元を追加します。",
"read": "読む",
"docs_here": "ドキュメントはこちら",
"delete_confirmation_title": "この取り込みを削除してもよろしいですか?",
"delete_confirmation_description": "これにより、この取り込みに関連するすべてのアーカイブ済みメール、添付ファイル、インデックス、およびファイルが削除されます。新しいメールの同期を停止したいだけの場合は、代わりに取り込みを一時停止できます。",
"deleting": "削除中",
"confirm": "確認",
"cancel": "キャンセル",
"bulk_delete_confirmation_title": "選択した{{count}}件の取り込みを削除してもよろしいですか?",
"bulk_delete_confirmation_description": "これにより、これらの取り込みに関連するすべてのアーカイブ済みメール、添付ファイル、インデックス、およびファイルが削除されます。新しいメールの同期を停止したいだけの場合は、代わりに取り込みを一時停止できます。"
},
"search": {
"title": "検索",
"description": "アーカイブされたメールを検索します。",
"email_search": "メール検索",
"placeholder": "キーワード、送信者、受信者で検索...",
"search_button": "検索",
"search_options": "検索オプション",
"strategy_fuzzy": "あいまい",
"strategy_verbatim": "逐語的",
"strategy_frequency": "頻度",
"select_strategy": "戦略を選択",
"error": "エラー",
"found_results_in": "{{seconds}}秒で{{total}}件の結果が見つかりました",
"found_results": "{{total}}件の結果が見つかりました",
"from": "差出人",
"to": "宛先",
"in_email_body": "メール本文内",
"in_attachment": "添付ファイル内: {{filename}}",
"prev": "前へ",
"next": "次へ"
},
"roles": {
"title": "ロール管理",
"role_management": "ロール管理",
"create_new": "新規作成",
"name": "名前",
"created_at": "作成日",
"actions": "アクション",
"open_menu": "メニューを開く",
"view_policy": "ポリシーを表示",
"edit": "編集",
"delete": "削除",
"no_roles_found": "ロールが見つかりません。",
"role_policy": "ロールポリシー",
"viewing_policy_for_role": "ロールのポリシーを表示中: {{name}}",
"create": "作成",
"role": "ロール",
"edit_description": "ここでロールを変更します。",
"create_description": "システムに新しいロールを追加します。",
"delete_confirmation_title": "このロールを削除してもよろしいですか?",
"delete_confirmation_description": "この操作は元に戻せません。これにより、ロールが完全に削除されます。",
"deleting": "削除中",
"confirm": "確認",
"cancel": "キャンセル"
},
"system_settings": {
"title": "システム設定",
"system_settings": "システム設定",
"description": "グローバルなアプリケーション設定を管理します。",
"language": "言語",
"default_theme": "デフォルトのテーマ",
"light": "ライト",
"dark": "ダーク",
"system": "システム",
"support_email": "サポートメール",
"saving": "保存中",
"save_changes": "変更を保存"
},
"users": {
"title": "ユーザー管理",
"user_management": "ユーザー管理",
"create_new": "新規作成",
"name": "名前",
"email": "メール",
"role": "ロール",
"created_at": "作成日",
"actions": "アクション",
"open_menu": "メニューを開く",
"edit": "編集",
"delete": "削除",
"no_users_found": "ユーザーが見つかりません。",
"create": "作成",
"user": "ユーザー",
"edit_description": "ここでユーザーを変更します。",
"create_description": "システムに新しいユーザーを追加します。",
"delete_confirmation_title": "このユーザーを削除してもよろしいですか?",
"delete_confirmation_description": "この操作は元に戻せません。これにより、ユーザーが完全に削除され、データがサーバーから削除されます。",
"deleting": "削除中",
"confirm": "確認",
"cancel": "キャンセル"
},
"setup": {
"title": "セットアップ",
"description": "Open Archiverの初期管理者アカウントをセットアップします。",
"welcome": "ようこそ",
"create_admin_account": "開始するには、最初の管理者アカウントを作成してください。",
"first_name": "名",
"last_name": "姓",
"email": "メール",
"password": "パスワード",
"creating_account": "アカウント作成中",
"create_account": "アカウントを作成"
},
"layout": {
"dashboard": "ダッシュボード",
"ingestions": "取り込み",
"archived_emails": "アーカイブされたメール",
"search": "検索",
"settings": "設定",
"system": "システム",
"users": "ユーザー",
"roles": "ロール",
"logout": "ログアウト"
},
"components": {
"charts": {
"emails_ingested": "取り込まれたメール",
"storage_used": "使用済みストレージ",
"emails": "メール"
},
"common": {
"submitting": "送信中...",
"submit": "送信",
"save": "保存"
},
"email_preview": {
"loading": "メールプレビューを読み込んでいます...",
"render_error": "メールプレビューをレンダリングできませんでした。",
"not_available": "このメールの生の.emlファイルはありません。"
},
"footer": {
"all_rights_reserved": "無断複写・転載を禁じます。"
},
"ingestion_source_form": {
"provider_generic_imap": "汎用IMAP",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "PSTインポート",
"provider_eml_import": "EMLインポート",
"select_provider": "プロバイダーを選択",
"service_account_key": "サービスアカウントキー (JSON)",
"service_account_key_placeholder": "サービスアカウントキーのJSONコンテンツを貼り付けます",
"impersonated_admin_email": "偽装された管理者メール",
"client_id": "アプリケーション (クライアント) ID",
"client_secret": "クライアントシークレット値",
"client_secret_placeholder": "シークレットIDではなく、シークレット値を入力してください",
"tenant_id": "ディレクトリ (テナント) ID",
"host": "ホスト",
"port": "ポート",
"username": "ユーザー名",
"use_tls": "TLSを使用",
"pst_file": "PSTファイル",
"eml_file": "EMLファイル",
"heads_up": "ご注意ください!",
"org_wide_warning": "これは組織全体の操作であることに注意してください。この種の取り込みは、組織内の<b>すべて</b>のメールボックスをインポートしてインデックスを作成します。特定のメールボックスのみをインポートする場合は、IMAPコネクタを使用してください。",
"upload_failed": "アップロードに失敗しました。もう一度お試しください"
},
"role_form": {
"policies_json": "ポリシー (JSON)",
"invalid_json": "ポリシーのJSON形式が無効です。"
},
"theme_switcher": {
"toggle_theme": "テーマを切り替える"
},
"user_form": {
"select_role": "役割を選択"
}
},
"dashboard_page": {
"title": "ダッシュボード",
"meta_description": "メールアーカイブの概要。",
"header": "ダッシュボード",
"create_ingestion": "取り込みを作成",
"no_ingestion_header": "取り込みソースが設定されていません。",
"no_ingestion_text": "受信トレイのアーカイブを開始するには、取り込みソースを追加してください。",
"total_emails_archived": "アーカイブされたメールの総数",
"total_storage_used": "総ストレージ使用量",
"failed_ingestions": "失敗した取り込み過去7日間",
"ingestion_history": "取り込み履歴",
"no_ingestion_history": "取り込み履歴はありません。",
"storage_by_source": "取り込みソース別のストレージ",
"no_ingestion_sources": "利用可能な取り込みソースはありません。",
"indexed_insights": "インデックス付きインサイト",
"top_10_senders": "トップ10送信者",
"no_indexed_insights": "インデックス付きインサイトはありません。"
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Inloggen",
"login_tip": "Voer hieronder uw e-mailadres in om in te loggen op uw account.",
"email": "E-mail",
"password": "Wachtwoord"
},
"common": {
"working": "Bezig"
},
"archive": {
"title": "Archief",
"no_subject": "Geen onderwerp",
"from": "Van",
"sent": "Verzonden",
"recipients": "Ontvangers",
"to": "Aan",
"meta_data": "Metadata",
"folder": "Map",
"tags": "Tags",
"size": "Grootte",
"email_preview": "E-mailvoorbeeld",
"attachments": "Bijlagen",
"download": "Downloaden",
"actions": "Acties",
"download_eml": "E-mail downloaden (.eml)",
"delete_email": "E-mail verwijderen",
"email_thread": "E-mailthread",
"delete_confirmation_title": "Weet u zeker dat u deze e-mail wilt verwijderen?",
"delete_confirmation_description": "Deze actie kan niet ongedaan worden gemaakt en zal de e-mail en de bijlagen permanent verwijderen.",
"deleting": "Verwijderen",
"confirm": "Bevestigen",
"cancel": "Annuleren",
"not_found": "E-mail niet gevonden."
},
"ingestions": {
"title": "Innamebronnen",
"ingestion_sources": "Innamebronnen",
"bulk_actions": "Bulkacties",
"force_sync": "Synchronisatie forceren",
"delete": "Verwijderen",
"create_new": "Nieuw aanmaken",
"name": "Naam",
"provider": "Provider",
"status": "Status",
"active": "Actief",
"created_at": "Aangemaakt op",
"actions": "Acties",
"last_sync_message": "Laatste synchronisatiebericht",
"empty": "Leeg",
"open_menu": "Menu openen",
"edit": "Bewerken",
"create": "Aanmaken",
"ingestion_source": "Innamebron",
"edit_description": "Breng hier wijzigingen aan in uw innamebron.",
"create_description": "Voeg een nieuwe innamebron toe om e-mails te archiveren.",
"read": "Lezen",
"docs_here": "documenten hier",
"delete_confirmation_title": "Weet u zeker dat u deze inname wilt verwijderen?",
"delete_confirmation_description": "Dit verwijdert alle gearchiveerde e-mails, bijlagen, indexering en bestanden die aan deze inname zijn gekoppeld. Als u alleen wilt stoppen met het synchroniseren van nieuwe e-mails, kunt u de inname in plaats daarvan pauzeren.",
"deleting": "Verwijderen",
"confirm": "Bevestigen",
"cancel": "Annuleren",
"bulk_delete_confirmation_title": "Weet u zeker dat u {{count}} geselecteerde innames wilt verwijderen?",
"bulk_delete_confirmation_description": "Dit verwijdert alle gearchiveerde e-mails, bijlagen, indexering en bestanden die aan deze innames zijn gekoppeld. Als u alleen wilt stoppen met het synchroniseren van nieuwe e-mails, kunt u de innames in plaats daarvan pauzeren."
},
"search": {
"title": "Zoeken",
"description": "Zoeken naar gearchiveerde e-mails.",
"email_search": "E-mail zoeken",
"placeholder": "Zoeken op trefwoord, afzender, ontvanger...",
"search_button": "Zoeken",
"search_options": "Zoekopties",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Letterlijk",
"strategy_frequency": "Frequentie",
"select_strategy": "Selecteer een strategie",
"error": "Fout",
"found_results_in": "{{total}} resultaten gevonden in {{seconds}}s",
"found_results": "{{total}} resultaten gevonden",
"from": "Van",
"to": "Aan",
"in_email_body": "In e-mailtekst",
"in_attachment": "In bijlage: {{filename}}",
"prev": "Vorige",
"next": "Volgende"
},
"roles": {
"title": "Rollenbeheer",
"role_management": "Rollenbeheer",
"create_new": "Nieuw aanmaken",
"name": "Naam",
"created_at": "Aangemaakt op",
"actions": "Acties",
"open_menu": "Menu openen",
"view_policy": "Beleid bekijken",
"edit": "Bewerken",
"delete": "Verwijderen",
"no_roles_found": "Geen rollen gevonden.",
"role_policy": "Rollenbeleid",
"viewing_policy_for_role": "Beleid bekijken voor rol: {{name}}",
"create": "Aanmaken",
"role": "Rol",
"edit_description": "Breng hier wijzigingen aan in de rol.",
"create_description": "Voeg een nieuwe rol toe aan het systeem.",
"delete_confirmation_title": "Weet u zeker dat u deze rol wilt verwijderen?",
"delete_confirmation_description": "Deze actie kan niet ongedaan worden gemaakt. Dit zal de rol permanent verwijderen.",
"deleting": "Verwijderen",
"confirm": "Bevestigen",
"cancel": "Annuleren"
},
"system_settings": {
"title": "Systeeminstellingen",
"system_settings": "Systeeminstellingen",
"description": "Beheer de algemene applicatie-instellingen.",
"language": "Taal",
"default_theme": "Standaardthema",
"light": "Licht",
"dark": "Donker",
"system": "Systeem",
"support_email": "Ondersteunings-e-mail",
"saving": "Opslaan",
"save_changes": "Wijzigingen opslaan"
},
"users": {
"title": "Gebruikersbeheer",
"user_management": "Gebruikersbeheer",
"create_new": "Nieuw aanmaken",
"name": "Naam",
"email": "E-mail",
"role": "Rol",
"created_at": "Aangemaakt op",
"actions": "Acties",
"open_menu": "Menu openen",
"edit": "Bewerken",
"delete": "Verwijderen",
"no_users_found": "Geen gebruikers gevonden.",
"create": "Aanmaken",
"user": "Gebruiker",
"edit_description": "Breng hier wijzigingen aan in de gebruiker.",
"create_description": "Voeg een nieuwe gebruiker toe aan het systeem.",
"delete_confirmation_title": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
"delete_confirmation_description": "Deze actie kan niet ongedaan worden gemaakt. Dit zal de gebruiker permanent verwijderen en hun gegevens van onze servers verwijderen.",
"deleting": "Verwijderen",
"confirm": "Bevestigen",
"cancel": "Annuleren"
},
"setup": {
"title": "Installatie",
"description": "Stel het initiële beheerdersaccount in voor Open Archiver.",
"welcome": "Welkom",
"create_admin_account": "Maak het eerste beheerdersaccount aan om te beginnen.",
"first_name": "Voornaam",
"last_name": "Achternaam",
"email": "E-mail",
"password": "Wachtwoord",
"creating_account": "Account aanmaken",
"create_account": "Account aanmaken"
},
"layout": {
"dashboard": "Dashboard",
"ingestions": "Innames",
"archived_emails": "Gearchiveerde e-mails",
"search": "Zoeken",
"settings": "Instellingen",
"system": "Systeem",
"users": "Gebruikers",
"roles": "Rollen",
"logout": "Uitloggen"
},
"components": {
"charts": {
"emails_ingested": "E-mails opgenomen",
"storage_used": "Opslag gebruikt",
"emails": "E-mails"
},
"common": {
"submitting": "Verzenden...",
"submit": "Verzenden",
"save": "Opslaan"
},
"email_preview": {
"loading": "E-mailvoorbeeld laden...",
"render_error": "Kan e-mailvoorbeeld niet weergeven.",
"not_available": "Ruwe .eml-bestand niet beschikbaar voor deze e-mail."
},
"footer": {
"all_rights_reserved": "Alle rechten voorbehouden."
},
"ingestion_source_form": {
"provider_generic_imap": "Generieke IMAP",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "PST importeren",
"provider_eml_import": "EML importeren",
"select_provider": "Selecteer een provider",
"service_account_key": "Servicesleutel (JSON)",
"service_account_key_placeholder": "Plak de JSON-inhoud van uw servicesleutel",
"impersonated_admin_email": "Geïmpersoneerde beheerders-e-mail",
"client_id": "Applicatie (client) ID",
"client_secret": "Clientgeheimwaarde",
"client_secret_placeholder": "Voer de geheime waarde in, niet de geheime ID",
"tenant_id": "Directory (tenant) ID",
"host": "Host",
"port": "Poort",
"username": "Gebruikersnaam",
"use_tls": "TLS gebruiken",
"pst_file": "PST-bestand",
"eml_file": "EML-bestand",
"heads_up": "Let op!",
"org_wide_warning": "Houd er rekening mee dat dit een organisatiebrede bewerking is. Dit type inname importeert en indexeert <b>alle</b> e-mailboxen in uw organisatie. Als u alleen specifieke e-mailboxen wilt importeren, gebruik dan de IMAP-connector.",
"upload_failed": "Uploaden mislukt, probeer het opnieuw"
},
"role_form": {
"policies_json": "Beleid (JSON)",
"invalid_json": "Ongeldig JSON-formaat voor beleid."
},
"theme_switcher": {
"toggle_theme": "Thema wisselen"
},
"user_form": {
"select_role": "Selecteer een rol"
}
},
"dashboard_page": {
"title": "Dashboard",
"meta_description": "Overzicht van uw e-mailarchief.",
"header": "Dashboard",
"create_ingestion": "Maak een inname",
"no_ingestion_header": "U heeft geen innamebron ingesteld.",
"no_ingestion_text": "Voeg een innamebron toe om uw inboxen te archiveren.",
"total_emails_archived": "Totaal aantal gearchiveerde e-mails",
"total_storage_used": "Totaal gebruikte opslag",
"failed_ingestions": "Mislukte innames (laatste 7 dagen)",
"ingestion_history": "Innamegeschiedenis",
"no_ingestion_history": "Geen innamegeschiedenis beschikbaar.",
"storage_by_source": "Opslag per innamebron",
"no_ingestion_sources": "Geen innamebronnen beschikbaar.",
"indexed_insights": "Geïndexeerde inzichten",
"top_10_senders": "Top 10 afzenders",
"no_indexed_insights": "Geen geïndexeerde inzichten beschikbaar."
}
}
}

View File

@@ -0,0 +1,245 @@
{
"app": {
"auth": {
"login": "Login",
"login_tip": "Digite seu e-mail abaixo para fazer login em sua conta.",
"email": "E-mail",
"password": "Senha"
},
"common": {
"working": "Trabalhando"
},
"archive": {
"title": "Arquivo",
"no_subject": "Sem assunto",
"from": "De",
"sent": "Enviado",
"recipients": "Destinatários",
"to": "Para",
"meta_data": "Metadados",
"folder": "Pasta",
"tags": "Tags",
"size": "Tamanho",
"email_preview": "Visualização de e-mail",
"attachments": "Anexos",
"download": "Baixar",
"actions": "Ações",
"download_eml": "Baixar e-mail (.eml)",
"delete_email": "Excluir e-mail",
"email_thread": "Thread de e-mail",
"delete_confirmation_title": "Tem certeza de que deseja excluir este e-mail?",
"delete_confirmation_description": "Esta ação não pode ser desfeita e removerá permanentemente o e-mail e seus anexos.",
"deleting": "Excluindo",
"confirm": "Confirmar",
"cancel": "Cancelar",
"not_found": "E-mail não encontrado."
},
"ingestions": {
"title": "Fontes de ingestão",
"ingestion_sources": "Fontes de ingestão",
"bulk_actions": "Ações em massa",
"force_sync": "Forçar sincronização",
"delete": "Excluir",
"create_new": "Criar novo",
"name": "Nome",
"provider": "Provedor",
"status": "Status",
"active": "Ativo",
"created_at": "Criado em",
"actions": "Ações",
"last_sync_message": "Última mensagem de sincronização",
"empty": "Vazio",
"open_menu": "Abrir menu",
"edit": "Editar",
"create": "Criar",
"ingestion_source": "Fonte de ingestão",
"edit_description": "Faça alterações em sua fonte de ingestão aqui.",
"create_description": "Adicione uma nova fonte de ingestão para começar a arquivar e-mails.",
"read": "Ler",
"docs_here": "documentos aqui",
"delete_confirmation_title": "Tem certeza de que deseja excluir esta ingestão?",
"delete_confirmation_description": "Isso excluirá todos os e-mails arquivados, anexos, indexação e arquivos associados a esta ingestão. Se você deseja apenas parar de sincronizar novos e-mails, pode pausar a ingestão.",
"deleting": "Excluindo",
"confirm": "Confirmar",
"cancel": "Cancelar",
"bulk_delete_confirmation_title": "Tem certeza de que deseja excluir {{count}} ingestões selecionadas?",
"bulk_delete_confirmation_description": "Isso excluirá todos os e-mails arquivados, anexos, indexação e arquivos associados a essas ingestões. Se você deseja apenas parar de sincronizar novos e-mails, pode pausar as ingestões."
},
"search": {
"title": "Pesquisar",
"description": "Pesquisar e-mails arquivados.",
"email_search": "Pesquisa de e-mail",
"placeholder": "Pesquisar por palavra-chave, remetente, destinatário...",
"search_button": "Pesquisar",
"search_options": "Opções de pesquisa",
"strategy_fuzzy": "Fuzzy",
"strategy_verbatim": "Verbatim",
"strategy_frequency": "Frequência",
"select_strategy": "Selecione uma estratégia",
"error": "Erro",
"found_results_in": "Encontrados {{total}} resultados em {{seconds}}s",
"found_results": "Encontrados {{total}} resultados",
"from": "De",
"to": "Para",
"in_email_body": "No corpo do e-mail",
"in_attachment": "No anexo: {{filename}}",
"prev": "Anterior",
"next": "Próximo"
},
"roles": {
"title": "Gerenciamento de funções",
"role_management": "Gerenciamento de funções",
"create_new": "Criar novo",
"name": "Nome",
"created_at": "Criado em",
"actions": "Ações",
"open_menu": "Abrir menu",
"view_policy": "Visualizar política",
"edit": "Editar",
"delete": "Excluir",
"no_roles_found": "Nenhuma função encontrada.",
"role_policy": "Política de função",
"viewing_policy_for_role": "Visualizando política para a função: {{name}}",
"create": "Criar",
"role": "Função",
"edit_description": "Faça alterações na função aqui.",
"create_description": "Adicione uma nova função ao sistema.",
"delete_confirmation_title": "Tem certeza de que deseja excluir esta função?",
"delete_confirmation_description": "Esta ação não pode ser desfeita. Isso excluirá permanentemente a função.",
"deleting": "Excluindo",
"confirm": "Confirmar",
"cancel": "Cancelar"
},
"system_settings": {
"title": "Configurações do sistema",
"system_settings": "Configurações do sistema",
"description": "Gerenciar configurações globais do aplicativo.",
"language": "Idioma",
"default_theme": "Tema padrão",
"light": "Claro",
"dark": "Escuro",
"system": "Sistema",
"support_email": "E-mail de suporte",
"saving": "Salvando",
"save_changes": "Salvar alterações"
},
"users": {
"title": "Gerenciamento de usuários",
"user_management": "Gerenciamento de usuários",
"create_new": "Criar novo",
"name": "Nome",
"email": "E-mail",
"role": "Função",
"created_at": "Criado em",
"actions": "Ações",
"open_menu": "Abrir menu",
"edit": "Editar",
"delete": "Excluir",
"no_users_found": "Nenhum usuário encontrado.",
"create": "Criar",
"user": "Usuário",
"edit_description": "Faça alterações no usuário aqui.",
"create_description": "Adicione um novo usuário ao sistema.",
"delete_confirmation_title": "Tem certeza de que deseja excluir este usuário?",
"delete_confirmation_description": "Esta ação não pode ser desfeita. Isso excluirá permanentemente o usuário e removerá seus dados de nossos servidores.",
"deleting": "Excluindo",
"confirm": "Confirmar",
"cancel": "Cancelar"
},
"setup": {
"title": "Configuração",
"description": "Configure a conta de administrador inicial para o Open Archiver.",
"welcome": "Bem-vindo",
"create_admin_account": "Crie a primeira conta de administrador para começar.",
"first_name": "Primeiro nome",
"last_name": "Último nome",
"email": "E-mail",
"password": "Senha",
"creating_account": "Criando conta",
"create_account": "Criar conta"
},
"layout": {
"dashboard": "Painel",
"ingestions": "Ingestões",
"archived_emails": "E-mails arquivados",
"search": "Pesquisar",
"settings": "Configurações",
"system": "Sistema",
"users": "Usuários",
"roles": "Funções",
"logout": "Sair"
},
"components": {
"charts": {
"emails_ingested": "E-mails ingeridos",
"storage_used": "Armazenamento usado",
"emails": "E-mails"
},
"common": {
"submitting": "Enviando...",
"submit": "Enviar",
"save": "Salvar"
},
"email_preview": {
"loading": "Carregando visualização de e-mail...",
"render_error": "Não foi possível renderizar a visualização do e-mail.",
"not_available": "Arquivo .eml bruto não disponível para este e-mail."
},
"footer": {
"all_rights_reserved": "Todos os direitos reservados."
},
"ingestion_source_form": {
"provider_generic_imap": "IMAP genérico",
"provider_google_workspace": "Google Workspace",
"provider_microsoft_365": "Microsoft 365",
"provider_pst_import": "Importação de PST",
"provider_eml_import": "Importação de EML",
"select_provider": "Selecione um provedor",
"service_account_key": "Chave da conta de serviço (JSON)",
"service_account_key_placeholder": "Cole o conteúdo JSON da sua chave de conta de serviço",
"impersonated_admin_email": "E-mail de administrador representado",
"client_id": "ID do aplicativo (cliente)",
"client_secret": "Valor secreto do cliente",
"client_secret_placeholder": "Digite o valor secreto, não o ID secreto",
"tenant_id": "ID do diretório (locatário)",
"host": "Host",
"port": "Porta",
"username": "Nome de usuário",
"use_tls": "Usar TLS",
"pst_file": "Arquivo PST",
"eml_file": "Arquivo EML",
"heads_up": "Atenção!",
"org_wide_warning": "Observe que esta é uma operação em toda a organização. Esse tipo de ingestão importará e indexará <b>todas</b> as caixas de correio de e-mail em sua organização. Se você deseja importar apenas caixas de correio de e-mail específicas, use o conector IMAP.",
"upload_failed": "Falha no upload, tente novamente"
},
"role_form": {
"policies_json": "Políticas (JSON)",
"invalid_json": "Formato JSON inválido para políticas."
},
"theme_switcher": {
"toggle_theme": "Alternar tema"
},
"user_form": {
"select_role": "Selecione uma função"
}
},
"dashboard_page": {
"title": "Painel",
"meta_description": "Visão geral do seu arquivo de e-mail.",
"header": "Painel",
"create_ingestion": "Criar uma ingestão",
"no_ingestion_header": "Você não tem nenhuma fonte de ingestão configurada.",
"no_ingestion_text": "Adicione uma fonte de ingestão para começar a arquivar suas caixas de entrada.",
"total_emails_archived": "Total de e-mails arquivados",
"total_storage_used": "Armazenamento total usado",
"failed_ingestions": "Ingestões com falha (últimos 7 dias)",
"ingestion_history": "Histórico de ingestão",
"no_ingestion_history": "Nenhum histórico de ingestão disponível.",
"storage_by_source": "Armazenamento por fonte de ingestão",
"no_ingestion_sources": "Nenhuma fonte de ingestão disponível.",
"indexed_insights": "Informações indexadas",
"top_10_senders": "10 principais remetentes",
"no_indexed_insights": "Nenhuma informação indexada disponível."
}
}
}

View File

@@ -2,6 +2,7 @@ import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import 'dotenv/config';
import { api } from '$lib/server/api';
import type { SystemSettings } from '@open-archiver/types';
export const load: LayoutServerLoad = async (event) => {
const { locals, url } = event;
@@ -21,7 +22,7 @@ export const load: LayoutServerLoad = async (event) => {
}
const settingsResponse = await api('/settings', event);
const settings = settingsResponse.ok ? await settingsResponse.json() : null;
const settings: SystemSettings | null = settingsResponse.ok ? await settingsResponse.json() : null;
return {
user: locals.user,

View File

@@ -0,0 +1,21 @@
import { loadTranslations } from '$lib/translations';
import type { LayoutLoad } from './$types';
import { browser } from '$app/environment';
import type { SupportedLanguage } from '@open-archiver/types';
export const load: LayoutLoad = async ({ url, data }) => {
const { pathname } = url;
let initLocale: SupportedLanguage = 'en'; // Default fallback
if (data.settings?.language) {
initLocale = data.settings.language;
}
console.log(initLocale);
await loadTranslations(initLocale, pathname);
return {
...data
};
};

View File

@@ -5,6 +5,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/state';
import ThemeSwitcher from '$lib/components/custom/ThemeSwitcher.svelte';
import { t } from '$lib/translations';
const navItems: {
href?: string;
label: string;
@@ -13,24 +14,24 @@
label: string;
}[];
}[] = [
{ href: '/dashboard', label: 'Dashboard' },
{ href: '/dashboard/ingestions', label: 'Ingestions' },
{ href: '/dashboard/archived-emails', label: 'Archived emails' },
{ href: '/dashboard/search', label: 'Search' },
{ href: '/dashboard', label: $t('app.layout.dashboard') },
{ href: '/dashboard/ingestions', label: $t('app.layout.ingestions') },
{ href: '/dashboard/archived-emails', label: $t('app.layout.archived_emails') },
{ href: '/dashboard/search', label: $t('app.layout.search') },
{
label: 'Settings',
label: $t('app.layout.settings'),
subMenu: [
{
href: '/dashboard/settings/system',
label: 'System',
label: $t('app.layout.system'),
},
{
href: '/dashboard/settings/users',
label: 'Users',
label: $t('app.layout.users'),
},
{
href: '/dashboard/settings/roles',
label: 'Roles',
label: $t('app.layout.roles'),
},
],
},
@@ -90,7 +91,7 @@
</NavigationMenu.Root>
<div class="flex items-center gap-4">
<ThemeSwitcher />
<Button onclick={handleLogout} variant="outline">Logout</Button>
<Button onclick={handleLogout} variant="outline">{$t('app.layout.logout')}</Button>
</div>
</div>
</header>

View File

@@ -8,6 +8,7 @@
import TopSendersChart from '$lib/components/custom/charts/TopSendersChart.svelte';
import IngestionHistoryChart from '$lib/components/custom/charts/IngestionHistoryChart.svelte';
import StorageBySourceChart from '$lib/components/custom/charts/StorageBySourceChart.svelte';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
const transformedHistory = $derived(
@@ -19,20 +20,20 @@
</script>
<svelte:head>
<title>Dashboard - OpenArchiver</title>
<meta name="description" content="Overview of your email archive." />
<title>{$t('app.dashboard_page.title')} - OpenArchiver</title>
<meta name="description" content={$t('app.dashboard_page.meta_description')} />
</svelte:head>
<div class="flex-1 space-y-4">
<div class="flex items-center justify-between space-y-2">
<h2 class="text-3xl font-bold tracking-tight">Dashboard</h2>
<h2 class="text-3xl font-bold tracking-tight">{$t('app.dashboard_page.header')}</h2>
</div>
{#if !data.ingestionSources || data.ingestionSources?.length === 0}
<div>
<EmptyState
buttonText="Create an ingestion"
header="You don't have any ingestion source set up."
text="Add an ingestion source to start archiving your inboxes."
buttonText={$t('app.dashboard_page.create_ingestion')}
header={$t('app.dashboard_page.no_ingestion_header')}
text={$t('app.dashboard_page.no_ingestion_text')}
click={() => {
goto('/dashboard/ingestions');
}}
@@ -47,9 +48,9 @@
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium"
>Total Emails Archived</Card.Title
>
<Card.Title class="text-sm font-medium">
{$t('app.dashboard_page.total_emails_archived')}
</Card.Title>
<Archive class="text-muted-foreground h-4 w-4" />
</Card.Header>
<Card.Content>
@@ -62,7 +63,9 @@
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium">Total Storage Used</Card.Title>
<Card.Title class="text-sm font-medium"
>{$t('app.dashboard_page.total_storage_used')}</Card.Title
>
<HardDrive class="text-muted-foreground h-4 w-4" />
</Card.Header>
<Card.Content>
@@ -75,9 +78,9 @@
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium"
>Failed Ingestions (Last 7 Days)</Card.Title
>
<Card.Title class="text-sm font-medium">
{$t('app.dashboard_page.failed_ingestions')}
</Card.Title>
<CircleAlert class=" text-muted-foreground h-4 w-4" />
</Card.Header>
<Card.Content>
@@ -97,13 +100,13 @@
<div class=" lg:col-span-2">
<Card.Root>
<Card.Header>
<Card.Title>Ingestion History</Card.Title>
<Card.Title>{$t('app.dashboard_page.ingestion_history')}</Card.Title>
</Card.Header>
<Card.Content class=" pl-4">
{#if transformedHistory.length > 0}
<IngestionHistoryChart data={transformedHistory} />
{:else}
<p>No ingestion history available.</p>
<p>{$t('app.dashboard_page.no_ingestion_history')}</p>
{/if}
</Card.Content>
</Card.Root>
@@ -111,31 +114,33 @@
<div class=" lg:col-span-1">
<Card.Root class="h-full">
<Card.Header>
<Card.Title>Storage by Ingestion Source</Card.Title>
<Card.Title>{$t('app.dashboard_page.storage_by_source')}</Card.Title>
</Card.Header>
<Card.Content class="h-full">
{#if data.ingestionSources && data.ingestionSources.length > 0}
<StorageBySourceChart data={data.ingestionSources} />
{:else}
<p>No ingestion sources available.</p>
<p>{$t('app.dashboard_page.no_ingestion_sources')}</p>
{/if}
</Card.Content>
</Card.Root>
</div>
</div>
<div>
<h1 class="text-xl font-semibold leading-6">Indexed insights</h1>
<h1 class="text-xl font-semibold leading-6">
{$t('app.dashboard_page.indexed_insights')}
</h1>
</div>
<div class="grid grid-cols-1">
<Card.Root>
<Card.Header>
<Card.Title>Top 10 Senders</Card.Title>
<Card.Title>{$t('app.dashboard_page.top_10_senders')}</Card.Title>
</Card.Header>
<Card.Content>
{#if data.indexedInsights && data.indexedInsights.topSenders.length > 0}
<TopSendersChart data={data.indexedInsights.topSenders} />
{:else}
<p>No indexed insights available.</p>
<p>{$t('app.dashboard_page.no_indexed_insights')}</p>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -4,6 +4,7 @@
import { Button } from '$lib/components/ui/button';
import * as Select from '$lib/components/ui/select';
import { goto } from '$app/navigation';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
@@ -68,11 +69,11 @@
</script>
<svelte:head>
<title>Archived emails - OpenArchiver</title>
<title>{$t('app.archived_emails_page.title')} - OpenArchiver</title>
</svelte:head>
<div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">Archived Emails</h1>
<h1 class="text-2xl font-bold">{$t('app.archived_emails_page.header')}</h1>
{#if ingestionSources.length > 0}
<div class="w-[250px]">
<Select.Root
@@ -84,7 +85,7 @@
<span
>{selectedIngestionSourceId
? ingestionSources.find((s) => s.id === selectedIngestionSourceId)?.name
: 'Select an ingestion source'}</span
: $t('app.archived_emails_page.select_ingestion_source')}</span
>
</Select.Trigger>
<Select.Content>
@@ -101,12 +102,12 @@
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Date</Table.Head>
<Table.Head>Subject</Table.Head>
<Table.Head>Sender</Table.Head>
<Table.Head>Inbox</Table.Head>
<Table.Head>Path</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
<Table.Head>{$t('app.archived_emails_page.date')}</Table.Head>
<Table.Head>{$t('app.archived_emails_page.subject')}</Table.Head>
<Table.Head>{$t('app.archived_emails_page.sender')}</Table.Head>
<Table.Head>{$t('app.archived_emails_page.inbox')}</Table.Head>
<Table.Head>{$t('app.archived_emails_page.path')}</Table.Head>
<Table.Head class="text-right">{$t('app.archived_emails_page.actions')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body class="text-sm">
@@ -135,7 +136,9 @@
</Table.Cell>
<Table.Cell class="text-right">
<a href={`/dashboard/archived-emails/${email.id}`}>
<Button variant="outline">View</Button>
<Button variant="outline"
>{$t('app.archived_emails_page.view')}</Button
>
</a>
</Table.Cell>
</Table.Row>
@@ -143,7 +146,7 @@
{:else}
<Table.Row>
<Table.Cell colspan={5} class="text-center"
>No archived emails found.</Table.Cell
>{$t('app.archived_emails_page.no_emails_found')}</Table.Cell
>
</Table.Row>
{/if}
@@ -159,7 +162,9 @@
}&limit=${archivedEmails.limit}`}
class={archivedEmails.page === 1 ? 'pointer-events-none' : ''}
>
<Button variant="outline" disabled={archivedEmails.page === 1}>Prev</Button>
<Button variant="outline" disabled={archivedEmails.page === 1}
>{$t('app.archived_emails_page.prev')}</Button
>
</a>
{#each paginationItems as item}
@@ -187,7 +192,8 @@
<Button
variant="outline"
disabled={archivedEmails.page ===
Math.ceil(archivedEmails.total / archivedEmails.limit)}>Next</Button
Math.ceil(archivedEmails.total / archivedEmails.limit)}
>{$t('app.archived_emails_page.next')}</Button
>
</a>
</div>

View File

@@ -10,6 +10,7 @@
import { goto } from '$app/navigation';
import * as Dialog from '$lib/components/ui/dialog';
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
let email = $derived(data.email);
@@ -72,7 +73,7 @@
</script>
<svelte:head>
<title>{email?.subject} | Archived emails - OpenArchiver</title>
<title>{email?.subject} | {$t('app.archive.title')} - OpenArchiver</title>
</svelte:head>
{#if email}
@@ -80,29 +81,31 @@
<div class="col-span-3 md:col-span-2">
<Card.Root>
<Card.Header>
<Card.Title>{email.subject || 'No Subject'}</Card.Title>
<Card.Title>{email.subject || $t('app.archive.no_subject')}</Card.Title>
<Card.Description>
From: {email.senderEmail || email.senderName} | Sent: {new Date(
email.sentAt
).toLocaleString()}
{$t('app.archive.from')}: {email.senderEmail || email.senderName} | {$t(
'app.archive.sent'
)}: {new Date(email.sentAt).toLocaleString()}
</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-4">
<div class="space-y-1">
<h3 class="font-semibold">Recipients</h3>
<h3 class="font-semibold">{$t('app.archive.recipients')}</h3>
<Card.Description>
<p>
To: {email.recipients.map((r) => r.email || r.name).join(', ')}
{$t('app.archive.to')}: {email.recipients
.map((r) => r.email || r.name)
.join(', ')}
</p>
</Card.Description>
</div>
<div class=" space-y-1">
<h3 class="font-semibold">Meta data</h3>
<h3 class="font-semibold">{$t('app.archive.meta_data')}</h3>
<Card.Description class="space-y-2">
{#if email.path}
<div class="flex flex-wrap items-center gap-2">
<span>Folder:</span>
<span>{$t('app.archive.folder')}:</span>
<span class=" bg-muted truncate rounded p-1.5 text-xs"
>{email.path || '/'}</span
>
@@ -110,7 +113,7 @@
{/if}
{#if email.tags && email.tags.length > 0}
<div class="flex flex-wrap items-center gap-2">
<span> Tags: </span>
<span> {$t('app.archive.tags')}: </span>
{#each email.tags as tag}
<span class=" bg-muted truncate rounded p-1.5 text-xs"
>{tag}</span
@@ -119,7 +122,7 @@
</div>
{/if}
<div class="flex flex-wrap items-center gap-2">
<span>size:</span>
<span>{$t('app.archive.size')}:</span>
<span class=" bg-muted truncate rounded p-1.5 text-xs"
>{formatBytes(email.sizeBytes)}</span
>
@@ -127,12 +130,14 @@
</Card.Description>
</div>
<div>
<h3 class="font-semibold">Email Preview</h3>
<h3 class="font-semibold">{$t('app.archive.email_preview')}</h3>
<EmailPreview raw={email.raw} />
</div>
{#if email.attachments && email.attachments.length > 0}
<div>
<h3 class="font-semibold">Attachments</h3>
<h3 class="font-semibold">
{$t('app.archive.attachments')}
</h3>
<ul class="mt-2 space-y-2">
{#each email.attachments as attachment}
<li
@@ -152,7 +157,7 @@
attachment.filename
)}
>
Download
{$t('app.archive.download')}
</Button>
</li>
{/each}
@@ -166,16 +171,16 @@
<div class="col-span-3 space-y-6 md:col-span-1">
<Card.Root>
<Card.Header>
<Card.Title>Actions</Card.Title>
<Card.Title>{$t('app.archive.actions')}</Card.Title>
</Card.Header>
<Card.Content class="space-y-2">
<Button
onclick={() =>
download(email.storagePath, `${email.subject || 'email'}.eml`)}
>Download Email (.eml)</Button
>{$t('app.archive.download_eml')}</Button
>
<Button variant="destructive" onclick={() => (isDeleteDialogOpen = true)}>
Delete Email
{$t('app.archive.delete_email')}
</Button>
</Card.Content>
</Card.Root>
@@ -183,7 +188,7 @@
{#if email.thread && email.thread.length > 1}
<Card.Root>
<Card.Header>
<Card.Title>Email thread</Card.Title>
<Card.Title>{$t('app.archive.email_thread')}</Card.Title>
</Card.Header>
<Card.Content>
<EmailThread thread={email.thread} currentEmailId={email.id} />
@@ -196,10 +201,9 @@
<Dialog.Root bind:open={isDeleteDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>Are you sure you want to delete this email?</Dialog.Title>
<Dialog.Title>{$t('app.archive.delete_confirmation_title')}</Dialog.Title>
<Dialog.Description>
This action cannot be undone and will permanently remove the email and its
attachments.
{$t('app.archive.delete_confirmation_description')}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="sm:justify-start">
@@ -209,14 +213,18 @@
onclick={confirmDelete}
disabled={isDeleting}
>
{#if isDeleting}Deleting...{:else}Confirm{/if}
{#if isDeleting}
{$t('app.archive.deleting')}...
{:else}
{$t('app.archive.confirm')}
{/if}
</Button>
<Dialog.Close>
<Button type="button" variant="secondary">Cancel</Button>
<Button type="button" variant="secondary">{$t('app.archive.cancel')}</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
{:else}
<p>Email not found.</p>
<p>{$t('app.archive.not_found')}</p>
{/if}

View File

@@ -13,6 +13,7 @@
import Badge from '$lib/components/ui/badge/badge.svelte';
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
let ingestionSources = $state(data.ingestionSources);
@@ -266,38 +267,40 @@
</script>
<svelte:head>
<title>Ingestion sources - OpenArchiver</title>
<title>{$t('app.ingestions.title')} - OpenArchiver</title>
</svelte:head>
<div class="">
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-4">
<h1 class="text-2xl font-bold">Ingestion Sources</h1>
<h1 class="text-2xl font-bold">{$t('app.ingestions.ingestion_sources')}</h1>
{#if selectedIds.length > 0}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="outline">
Bulk Actions ({selectedIds.length})
{$t('app.ingestions.bulk_actions')} ({selectedIds.length})
<MoreHorizontal class="ml-2 h-4 w-4" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item onclick={handleBulkForceSync}>
<RefreshCw class="mr-2 h-4 w-4" />
Force Sync
{$t('app.ingestions.force_sync')}
</DropdownMenu.Item>
<DropdownMenu.Item
class="text-red-600"
onclick={() => (isBulkDeleteDialogOpen = true)}
>
<Trash class="mr-2 h-4 w-4" />
Delete
{$t('app.ingestions.delete')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
</div>
<Button onclick={openCreateDialog} disabled={data.isDemo}>Create New</Button>
<Button onclick={openCreateDialog} disabled={data.isDemo}
>{$t('app.ingestions.create_new')}</Button
>
</div>
<div class="rounded-md border">
@@ -319,12 +322,12 @@
: ((selectedIds.length > 0 ? 'indeterminate' : false) as any)}
/>
</Table.Head>
<Table.Head>Name</Table.Head>
<Table.Head>Provider</Table.Head>
<Table.Head>Status</Table.Head>
<Table.Head>Active</Table.Head>
<Table.Head>Created At</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
<Table.Head>{$t('app.ingestions.name')}</Table.Head>
<Table.Head>{$t('app.ingestions.provider')}</Table.Head>
<Table.Head>{$t('app.ingestions.status')}</Table.Head>
<Table.Head>{$t('app.ingestions.active')}</Table.Head>
<Table.Head>{$t('app.ingestions.created_at')}</Table.Head>
<Table.Head class="text-right">{$t('app.ingestions.actions')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -369,8 +372,9 @@
<HoverCard.Content class="{getStatusClasses(source.status)} ">
<div class="flex flex-col space-y-4 text-sm">
<p class=" font-mono">
<b>Last sync message:</b>
{source.lastSyncStatusMessage || 'Empty'}
<b>{$t('app.ingestions.last_sync_message')}:</b>
{source.lastSyncStatusMessage ||
$t('app.ingestions.empty')}
</p>
</div>
</HoverCard.Content>
@@ -393,23 +397,27 @@
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" class="h-8 w-8 p-0">
<span class="sr-only">Open menu</span>
<span class="sr-only"
>{$t('app.ingestions.open_menu')}</span
>
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Label
>{$t('app.ingestions.actions')}</DropdownMenu.Label
>
<DropdownMenu.Item onclick={() => openEditDialog(source)}
>Edit</DropdownMenu.Item
>{$t('app.ingestions.edit')}</DropdownMenu.Item
>
<DropdownMenu.Item onclick={() => handleSync(source.id)}
>Force sync</DropdownMenu.Item
>{$t('app.ingestions.force_sync')}</DropdownMenu.Item
>
<DropdownMenu.Separator />
<DropdownMenu.Item
class="text-red-600"
onclick={() => openDeleteDialog(source)}
>Delete</DropdownMenu.Item
>{$t('app.ingestions.delete')}</DropdownMenu.Item
>
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -429,17 +437,21 @@
<Dialog.Root bind:open={isDialogOpen}>
<Dialog.Content class="sm:max-w-120 md:max-w-180">
<Dialog.Header>
<Dialog.Title>{selectedSource ? 'Edit' : 'Create'} Ingestion Source</Dialog.Title>
<Dialog.Title
>{selectedSource ? $t('app.ingestions.edit') : $t('app.ingestions.create')}{' '}
{$t('app.ingestions.ingestion_source')}</Dialog.Title
>
<Dialog.Description>
{selectedSource
? 'Make changes to your ingestion source here.'
: 'Add a new ingestion source to start archiving emails.'}
? $t('app.ingestions.edit_description')
: $t('app.ingestions.create_description')}
<span
>Read <a
>{$t('app.ingestions.read')}{' '}
<a
class="text-primary underline underline-offset-2"
target="_blank"
href="https://docs.openarchiver.com/user-guides/email-providers/"
>docs here</a
>{$t('app.ingestions.docs_here')}</a
>.</span
>
</Dialog.Description>
@@ -451,11 +463,9 @@
<Dialog.Root bind:open={isDeleteDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>Are you sure you want to delete this ingestion?</Dialog.Title>
<Dialog.Title>{$t('app.ingestions.delete_confirmation_title')}</Dialog.Title>
<Dialog.Description>
This will delete all archived emails, attachments, indexing, and files associated
with this ingestion. If you only want to stop syncing new emails, you can pause the
ingestion instead.
{$t('app.ingestions.delete_confirmation_description')}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="sm:justify-start">
@@ -464,10 +474,14 @@
variant="destructive"
onclick={confirmDelete}
disabled={isDeleting}
>{#if isDeleting}Deleting...{:else}Confirm{/if}</Button
>{#if isDeleting}
{$t('app.ingestions.deleting')}...
{:else}
{$t('app.ingestions.confirm')}
{/if}</Button
>
<Dialog.Close>
<Button type="button" variant="secondary">Cancel</Button>
<Button type="button" variant="secondary">{$t('app.ingestions.cancel')}</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
@@ -477,12 +491,12 @@
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title
>Are you sure you want to delete {selectedIds.length} selected ingestions?</Dialog.Title
>{$t('app.ingestions.bulk_delete_confirmation_title', {
count: selectedIds.length,
} as any)}</Dialog.Title
>
<Dialog.Description>
This will delete all archived emails, attachments, indexing, and files associated
with these ingestions. If you only want to stop syncing new emails, you can pause
the ingestions instead.
{$t('app.ingestions.bulk_delete_confirmation_description')}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="sm:justify-start">
@@ -491,10 +505,14 @@
variant="destructive"
onclick={handleBulkDelete}
disabled={isDeleting}
>{#if isDeleting}Deleting...{:else}Confirm{/if}</Button
>{#if isDeleting}
{$t('app.ingestions.deleting')}...
{:else}
{$t('app.ingestions.confirm')}
{/if}</Button
>
<Dialog.Close>
<Button type="button" variant="secondary">Cancel</Button>
<Button type="button" variant="secondary">{$t('app.ingestions.cancel')}</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -16,6 +16,7 @@
import type { MatchingStrategy } from '@open-archiver/types';
import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
import * as Alert from '$lib/components/ui/alert/index.js';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
let searchResult = $derived(data.searchResult);
@@ -27,13 +28,14 @@
);
const strategies = [
{ value: 'last', label: 'Fuzzy' },
{ value: 'all', label: 'Verbatim' },
{ value: 'frequency', label: 'Frequency' },
{ value: 'last', label: $t('app.search.strategy_fuzzy') },
{ value: 'all', label: $t('app.search.strategy_verbatim') },
{ value: 'frequency', label: $t('app.search.strategy_frequency') },
];
const triggerContent = $derived(
strategies.find((s) => s.value === matchingStrategy)?.label ?? 'Select a strategy'
strategies.find((s) => s.value === matchingStrategy)?.label ??
$t('app.search.select_strategy')
);
let isMounted = $state(false);
@@ -170,25 +172,27 @@
</script>
<svelte:head>
<title>Search | Open Archiver</title>
<meta name="description" content="Search for archived emails." />
<title>{$t('app.search.title')} | Open Archiver</title>
<meta name="description" content={$t('app.search.description')} />
</svelte:head>
<div class="container mx-auto p-4 md:p-8">
<h1 class="mb-4 text-2xl font-bold">Email Search</h1>
<h1 class="mb-4 text-2xl font-bold">{$t('app.search.email_search')}</h1>
<form onsubmit={handleSearch} class="mb-8 flex flex-col space-y-2">
<div class="flex items-center gap-2">
<Input
type="search"
name="keywords"
placeholder="Search by keyword, sender, recipient..."
placeholder={$t('app.search.placeholder')}
class=" h-12 flex-grow"
bind:value={keywords}
/>
<Button type="submit" class="h-12 cursor-pointer">Search</Button>
<Button type="submit" class="h-12 cursor-pointer"
>{$t('app.search.search_button')}</Button
>
</div>
<div class="mt-1 text-xs font-medium">Search options</div>
<div class="mt-1 text-xs font-medium">{$t('app.search.search_options')}</div>
<div class="flex items-center gap-2">
<Select.Root type="single" name="matchingStrategy" bind:value={matchingStrategy}>
<Select.Trigger class=" w-[180px] cursor-pointer">
@@ -212,7 +216,7 @@
{#if error}
<Alert.Root variant="destructive">
<CircleAlertIcon class="size-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Title>{$t('app.search.error')}</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
@@ -220,9 +224,12 @@
{#if searchResult}
<p class="text-muted-foreground mb-4">
{#if searchResult.total > 0}
Found {searchResult.total} results in {searchResult.processingTimeMs / 1000}s
{$t('app.search.found_results_in', {
total: searchResult.total,
seconds: searchResult.processingTimeMs / 1000,
} as any)}
{:else}
Found {searchResult.total} results
{$t('app.search.found_results', { total: searchResult.total } as any)}
{/if}
</p>
@@ -240,7 +247,7 @@
{/if}
</CardTitle>
<CardDescription class="flex items-center space-x-1">
<span>From:</span>
<span>{$t('app.search.from')}:</span>
{#if !isMounted}
<span class="bg-accent h-4 w-40 animate-pulse rounded-md"
></span>
@@ -251,7 +258,7 @@
></span>
{/if}
<span class="mx-2">|</span>
<span>To:</span>
<span>{$t('app.search.to')}:</span>
{#if !isMounted}
<span class="bg-accent h-4 w-40 animate-pulse rounded-md"
></span>
@@ -280,7 +287,9 @@
<div
class="space-y-2 rounded-md bg-slate-100 p-2 dark:bg-slate-800"
>
<p class="text-sm text-gray-500">In email body:</p>
<p class="text-sm text-gray-500">
{$t('app.search.in_email_body')}:
</p>
{#if !isMounted}
<Skeleton class="my-2 h-5 w-full bg-gray-200" />
{:else}
@@ -302,7 +311,9 @@
class="space-y-2 rounded-md bg-slate-100 p-2 dark:bg-slate-800"
>
<p class="text-sm text-gray-500">
In attachment: {attachment.filename}
{$t('app.search.in_attachment', {
filename: attachment.filename,
} as any)}
</p>
{#if !isMounted}
<Skeleton class="my-2 h-5 w-full bg-gray-200" />
@@ -331,7 +342,7 @@
}&matchingStrategy=${matchingStrategy}`}
class={page === 1 ? 'pointer-events-none' : ''}
>
<Button variant="outline" disabled={page === 1}>Prev</Button>
<Button variant="outline" disabled={page === 1}>{$t('app.search.prev')}</Button>
</a>
{#each paginationItems as item}
@@ -357,7 +368,7 @@
<Button
variant="outline"
disabled={page === Math.ceil(searchResult.total / searchResult.limit)}
>Next</Button
>{$t('app.search.next')}</Button
>
</a>
</div>

View File

@@ -9,6 +9,7 @@
import RoleForm from '$lib/components/custom/RoleForm.svelte';
import { api } from '$lib/api.client';
import type { Role } from '@open-archiver/types';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
let roles = $state(data.roles);
@@ -108,22 +109,22 @@
</script>
<svelte:head>
<title>Role Management - OpenArchiver</title>
<title>{$t('app.roles.title')} - OpenArchiver</title>
</svelte:head>
<div class="">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">Role Management</h1>
<Button onclick={openCreateDialog}>Create New</Button>
<h1 class="text-2xl font-bold">{$t('app.roles.role_management')}</h1>
<Button onclick={openCreateDialog}>{$t('app.roles.create_new')}</Button>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Name</Table.Head>
<Table.Head>Created At</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
<Table.Head>{$t('app.roles.name')}</Table.Head>
<Table.Head>{$t('app.roles.created_at')}</Table.Head>
<Table.Head class="text-right">{$t('app.roles.actions')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -136,25 +137,27 @@
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" class="h-8 w-8 p-0">
<span class="sr-only">Open menu</span>
<span class="sr-only">{$t('app.roles.open_menu')}</span>
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Label
>{$t('app.roles.actions')}</DropdownMenu.Label
>
<DropdownMenu.Item
onclick={() => openViewPolicyDialog(role)}
class="cursor-pointer"
>
<Eye class="mr-2 h-4 w-4" />
View Policy
{$t('app.roles.view_policy')}
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => openEditDialog(role)}
class="cursor-pointer"
>
<Edit class="mr-2 h-4 w-4" />
Edit
{$t('app.roles.edit')}
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
@@ -162,7 +165,7 @@
onclick={() => openDeleteDialog(role)}
>
<Trash class="mr-2 h-4 w-4" />
Delete
{$t('app.roles.delete')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -171,7 +174,8 @@
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center">No roles found.</Table.Cell
<Table.Cell colspan={3} class="h-24 text-center"
>{$t('app.roles.no_roles_found')}</Table.Cell
>
</Table.Row>
{/if}
@@ -183,9 +187,9 @@
<Dialog.Root bind:open={isViewPolicyDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>Role Policy</Dialog.Title>
<Dialog.Title>{$t('app.roles.role_policy')}</Dialog.Title>
<Dialog.Description>
Viewing policy for role: {selectedRole?.name}
{$t('app.roles.viewing_policy_for_role', { name: selectedRole?.name } as any)}
</Dialog.Description>
</Dialog.Header>
<div
@@ -199,9 +203,14 @@
<Dialog.Root bind:open={isFormDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>{selectedRole ? 'Edit' : 'Create'} Role</Dialog.Title>
<Dialog.Title
>{selectedRole ? $t('app.roles.edit') : $t('app.roles.create')}
{$t('app.roles.role')}</Dialog.Title
>
<Dialog.Description>
{selectedRole ? 'Make changes to the role here.' : 'Add a new role to the system.'}
{selectedRole
? $t('app.roles.edit_description')
: $t('app.roles.create_description')}
</Dialog.Description>
</Dialog.Header>
<RoleForm role={selectedRole} onSubmit={handleFormSubmit} />
@@ -211,9 +220,9 @@
<Dialog.Root bind:open={isDeleteDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>Are you sure you want to delete this role?</Dialog.Title>
<Dialog.Title>{$t('app.roles.delete_confirmation_title')}</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete the role.
{$t('app.roles.delete_confirmation_description')}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="sm:justify-start">
@@ -223,10 +232,14 @@
onclick={confirmDelete}
disabled={isDeleting}
>
{#if isDeleting}Deleting...{:else}Confirm{/if}
{#if isDeleting}
{$t('app.roles.deleting')}...
{:else}
{$t('app.roles.confirm')}
{/if}
</Button>
<Dialog.Close>
<Button type="button" variant="secondary">Cancel</Button>
<Button type="button" variant="secondary">{$t('app.roles.cancel')}</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -8,20 +8,22 @@
import * as Select from '$lib/components/ui/select';
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import type { SupportedLanguage } from '@open-archiver/types';
import { t } from '$lib/translations';
let { data, form }: { data: PageData; form: any } = $props();
let settings = $state(data.settings);
let isSaving = $state(false);
const languageOptions: { value: SupportedLanguage; label: string }[] = [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
{ value: 'de', label: 'German' },
{ value: 'it', label: 'Italian' },
{ value: 'pt', label: 'Portuguese' },
{ value: 'nl', label: 'Dutch' },
{ value: 'ja', label: 'Japanese' },
{ value: 'en', label: '🇬🇧 English' },
{ value: 'de', label: '🇩🇪 Deutsch' },
{ value: 'fr', label: '🇫🇷 Français' },
{ value: 'et', label: '🇪🇪 Eesti' },
{ value: 'es', label: '🇪🇸 Español' },
{ value: 'it', label: '🇮🇹 Italiano' },
{ value: 'pt', label: '🇵🇹 Português' },
{ value: 'nl', label: '🇳🇱 Nederlands' },
{ value: 'ja', label: '🇯🇵 日本語' },
];
const languageTriggerContent = $derived(
@@ -52,35 +54,37 @@
</script>
<svelte:head>
<title>System Settings - OpenArchiver</title>
<title>{$t('app.system_settings.title')} - OpenArchiver</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold">System Settings</h1>
<p class="text-muted-foreground">Manage global application settings.</p>
<h1 class="text-2xl font-bold">{$t('app.system_settings.system_settings')}</h1>
<p class="text-muted-foreground">{$t('app.system_settings.description')}</p>
</div>
<form method="POST" class="space-y-8" onsubmit={() => (isSaving = true)}>
<Card.Root>
<Card.Content class="space-y-4">
<!-- Hide language setting for now -->
<!-- <div class="grid gap-2">
<Label.Root class="mb-1" for="language">Language</Label.Root>
<div class="grid gap-2">
<Label.Root class="mb-1" for="language"
>{$t('app.system_settings.language')}</Label.Root
>
<Select.Root name="language" bind:value={settings.language} type="single">
<Select.Trigger class="w-[280px]">
{languageTriggerContent}
</Select.Trigger>
<Select.Content>
<Select.Content>
{#each languageOptions as lang}
<Select.Item value={lang.value}>{lang.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div> -->
</div>
<div class="grid gap-2">
<Label.Root class="mb-1">Default theme</Label.Root>
<Label.Root class="mb-1">{$t('app.system_settings.default_theme')}</Label.Root>
<RadioGroup.Root
bind:value={settings.theme}
name="theme"
@@ -88,21 +92,23 @@
>
<div class="flex items-center gap-2">
<RadioGroup.Item value="light" id="light" />
<Label.Root for="light">Light</Label.Root>
<Label.Root for="light">{$t('app.system_settings.light')}</Label.Root>
</div>
<div class="flex items-center gap-2">
<RadioGroup.Item value="dark" id="dark" />
<Label.Root for="dark">Dark</Label.Root>
<Label.Root for="dark">{$t('app.system_settings.dark')}</Label.Root>
</div>
<div class="flex items-center gap-2">
<RadioGroup.Item value="system" id="system" />
<Label.Root for="system">System</Label.Root>
<Label.Root for="system">{$t('app.system_settings.system')}</Label.Root>
</div>
</RadioGroup.Root>
</div>
<div class="grid gap-2">
<Label.Root class="mb-1" for="supportEmail">Support Email</Label.Root>
<Label.Root class="mb-1" for="supportEmail"
>{$t('app.system_settings.support_email')}</Label.Root
>
<Input
id="supportEmail"
name="supportEmail"
@@ -115,7 +121,11 @@
</Card.Content>
<Card.Footer class="border-t px-6 py-4">
<Button type="submit" disabled={isSaving}>
{#if isSaving}Saving...{:else}Save Changes{/if}
{#if isSaving}
{$t('app.system_settings.saving')}...
{:else}
{$t('app.system_settings.save_changes')}
{/if}
</Button>
</Card.Footer>
</Card.Root>

View File

@@ -9,6 +9,7 @@
import UserForm from '$lib/components/custom/UserForm.svelte';
import { api } from '$lib/api.client';
import type { User } from '@open-archiver/types';
import { t } from '$lib/translations';
let { data }: { data: PageData } = $props();
let users = $state(data.users);
@@ -103,24 +104,24 @@
</script>
<svelte:head>
<title>User Management - OpenArchiver</title>
<title>{$t('app.users.title')} - OpenArchiver</title>
</svelte:head>
<div class="">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">User Management</h1>
<Button onclick={openCreateDialog}>Create New</Button>
<h1 class="text-2xl font-bold">{$t('app.users.user_management')}</h1>
<Button onclick={openCreateDialog}>{$t('app.users.create_new')}</Button>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>Name</Table.Head>
<Table.Head>Email</Table.Head>
<Table.Head>Role</Table.Head>
<Table.Head>Created At</Table.Head>
<Table.Head class="text-right">Actions</Table.Head>
<Table.Head>{$t('app.users.name')}</Table.Head>
<Table.Head>{$t('app.users.email')}</Table.Head>
<Table.Head>{$t('app.users.role')}</Table.Head>
<Table.Head>{$t('app.users.created_at')}</Table.Head>
<Table.Head class="text-right">{$t('app.users.actions')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -135,18 +136,20 @@
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" class="h-8 w-8 p-0">
<span class="sr-only">Open menu</span>
<span class="sr-only">{$t('app.users.open_menu')}</span>
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Label
>{$t('app.users.actions')}</DropdownMenu.Label
>
<DropdownMenu.Item
onclick={() => openEditDialog(user)}
class="cursor-pointer"
>
<Edit class="mr-2 h-4 w-4" />
Edit</DropdownMenu.Item
{$t('app.users.edit')}</DropdownMenu.Item
>
<DropdownMenu.Separator />
<DropdownMenu.Item
@@ -154,7 +157,7 @@
onclick={() => openDeleteDialog(user)}
>
<Trash class="mr-2 h-4 w-4" />
Delete
{$t('app.users.delete')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -163,7 +166,8 @@
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-24 text-center">No users found.</Table.Cell
<Table.Cell colspan={5} class="h-24 text-center"
>{$t('app.users.no_users_found')}</Table.Cell
>
</Table.Row>
{/if}
@@ -175,9 +179,14 @@
<Dialog.Root bind:open={isDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>{selectedUser ? 'Edit' : 'Create'} User</Dialog.Title>
<Dialog.Title
>{selectedUser ? $t('app.users.edit') : $t('app.users.create')}
{$t('app.users.user')}</Dialog.Title
>
<Dialog.Description>
{selectedUser ? 'Make changes to the user here.' : 'Add a new user to the system.'}
{selectedUser
? $t('app.users.edit_description')
: $t('app.users.create_description')}
</Dialog.Description>
</Dialog.Header>
<UserForm {roles} user={selectedUser} onSubmit={handleFormSubmit} />
@@ -187,10 +196,9 @@
<Dialog.Root bind:open={isDeleteDialogOpen}>
<Dialog.Content class="sm:max-w-lg">
<Dialog.Header>
<Dialog.Title>Are you sure you want to delete this user?</Dialog.Title>
<Dialog.Title>{$t('app.users.delete_confirmation_title')}</Dialog.Title>
<Dialog.Description>
This action cannot be undone. This will permanently delete the user and remove their
data from our servers.
{$t('app.users.delete_confirmation_description')}
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer class="sm:justify-start">
@@ -200,10 +208,14 @@
onclick={confirmDelete}
disabled={isDeleting}
>
{#if isDeleting}Deleting...{:else}Confirm{/if}
{#if isDeleting}
{$t('app.users.deleting')}...
{:else}
{$t('app.users.confirm')}
{/if}
</Button>
<Dialog.Close>
<Button type="button" variant="secondary">Cancel</Button>
<Button type="button" variant="secondary">{$t('app.users.cancel')}</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>

View File

@@ -7,6 +7,7 @@
import { api } from '$lib/api.client';
import { authStore } from '$lib/stores/auth.store';
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import { t } from '$lib/translations';
let first_name = '';
let last_name = '';
@@ -45,11 +46,8 @@
</script>
<svelte:head>
<title>Setup - Open Archiver</title>
<meta
name="description"
content="Set up the initial administrator account for Open Archiver."
/>
<title>{$t('app.setup.title')} - Open Archiver</title>
<meta name="description" content={$t('app.setup.description')} />
</svelte:head>
<div
@@ -67,35 +65,33 @@
</div>
<Card.Root class="w-full max-w-md">
<Card.Header class="space-y-1">
<Card.Title class="text-2xl">Welcome</Card.Title>
<Card.Description
>Create the first administrator account to get started.</Card.Description
>
<Card.Title class="text-2xl">{$t('app.setup.welcome')}</Card.Title>
<Card.Description>{$t('app.setup.create_admin_account')}</Card.Description>
</Card.Header>
<Card.Content class="grid gap-4">
<form on:submit|preventDefault={handleSubmit} class="grid gap-4">
<div class="grid gap-2">
<Label for="first_name">First name</Label>
<Label for="first_name">{$t('app.setup.first_name')}</Label>
<Input
id="first_name"
type="text"
placeholder="First name"
placeholder={$t('app.setup.first_name')}
bind:value={first_name}
required
/>
</div>
<div class="grid gap-2">
<Label for="last_name">Last name</Label>
<Label for="last_name">{$t('app.setup.last_name')}</Label>
<Input
id="last_name"
type="text"
placeholder="Last name"
placeholder={$t('app.setup.last_name')}
bind:value={last_name}
required
/>
</div>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Label for="email">{$t('app.setup.email')}</Label>
<Input
id="email"
type="email"
@@ -105,15 +101,15 @@
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Label for="password">{$t('app.setup.password')}</Label>
<Input id="password" type="password" bind:value={password} required />
</div>
<Button type="submit" class="w-full" disabled={isLoading}>
{#if isLoading}
<span>Creating Account...</span>
<span>{$t('app.setup.creating_account')}...</span>
{:else}
<span>Create Account</span>
<span>{$t('app.setup.create_account')}</span>
{/if}
</Button>
</form>

View File

@@ -8,6 +8,7 @@
import { authStore } from '$lib/stores/auth.store';
import type { LoginResponse } from '@open-archiver/types';
import { setAlert } from '$lib/components/custom/alert/alert-state.svelte';
import { t } from '$lib/translations';
let email = '';
let password = '';
@@ -50,7 +51,7 @@
</script>
<svelte:head>
<title>Login - Open Archiver</title>
<title>{$t('app.auth.login')} - Open Archiver</title>
<meta name="description" content="Login to your Open Archiver account." />
</svelte:head>
@@ -69,13 +70,13 @@
</div>
<Card.Root class="w-full max-w-md">
<Card.Header class="space-y-1">
<Card.Title class="text-2xl">Login</Card.Title>
<Card.Description>Enter your email below to login to your account.</Card.Description>
<Card.Title class="text-2xl">{$t('app.auth.login')}</Card.Title>
<Card.Description>{$t('app.auth.login_tip')}</Card.Description>
</Card.Header>
<Card.Content class="grid gap-4">
<form onsubmit={handleSubmit} class="grid gap-4">
<div class="grid gap-2">
<Label for="email">Email</Label>
<Label for="email">{$t('app.auth.email')}</Label>
<Input
id="email"
type="email"
@@ -85,12 +86,12 @@
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Label for="password">{$t('app.auth.password')}</Label>
<Input id="password" type="password" bind:value={password} required />
</div>
<Button type="submit" class=" w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
{isLoading ? $t('app.common.working') : $t('app.auth.login')}
</Button>
</form>
</Card.Content>

View File

@@ -6,7 +6,8 @@ export type SupportedLanguage =
| 'it' // Italian
| 'pt' // Portuguese
| 'nl' // Dutch
| 'ja'; // Japanese
| 'ja' // Japanese
| 'et'; // Estonian
export type Theme = 'light' | 'dark' | 'system';

28
pnpm-lock.yaml generated
View File

@@ -220,6 +220,9 @@ importers:
svelte-persisted-store:
specifier: ^0.12.0
version: 0.12.0(svelte@5.35.5)
sveltekit-i18n:
specifier: ^2.4.2
version: 2.4.2(svelte@5.35.5)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
@@ -1589,6 +1592,14 @@ packages:
svelte: ^5.0.0
vite: ^6.0.0
'@sveltekit-i18n/base@1.3.7':
resolution: {integrity: sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==}
peerDependencies:
svelte: '>=3.49.0'
'@sveltekit-i18n/parser-default@1.1.1':
resolution: {integrity: sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==}
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@@ -4370,6 +4381,11 @@ packages:
resolution: {integrity: sha512-KuRvI82rhh0RMz1EKsUJD96gZyHJ+h2+8zrwO8iqE/p/CmcNKvIItDUAeUePhuCDgtegDJmF8IKThbHIfmTgTA==}
engines: {node: '>=18'}
sveltekit-i18n@2.4.2:
resolution: {integrity: sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==}
peerDependencies:
svelte: '>=3.49.0'
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
@@ -6320,6 +6336,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@sveltekit-i18n/base@1.3.7(svelte@5.35.5)':
dependencies:
svelte: 5.35.5
'@sveltekit-i18n/parser-default@1.1.1': {}
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
@@ -9308,6 +9330,12 @@ snapshots:
magic-string: 0.30.17
zimmerframe: 1.1.2
sveltekit-i18n@2.4.2(svelte@5.35.5):
dependencies:
'@sveltekit-i18n/base': 1.3.7(svelte@5.35.5)
'@sveltekit-i18n/parser-default': 1.1.1
svelte: 5.35.5
tabbable@6.2.0: {}
tailwind-merge@3.0.2: {}