mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
feat: translate the appearance, reading, composing, identity, template, keyword and sidebar-app panes
This commit is contained in:
+198
-2
@@ -6,9 +6,16 @@
|
||||
"not_starred": "Not starred"
|
||||
},
|
||||
"calendar": {
|
||||
"agenda": {
|
||||
"days_clear": "The next {count} days are clear.",
|
||||
"no_upcoming": "No upcoming events"
|
||||
},
|
||||
"event_modal": {
|
||||
"participant_placeholder": "Add participant by email"
|
||||
},
|
||||
"events": {
|
||||
"tap_to_create": "Tap + to create one"
|
||||
},
|
||||
"invitation": {
|
||||
"accept": "Yes",
|
||||
"add_error": "Could not add the event",
|
||||
@@ -38,6 +45,12 @@
|
||||
"edit_calendar": "Edit calendar",
|
||||
"new_calendar": "New calendar"
|
||||
},
|
||||
"settings": {
|
||||
"show_week_numbers_mobile_desc": "Display week numbers in the month view.",
|
||||
"time_zone": "Time zone",
|
||||
"time_zone_auto": "Device time zone",
|
||||
"time_zone_desc": "Zone used for new events and for interpreting the calendar. \"Device\" follows the phone."
|
||||
},
|
||||
"share": {
|
||||
"add_people": "Add people",
|
||||
"error": "Failed to update sharing",
|
||||
@@ -58,16 +71,29 @@
|
||||
"done": "Done",
|
||||
"remove": "Remove",
|
||||
"retry": "Retry",
|
||||
"undo": "Undo"
|
||||
"undo": "Undo",
|
||||
"update": "Update"
|
||||
},
|
||||
"context_menu": {
|
||||
"color_tag": "Tag",
|
||||
"remove_color": "Remove tag"
|
||||
},
|
||||
"email_composer": {
|
||||
"attach_camera": "Camera",
|
||||
"attachment_reminder_body": "Your message mentions \"{keyword}\" but no file is attached. Send anyway?",
|
||||
"attachment_reminder_title": "Forgot an attachment?",
|
||||
"attachment_too_large": "This file is larger than the server allows ({size} > {max}).",
|
||||
"attachments_too_large_total": "The attachments would exceed the {max} this server allows per message.",
|
||||
"editor_unavailable": "Could not read the message content. Copy your text, then close and reopen the composer.",
|
||||
"identity_unavailable": "Identity unavailable",
|
||||
"image_load_failed": "Could not load image",
|
||||
"no_sent_folder": "No Sent folder found",
|
||||
"permission_camera": "Camera permission is required to take a photo.",
|
||||
"permission_photos": "Photo library permission is required to attach images.",
|
||||
"recipient_add_contact": "Add to contacts",
|
||||
"recipient_copy": "Copy address",
|
||||
"recipient_move_to": "Move to {field}",
|
||||
"recipient_remove": "Remove",
|
||||
"save_draft": "Save Draft",
|
||||
"schedule_custom": "Pick date & time…",
|
||||
"schedule_in_1h": "In 1 hour",
|
||||
@@ -75,6 +101,7 @@
|
||||
"schedule_past_body": "The scheduled time must be in the future.",
|
||||
"schedule_past_title": "Pick a future time",
|
||||
"schedule_tomorrow": "Tomorrow, 8:00 AM",
|
||||
"schedule_tomorrow_morning": "Tomorrow morning",
|
||||
"schedule_too_late_body": "That is later than this server allows. Pick an earlier time.",
|
||||
"schedule_too_late_title": "Too far ahead",
|
||||
"schedule_unsupported_body": "This mail server does not support scheduled send.",
|
||||
@@ -85,6 +112,7 @@
|
||||
"send_timeout_body": "The message may already have gone out. Check your Sent folder before sending it again.",
|
||||
"send_timeout_title": "No answer from the server",
|
||||
"undo_send_label": "Sending in {seconds}s…",
|
||||
"uploading": "Uploading...",
|
||||
"validation": {
|
||||
"subject_required": "Add a subject"
|
||||
}
|
||||
@@ -121,6 +149,23 @@
|
||||
"unknown_tag": "not defined here"
|
||||
},
|
||||
"email_viewer": {
|
||||
"address_actions": {
|
||||
"add_contact": "Add to contacts",
|
||||
"compose": "New message",
|
||||
"copy": "Copy address",
|
||||
"copy_failed": "Could not copy the address."
|
||||
},
|
||||
"attachment_actions": {
|
||||
"open": "Open",
|
||||
"preview": "Preview",
|
||||
"save": "Save",
|
||||
"share": "Share"
|
||||
},
|
||||
"attachment_failed": "Could not open attachment",
|
||||
"authentication": {
|
||||
"spoof_warning": "The sender address could not be verified - this message may be spoofed."
|
||||
},
|
||||
"bundle_too_large": "The attachments are too large to bundle.",
|
||||
"color_tag": {
|
||||
"blue": "Blue",
|
||||
"green": "Green",
|
||||
@@ -132,8 +177,22 @@
|
||||
"title": "Color Tag",
|
||||
"yellow": "Yellow"
|
||||
},
|
||||
"forwarded": "Forwarded",
|
||||
"preview": {
|
||||
"open_external": "Open in app",
|
||||
"pdf_external": "PDFs open in your PDF viewer on this device.",
|
||||
"unsupported": "No preview for this file type."
|
||||
},
|
||||
"received_via": "to {name}",
|
||||
"remove_color": "Remove tag",
|
||||
"set_color": "Set tag"
|
||||
"replied": "Replied",
|
||||
"set_color": "Set tag",
|
||||
"show_all_count": "Show all ({count})",
|
||||
"show_less": "Show less",
|
||||
"smime_unsupported": "Signed/encrypted with S/MIME - not supported on mobile",
|
||||
"unsubscribe_banner": {
|
||||
"no_identity": "No sending identity available"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"done": "Done",
|
||||
@@ -148,6 +207,12 @@
|
||||
"share_search_placeholder": "Search by name or email",
|
||||
"shared_with": "Shared with"
|
||||
},
|
||||
"identities": {
|
||||
"badge": {
|
||||
"to": "to {identity}",
|
||||
"via": "via {identity}"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"mobile": {
|
||||
"add_subtitle": "You're signed in to these already.",
|
||||
@@ -274,6 +339,9 @@
|
||||
"language": {
|
||||
"restart_for_direction": "Restart the app to apply the new text direction.",
|
||||
"system_default": "System default"
|
||||
},
|
||||
"toolbar_labels": {
|
||||
"description_mobile": "Display text labels next to toolbar icons."
|
||||
}
|
||||
},
|
||||
"badges": {
|
||||
@@ -281,12 +349,114 @@
|
||||
"not_implemented": "Not implemented",
|
||||
"unavailable": "Unavailable"
|
||||
},
|
||||
"downloads": {
|
||||
"attachment_template": {
|
||||
"description_mobile": "Template used when saving or sharing an attachment."
|
||||
},
|
||||
"email_template": {
|
||||
"description_mobile": "Template used when exporting a message as an .eml file."
|
||||
},
|
||||
"strip_diacritics": {
|
||||
"description_mobile": "Convert accented letters to plain ASCII (ä → a)."
|
||||
},
|
||||
"transform_description": "Applied to every exported filename.",
|
||||
"transform_title": "Filename Transform"
|
||||
},
|
||||
"email_behavior": {
|
||||
"archive_mode": {
|
||||
"no_archive": "No archive folder found.",
|
||||
"reorganize_result": "Moved {moved} of {total} emails.",
|
||||
"reorganizing": "Reorganizing…"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"description_mobile": "What happens when you tap an attachment."
|
||||
},
|
||||
"auto_select_reply_identity": {
|
||||
"description_mobile": "When replying, send from the address the message was originally sent to."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Compose and read in plain text only."
|
||||
},
|
||||
"send_delay": {
|
||||
"description_mobile": "Hold outgoing mail for a few seconds so you can cancel it. Requires server support."
|
||||
},
|
||||
"swipe_actions": {
|
||||
"move": "Move to folder",
|
||||
"pin": "Pin"
|
||||
},
|
||||
"swipe_left_action": {
|
||||
"description_mobile": "Action when you drag a row from its right edge towards the left.",
|
||||
"label_mobile": "Swipe left (right → left)"
|
||||
},
|
||||
"swipe_right_action": {
|
||||
"description_mobile": "Action when you drag a row from its left edge towards the right.",
|
||||
"label_mobile": "Swipe right (left → right)"
|
||||
}
|
||||
},
|
||||
"experimental": {
|
||||
"title": "Experimental Feature"
|
||||
},
|
||||
"identities": {
|
||||
"cannot_delete": "The primary identity cannot be removed.",
|
||||
"cannot_delete_title": "Cannot delete",
|
||||
"count": "{count, plural, =0 {No identities} one {# identity} other {# identities}}",
|
||||
"create": "New identity",
|
||||
"delete_confirm": "Remove \"{name}\"?",
|
||||
"delete_failed": "Delete failed",
|
||||
"delete_title": "Delete identity",
|
||||
"description_mobile": "Manage sender names, email addresses, and signatures. Tap a row to edit.",
|
||||
"display_name": "Display name",
|
||||
"edit": "Edit identity",
|
||||
"email_address": "Email address",
|
||||
"email_locked": "JMAP does not allow changing an identity's email; create a new one instead.",
|
||||
"invalid_email": "Enter a valid email address for this identity.",
|
||||
"invalid_email_title": "Invalid email",
|
||||
"new": "New",
|
||||
"no_name": "(no name)",
|
||||
"primary": "primary",
|
||||
"save_failed": "Save failed",
|
||||
"text_signature": "Plain-text signature"
|
||||
},
|
||||
"keywords": {
|
||||
"description_mobile": "Colored tags to organize your mail.",
|
||||
"reset_defaults": "Reset to Defaults"
|
||||
},
|
||||
"layout": {
|
||||
"description": "Tune the email list interactions for mobile.",
|
||||
"quick_actions": {
|
||||
"description": "The three buttons shown between Prev/Next at the bottom of an open email. Defaults to Reply, Reply All and Forward.",
|
||||
"label": "Reader quick actions",
|
||||
"mark_unread": "Mark Read/Unread",
|
||||
"relocated": "{count, plural, one {{actions} is moved to the top toolbar so you can still use it.} other {{actions} are moved to the top toolbar so you can still use them.}}",
|
||||
"slot": "Slot {n}",
|
||||
"spam": "Spam",
|
||||
"star": "Star",
|
||||
"tag": "Tag"
|
||||
},
|
||||
"show_avatars_in_junk": "Show sender avatars in Junk",
|
||||
"show_avatars_in_junk_description": "Load sender logos for messages in the Junk folder. Off keeps spam senders from being contacted.",
|
||||
"show_folder_total_count": "Show total message count on folders",
|
||||
"show_folder_total_count_description": "Show the total next to the unread count in the folder list.",
|
||||
"swipe_mode": {
|
||||
"instant": "Instant (swipe = action)",
|
||||
"reveal": "Reveal (swipe, then tap)"
|
||||
},
|
||||
"unified_cross_account": "Unified views across accounts",
|
||||
"unified_cross_account_description": "Combine every signed-in account in \"All inboxes\". Off keeps the unified views inside the active account and its shared folders."
|
||||
},
|
||||
"message_list_order": {
|
||||
"description": "Which messages come first in the list. Newest first is the tie-breaker within each group; the header toggle still flips oldest/newest.",
|
||||
"preset_chronological": "Newest first",
|
||||
"preset_custom": "Custom",
|
||||
"preset_starred_first": "Starred first",
|
||||
"preset_tagged_first": "Tagged first",
|
||||
"preset_unread_first": "Unread first",
|
||||
"scope": "Apply to",
|
||||
"scope_all": "All folders",
|
||||
"scope_inbox": "Inbox only",
|
||||
"tag": "Tag",
|
||||
"title": "Message list order"
|
||||
},
|
||||
"notifications": {
|
||||
"email": {
|
||||
"sound_channel_hint": "Sound and vibration for mail alerts are controlled by the Android notification channel: long-press a notification or open the system app settings."
|
||||
@@ -357,6 +527,23 @@
|
||||
"title": "Public Keys"
|
||||
}
|
||||
},
|
||||
"sidebar_apps": {
|
||||
"add": "Add app",
|
||||
"empty": "No apps added yet.",
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"name_placeholder": "App name",
|
||||
"open_mode": "Open mode",
|
||||
"show_on_mobile": "Show on mobile",
|
||||
"url": "URL"
|
||||
},
|
||||
"keep_loaded_description_mobile": "Keep inline apps running in the background when switching away.",
|
||||
"open_mode": {
|
||||
"inline": "Inline",
|
||||
"new_tab": "New tab",
|
||||
"tab": "Tab"
|
||||
}
|
||||
},
|
||||
"smime": {
|
||||
"mobile_description": "Sign and encrypt mail with S/MIME certificates.",
|
||||
"mobile_explainer": "S/MIME is not available in the mobile app yet. Manage keys and certificates in the webmail; signed messages are shown without verification here."
|
||||
@@ -364,6 +551,15 @@
|
||||
"tabs": {
|
||||
"plugins": "Plugins"
|
||||
},
|
||||
"templates": {
|
||||
"delete_title": "Delete template"
|
||||
},
|
||||
"themes": {
|
||||
"default_description": "The default light and dark palettes.",
|
||||
"default_name": "Bulwark",
|
||||
"mobile_description": "Pick a colour theme. Themes follow the light/dark setting from Appearance; custom theme packages are managed in the webmail.",
|
||||
"title": "Themes"
|
||||
},
|
||||
"vacation": {
|
||||
"warnings": {
|
||||
"date_format": "Dates must be \"YYYY-MM-DD\" or \"YYYY-MM-DD HH:MM\""
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settin
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useSettingsStore, type ThemeMode, type FontSize, type Density } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
type Theme = ThemeMode;
|
||||
|
||||
@@ -60,7 +61,7 @@ function DensityPreview({ density }: { density: Density }) {
|
||||
<Text
|
||||
style={[
|
||||
styles.densitySubject,
|
||||
row.unread ? { color: c.text, fontWeight: '500' } : { color: 'rgba(250,250,250,0.8)' },
|
||||
row.unread ? { color: c.text, fontWeight: '500' } : { color: c.textSecondary },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@@ -82,6 +83,7 @@ export function AppearanceSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const theme = useSettingsStore((s) => s.theme);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const setTheme = useSettingsStore((s) => s.setTheme);
|
||||
const fontSize = useSettingsStore((s) => s.fontSize);
|
||||
const setFontSize = useSettingsStore((s) => s.setFontSize);
|
||||
@@ -99,52 +101,55 @@ export function AppearanceSettings() {
|
||||
}, [hydrated, hydrate]);
|
||||
|
||||
return (
|
||||
<SettingsSection title="Appearance" description="Customize how the interface looks and feels.">
|
||||
<SettingItem label="Theme" description="Choose between light, dark, or system theme.">
|
||||
<SettingsSection
|
||||
title={t('settings.appearance.title', "Appearance")}
|
||||
description={t('settings.appearance.description', "Customize the look and feel of your webmail")}
|
||||
>
|
||||
<SettingItem label={t('settings.appearance.theme.label', "Theme")} description={t('settings.appearance.theme.description', "Choose your preferred color scheme")}>
|
||||
<RadioGroup
|
||||
value={theme}
|
||||
onChange={(v) => setTheme(v as Theme)}
|
||||
options={[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'system', label: 'System' },
|
||||
{ value: 'light', label: t('settings.appearance.theme.light', "Light") },
|
||||
{ value: 'dark', label: t('settings.appearance.theme.dark', "Dark") },
|
||||
{ value: 'system', label: t('settings.appearance.theme.system', "System") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Font Size" description="Adjust text size.">
|
||||
<SettingItem label={t('settings.appearance.font_size.label', "Font Size")} description={t('settings.appearance.font_size.description', "Adjust text size for better readability")}>
|
||||
<RadioGroup
|
||||
value={fontSize}
|
||||
onChange={(v) => setFontSize(v as FontSize)}
|
||||
options={[
|
||||
{ value: 'small', label: 'Small' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'large', label: 'Large' },
|
||||
{ value: 'small', label: t('settings.appearance.font_size.small', "Small") },
|
||||
{ value: 'medium', label: t('settings.appearance.font_size.medium', "Medium") },
|
||||
{ value: 'large', label: t('settings.appearance.font_size.large', "Large") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<View>
|
||||
<SettingItem label="List Density" description="How tight the email list is packed." noBorder />
|
||||
<SettingItem label={t('settings.appearance.list_density.label', "Density")} description={t('settings.appearance.list_density.description', "Control spacing and padding across the UI")} noBorder />
|
||||
<RadioGroup
|
||||
value={density}
|
||||
onChange={(v) => setDensity(v as Density)}
|
||||
options={[
|
||||
{ value: 'extra-compact', label: 'Extra' },
|
||||
{ value: 'compact', label: 'Compact' },
|
||||
{ value: 'regular', label: 'Regular' },
|
||||
{ value: 'comfortable', label: 'Comfy' },
|
||||
{ value: 'extra-compact', label: t('settings.appearance.list_density.extra_compact', "Extra Compact") },
|
||||
{ value: 'compact', label: t('settings.appearance.list_density.compact', "Compact") },
|
||||
{ value: 'regular', label: t('settings.appearance.list_density.regular', "Regular") },
|
||||
{ value: 'comfortable', label: t('settings.appearance.list_density.comfortable', "Comfortable") },
|
||||
]}
|
||||
/>
|
||||
<DensityPreview density={density} />
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
|
||||
<SettingItem label="Toolbar Labels" description="Show text labels on toolbar buttons.">
|
||||
<SettingItem label={t('settings.appearance.toolbar_labels.label', "Show Toolbar Labels")} description={t('settings.appearance.toolbar_labels.description_mobile', "Display text labels next to toolbar icons.")}>
|
||||
<ToggleSwitch checked={showToolbarLabels} onChange={setShowToolbarLabels} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Animations" description="Enable or disable interface animations.">
|
||||
<SettingItem label={t('settings.appearance.animations.label', "Enable Animations")} description={t('settings.appearance.animations.description', "Show smooth transitions and effects")}>
|
||||
<ToggleSwitch checked={animationsEnabled} onChange={setAnimationsEnabled} />
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
@@ -190,7 +195,7 @@ function makeStyles(c: ThemePalette) {
|
||||
},
|
||||
densityPreviewText: {
|
||||
fontSize: 11,
|
||||
color: 'rgba(161,161,170,0.7)',
|
||||
color: c.textMuted,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
|
||||
@@ -6,11 +6,13 @@ import Button from '../Button';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
export function ComposingSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const autoSelectReplyIdentity = useSettingsStore((s) => s.autoSelectReplyIdentity);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const setAutoSelectReplyIdentity = useSettingsStore((s) => s.setAutoSelectReplyIdentity);
|
||||
const attachmentReminderEnabled = useSettingsStore((s) => s.attachmentReminderEnabled);
|
||||
const setAttachmentReminderEnabled = useSettingsStore((s) => s.setAttachmentReminderEnabled);
|
||||
@@ -40,29 +42,29 @@ export function ComposingSettings() {
|
||||
};
|
||||
|
||||
const SEND_DELAY_OPTIONS: { label: string; value: number }[] = [
|
||||
{ label: 'Off', value: 0 },
|
||||
{ label: '5s', value: 5 },
|
||||
{ label: '10s', value: 10 },
|
||||
{ label: '20s', value: 20 },
|
||||
{ label: '30s', value: 30 },
|
||||
// Same set the webmail accepts (0/10/30/60); the store rejects others.
|
||||
{ label: t('settings.email_behavior.send_delay.off', "Off"), value: 0 },
|
||||
{ label: t('settings.email_behavior.send_delay.seconds', '{seconds} seconds', { seconds: 10 }), value: 10 },
|
||||
{ label: t('settings.email_behavior.send_delay.seconds', '{seconds} seconds', { seconds: 30 }), value: 30 },
|
||||
{ label: t('settings.email_behavior.send_delay.seconds', '{seconds} seconds', { seconds: 60 }), value: 60 },
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title="Composing"
|
||||
description="How the composer behaves when you reply, forward, or send."
|
||||
title={t('settings.composer.title', "Composer")}
|
||||
description={t('settings.composer.description', "Configure email composition settings")}
|
||||
>
|
||||
<SettingItem
|
||||
label="Auto-select reply identity"
|
||||
description="When replying, pick the identity that received the original message."
|
||||
label={t('settings.email_behavior.auto_select_reply_identity.label', "Reply From Received Address")}
|
||||
description={t('settings.email_behavior.auto_select_reply_identity.description_mobile', "When replying, send from the address the message was originally sent to.")}
|
||||
>
|
||||
<ToggleSwitch checked={autoSelectReplyIdentity} onChange={setAutoSelectReplyIdentity} />
|
||||
</SettingItem>
|
||||
|
||||
<View style={styles.subBlock}>
|
||||
<Text style={styles.subLabel}>Undo send delay</Text>
|
||||
<Text style={styles.subLabel}>{t('settings.email_behavior.send_delay.label', "Undo send / send delay")}</Text>
|
||||
<Text style={styles.subDescription}>
|
||||
Hold outgoing mail for a few seconds so you can cancel it. Requires server support.
|
||||
{t('settings.email_behavior.send_delay.description_mobile', "Hold outgoing mail for a few seconds so you can cancel it. Requires server support.")}
|
||||
</Text>
|
||||
<View style={styles.segmentRow}>
|
||||
{SEND_DELAY_OPTIONS.map((opt) => {
|
||||
@@ -83,8 +85,8 @@ export function ComposingSettings() {
|
||||
</View>
|
||||
|
||||
<SettingItem
|
||||
label="Attachment reminder"
|
||||
description="Warn before sending if the message mentions an attachment but none is attached."
|
||||
label={t('settings.email_behavior.attachment_reminder.label', "Attachment Reminder")}
|
||||
description={t('settings.email_behavior.attachment_reminder.description', "Warn before sending when your message mentions attachments but none are attached")}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
@@ -94,9 +96,9 @@ export function ComposingSettings() {
|
||||
|
||||
{attachmentReminderEnabled && (
|
||||
<View style={styles.subBlock}>
|
||||
<Text style={styles.subLabel}>Trigger keywords</Text>
|
||||
<Text style={styles.subLabel}>{t('settings.email_behavior.attachment_reminder.keywords_label', "Trigger keywords")}</Text>
|
||||
<Text style={styles.subDescription}>
|
||||
Words that, when found in the body or subject without an attachment, trigger the reminder.
|
||||
{t('settings.email_behavior.attachment_reminder.keywords_description', "Words or phrases that trigger the reminder when found in your message")}
|
||||
</Text>
|
||||
|
||||
<View style={styles.chips}>
|
||||
@@ -114,7 +116,7 @@ export function ComposingSettings() {
|
||||
<TextInput
|
||||
value={newKeyword}
|
||||
onChangeText={setNewKeyword}
|
||||
placeholder="e.g. attached"
|
||||
placeholder={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
placeholderTextColor={c.textMuted}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
@@ -129,7 +131,7 @@ export function ComposingSettings() {
|
||||
disabled={!newKeyword.trim()}
|
||||
icon={<Plus size={14} color={c.primaryForeground} />}
|
||||
>
|
||||
Add
|
||||
{t('settings.email_behavior.attachment_reminder.add', "Add")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
updateIdentity,
|
||||
} from '../../api/identity';
|
||||
import type { Identity } from '../../api/types';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
type DraftIdentity = {
|
||||
id: string;
|
||||
@@ -51,6 +52,7 @@ export function IdentitySettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const identities = useSettingsStore((s) => s.identities);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const loading = useSettingsStore((s) => s.loading);
|
||||
const error = useSettingsStore((s) => s.error);
|
||||
const fetchIdentities = useSettingsStore((s) => s.fetchIdentities);
|
||||
@@ -72,7 +74,7 @@ export function IdentitySettings() {
|
||||
const name = editing.name.trim();
|
||||
const email = editing.email.trim();
|
||||
if (!email || !EMAIL_RE.test(email)) {
|
||||
Alert.alert('Invalid email', 'Enter a valid email address for this identity.');
|
||||
Alert.alert(t('settings.identities.invalid_email_title', "Invalid email"), t('settings.identities.invalid_email', "Enter a valid email address for this identity."));
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
@@ -95,7 +97,7 @@ export function IdentitySettings() {
|
||||
closeEditor();
|
||||
await fetchIdentities();
|
||||
} catch (err) {
|
||||
Alert.alert('Save failed', err instanceof Error ? err.message : String(err));
|
||||
Alert.alert(t('settings.identities.save_failed', "Save failed"), err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -103,16 +105,16 @@ export function IdentitySettings() {
|
||||
|
||||
const confirmDelete = (identity: Identity) => {
|
||||
if (!identity.mayDelete) {
|
||||
Alert.alert('Cannot delete', 'The primary identity cannot be removed.');
|
||||
Alert.alert(t('settings.identities.cannot_delete_title', "Cannot delete"), t('settings.identities.cannot_delete', "The primary identity cannot be removed."));
|
||||
return;
|
||||
}
|
||||
Alert.alert(
|
||||
'Delete identity',
|
||||
`Remove "${identity.name || identity.email}"?`,
|
||||
t('settings.identities.delete_title', "Delete identity"),
|
||||
t('settings.identities.delete_confirm', 'Remove "{name}"?', { name: identity.name || identity.email }),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: t('common.cancel', "Cancel"), style: 'cancel' },
|
||||
{
|
||||
text: 'Delete',
|
||||
text: t('common.delete', "Delete"),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setDeletingId(identity.id);
|
||||
@@ -120,7 +122,7 @@ export function IdentitySettings() {
|
||||
await deleteIdentity(identity.id);
|
||||
await fetchIdentities();
|
||||
} catch (err) {
|
||||
Alert.alert('Delete failed', err instanceof Error ? err.message : String(err));
|
||||
Alert.alert(t('settings.identities.delete_failed', "Delete failed"), err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
@@ -132,12 +134,14 @@ export function IdentitySettings() {
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title="Identities"
|
||||
description="Manage sender names, email addresses, and signatures. Tap a row to edit."
|
||||
title={t('settings.identities.title', "Sending Identities")}
|
||||
description={t('settings.identities.description_mobile', "Manage sender names, email addresses, and signatures. Tap a row to edit.")}
|
||||
>
|
||||
<View style={styles.headerRow}>
|
||||
<Text style={styles.count}>
|
||||
{loading ? 'Loading…' : identities.length === 1 ? '1 identity' : `${identities.length} identities`}
|
||||
{loading
|
||||
? t('common.loading', "Loading...")
|
||||
: t('settings.identities.count', '{count, plural, =0 {No identities} one {# identity} other {# identities}}', { count: identities.length })}
|
||||
</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -146,7 +150,7 @@ export function IdentitySettings() {
|
||||
icon={<Plus size={14} color={c.primaryForeground} />}
|
||||
disabled={loading}
|
||||
>
|
||||
New
|
||||
{t('settings.identities.new', "New")}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -163,12 +167,12 @@ export function IdentitySettings() {
|
||||
style={({ pressed }) => [styles.identityRow, pressed && styles.identityRowPressed]}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.identityName}>{identity.name || '(no name)'}</Text>
|
||||
<Text style={styles.identityName}>{identity.name || t('settings.identities.no_name', "(no name)")}</Text>
|
||||
<Text style={styles.identityEmail}>{identity.email}</Text>
|
||||
</View>
|
||||
{!identity.mayDelete ? (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>primary</Text>
|
||||
<Text style={styles.badgeText}>{t('settings.identities.primary', "primary")}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Pressable
|
||||
@@ -176,6 +180,8 @@ export function IdentitySettings() {
|
||||
hitSlop={8}
|
||||
style={styles.identityDelete}
|
||||
disabled={deletingId === identity.id}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t('common.delete', "Delete")}
|
||||
>
|
||||
{deletingId === identity.id ? (
|
||||
<ActivityIndicator size="small" color={c.error} />
|
||||
@@ -192,14 +198,14 @@ export function IdentitySettings() {
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
{editing?.id ? 'Edit identity' : 'New identity'}
|
||||
{editing?.id ? t('settings.identities.edit', "Edit identity") : t('settings.identities.create', "New identity")}
|
||||
</Text>
|
||||
<Pressable onPress={closeEditor} hitSlop={8}>
|
||||
<Pressable onPress={closeEditor} hitSlop={8} accessibilityRole="button" accessibilityLabel={t('common.close', "Close")}>
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalBody}>
|
||||
<Text style={styles.fieldLabel}>Display name</Text>
|
||||
<Text style={styles.fieldLabel}>{t('settings.identities.display_name', "Display name")}</Text>
|
||||
<TextInput
|
||||
value={editing?.name ?? ''}
|
||||
onChangeText={(name) => setEditing((d) => (d ? { ...d, name } : d))}
|
||||
@@ -207,7 +213,7 @@ export function IdentitySettings() {
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>Email address</Text>
|
||||
<Text style={styles.fieldLabel}>{t('settings.identities.email_address', "Email address")}</Text>
|
||||
<TextInput
|
||||
value={editing?.email ?? ''}
|
||||
onChangeText={(email) => setEditing((d) => (d ? { ...d, email } : d))}
|
||||
@@ -220,9 +226,9 @@ export function IdentitySettings() {
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
{!!editing?.id && (
|
||||
<Text style={styles.hint}>JMAP does not allow changing an identity's email; create a new one instead.</Text>
|
||||
<Text style={styles.hint}>{t('settings.identities.email_locked', "JMAP does not allow changing an identity's email; create a new one instead.")}</Text>
|
||||
)}
|
||||
<Text style={styles.fieldLabel}>Plain-text signature</Text>
|
||||
<Text style={styles.fieldLabel}>{t('settings.identities.text_signature', "Plain-text signature")}</Text>
|
||||
<TextInput
|
||||
value={editing?.textSignature ?? ''}
|
||||
onChangeText={(textSignature) => setEditing((d) => (d ? { ...d, textSignature } : d))}
|
||||
@@ -233,14 +239,14 @@ export function IdentitySettings() {
|
||||
/>
|
||||
</ScrollView>
|
||||
<View style={styles.modalActions}>
|
||||
<Button variant="outline" size="sm" onPress={closeEditor} disabled={saving}>Cancel</Button>
|
||||
<Button variant="outline" size="sm" onPress={closeEditor} disabled={saving}>{t('common.cancel', "Cancel")}</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onPress={() => { void saveDraft(); }}
|
||||
loading={saving}
|
||||
>
|
||||
Save
|
||||
{t('common.save', "Save")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
@@ -271,7 +277,7 @@ function makeStyles(c: ThemePalette) {
|
||||
identityDelete: { padding: 6 },
|
||||
badge: {
|
||||
paddingHorizontal: 6, paddingVertical: 2, borderRadius: radius.full,
|
||||
backgroundColor: 'rgba(59,130,246,0.15)',
|
||||
backgroundColor: c.primaryBg,
|
||||
},
|
||||
badgeText: { fontSize: 10, fontWeight: '500', color: c.primary },
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useKeywordsStore, type KeywordDef } from '../../stores/keywords-store';
|
||||
import { DARK_COLORS } from '../../theme/tokens';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
type Keyword = KeywordDef;
|
||||
|
||||
@@ -17,6 +18,7 @@ export function KeywordSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const keywords = useKeywordsStore((s) => s.keywords);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const addKeyword = useKeywordsStore((s) => s.add);
|
||||
const updateKeyword = useKeywordsStore((s) => s.update);
|
||||
const removeKeyword = useKeywordsStore((s) => s.remove);
|
||||
@@ -45,7 +47,7 @@ export function KeywordSettings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title="Keywords & Labels" description="Colored tags to organize your mail.">
|
||||
<SettingsSection title={t('settings.keywords.title', "Email Tags")} description={t('settings.keywords.description_mobile', "Colored tags to organize your mail.")}>
|
||||
<View style={{ gap: spacing.sm }}>
|
||||
{keywords.map((kw) => {
|
||||
if (editingId === kw.id) {
|
||||
@@ -66,10 +68,10 @@ export function KeywordSettings() {
|
||||
<Text style={styles.kwLabel}>{kw.label}</Text>
|
||||
<Text style={styles.kwId}>$label:{kw.id}</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 2 }}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(kw.id)}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(kw.id)} accessibilityRole="button" accessibilityLabel={t('settings.keywords.edit', "Edit tag")}>
|
||||
<Pencil size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => deleteKeyword(kw.id)}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => deleteKeyword(kw.id)} accessibilityRole="button" accessibilityLabel={t('settings.keywords.delete', "Delete tag")}>
|
||||
<Trash2 size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -89,11 +91,11 @@ export function KeywordSettings() {
|
||||
<View style={styles.bottomActions}>
|
||||
<Pressable style={styles.outlineBtn} onPress={() => setIsAdding(true)}>
|
||||
<Plus size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.outlineBtnText}>Add keyword</Text>
|
||||
<Text style={styles.outlineBtnText}>{t('settings.keywords.add_keyword', "Add Tag")}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.outlineBtn} onPress={resetDefaults}>
|
||||
<RotateCcw size={14} color={c.mutedForeground} />
|
||||
<Text style={styles.outlineBtnText}>Reset defaults</Text>
|
||||
<Text style={styles.outlineBtnText}>{t('settings.keywords.reset_defaults', "Reset to Defaults")}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
@@ -112,6 +114,7 @@ interface KeywordFormProps {
|
||||
function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const [label, setLabel] = useState(initial?.label ?? '');
|
||||
const [color, setColor] = useState<keyof typeof DARK_COLORS.tags>(initial?.color ?? 'blue');
|
||||
|
||||
@@ -133,21 +136,21 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
return (
|
||||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={styles.formLabel}>Label</Text>
|
||||
<Text style={styles.formLabel}>{t('settings.keywords.label_field', "Display Name")}</Text>
|
||||
<TextInput
|
||||
value={label}
|
||||
onChangeText={setLabel}
|
||||
placeholder="e.g. Important"
|
||||
placeholder={t('settings.keywords.label_placeholder', "e.g. Work, Personal, Urgent")}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.input}
|
||||
maxLength={30}
|
||||
autoFocus
|
||||
/>
|
||||
{isDuplicate && <Text style={styles.errorText}>An ID with that name already exists.</Text>}
|
||||
{isDuplicate && <Text style={styles.errorText}>{t('settings.keywords.id_exists', "This tag ID already exists")}</Text>}
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={styles.formLabel}>Color</Text>
|
||||
<Text style={styles.formLabel}>{t('settings.keywords.color_field', "Color")}</Text>
|
||||
<View style={styles.palette}>
|
||||
{PALETTE_KEYS.map((key) => {
|
||||
const p = c.tags[key];
|
||||
@@ -155,6 +158,9 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setColor(key)}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected: color === key }}
|
||||
accessibilityLabel={key}
|
||||
style={[
|
||||
styles.colorSwatch,
|
||||
{ backgroundColor: p.dot },
|
||||
@@ -169,7 +175,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
<View style={styles.formActions}>
|
||||
<Pressable style={styles.cancelFormBtn} onPress={onCancel}>
|
||||
<X size={14} color={c.text} />
|
||||
<Text style={styles.cancelFormText}>Cancel</Text>
|
||||
<Text style={styles.cancelFormText}>{t('common.cancel', "Cancel")}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.saveBtn, !isValid && { opacity: 0.5 }]}
|
||||
@@ -177,7 +183,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Check size={14} color={c.primaryForeground} />
|
||||
<Text style={styles.saveBtnText}>{initial ? 'Save' : 'Add'}</Text>
|
||||
<Text style={styles.saveBtnText}>{initial ? t('common.save', "Save") : t('common.add', "Add")}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
import { useEmailStore } from '../../stores/email-store';
|
||||
import { archiveEmails, queryEmails, getEmails } from '../../api/email';
|
||||
import { ownMailboxes } from '../../lib/mailbox-tree';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
export function ReadingSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||
const update = useSettingsStore((s) => s.updateSetting);
|
||||
|
||||
@@ -59,7 +61,7 @@ export function ReadingSettings() {
|
||||
(m) => m.role === 'archive' || m.name.toLowerCase() === 'archive',
|
||||
);
|
||||
if (!archiveMailbox) {
|
||||
setReorganizeResult('No archive folder found.');
|
||||
setReorganizeResult(t('settings.email_behavior.archive_mode.no_archive', "No archive folder found."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,58 +102,58 @@ export function ReadingSettings() {
|
||||
if (ids.length < PAGE) break;
|
||||
}
|
||||
|
||||
setReorganizeResult(`Moved ${moved} of ${total} emails.`);
|
||||
setReorganizeResult(t('settings.email_behavior.archive_mode.reorganize_result', 'Moved {moved} of {total} emails.', { moved, total }));
|
||||
} catch (err) {
|
||||
setReorganizeResult(err instanceof Error ? err.message : 'Reorganize failed.');
|
||||
setReorganizeResult(err instanceof Error ? err.message : t('settings.email_behavior.archive_mode.reorganize_error', "Failed to reorganize archive"));
|
||||
} finally {
|
||||
setReorganizing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title="Email Behavior" description="How your inbox behaves day to day.">
|
||||
<SettingItem label="Mark as Read" description="When to flag an email as read.">
|
||||
<SettingsSection title={t('settings.email_behavior.title', "Email Behavior")} description={t('settings.email_behavior.description', "Configure how emails are handled")}>
|
||||
<SettingItem label={t('settings.email_behavior.mark_read.label', "Mark as Read")} description={t('settings.email_behavior.mark_read.description', "When to mark emails as read when opened")}>
|
||||
<Select
|
||||
value={String(markAsReadDelay)}
|
||||
onChange={(v) => update('markAsReadDelay', Number(v))}
|
||||
options={[
|
||||
{ value: '0', label: 'Instant' },
|
||||
{ value: '3000', label: 'After 3s' },
|
||||
{ value: '5000', label: 'After 5s' },
|
||||
{ value: '-1', label: 'Never' },
|
||||
{ value: '0', label: t('settings.email_behavior.mark_read.instant', "Instantly") },
|
||||
{ value: '3000', label: t('settings.email_behavior.mark_read.delay_3s', "After 3 seconds") },
|
||||
{ value: '5000', label: t('settings.email_behavior.mark_read.delay_5s', "After 5 seconds") },
|
||||
{ value: '-1', label: t('settings.email_behavior.mark_read.never', "Never") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<View style={styles.group}>
|
||||
<SettingItem label="Delete Action" description="Where deleted emails go." noBorder />
|
||||
<SettingItem label={t('settings.email_behavior.delete_action.label', "Delete Action")} description={t('settings.email_behavior.delete_action.description', "What happens when you delete an email")} noBorder />
|
||||
<Select
|
||||
value={deleteAction}
|
||||
onChange={(v) => update('deleteAction', v as DeleteAction)}
|
||||
options={[
|
||||
{ value: 'trash', label: 'Move to Trash' },
|
||||
{ value: 'trash-and-read', label: 'Move to Trash and mark as read' },
|
||||
{ value: 'permanent', label: 'Permanently delete' },
|
||||
{ value: 'trash', label: t('settings.email_behavior.delete_action.trash', "Move to Trash") },
|
||||
{ value: 'trash-and-read', label: t('settings.email_behavior.delete_action.trash_and_read', "Move to Trash and mark as read") },
|
||||
{ value: 'permanent', label: t('settings.email_behavior.delete_action.permanent', "Delete Permanently") },
|
||||
]}
|
||||
/>
|
||||
{deleteAction === 'permanent' && (
|
||||
<View style={styles.warning}>
|
||||
<AlertTriangle size={14} color={c.error} />
|
||||
<Text style={styles.warningText}>Permanent deletion cannot be undone.</Text>
|
||||
<Text style={styles.warningText}>{t('settings.email_behavior.delete_action.warning', "Emails will be permanently deleted and cannot be recovered. This action is irreversible.")}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
|
||||
<View style={styles.group}>
|
||||
<SettingItem label="Archive Mode" description="Organize archive into subfolders." noBorder />
|
||||
<SettingItem label={t('settings.email_behavior.archive_mode.label', "Archive in")} description={t('settings.email_behavior.archive_mode.description', "How to organize emails when archiving")} noBorder />
|
||||
<Select
|
||||
value={archiveMode}
|
||||
onChange={(v) => update('archiveMode', v as ArchiveMode)}
|
||||
options={[
|
||||
{ value: 'single', label: 'Single folder' },
|
||||
{ value: 'year', label: 'By year' },
|
||||
{ value: 'month', label: 'By year/month' },
|
||||
{ value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") },
|
||||
{ value: 'year', label: t('settings.email_behavior.archive_mode.year', "A folder per year") },
|
||||
{ value: 'month', label: t('settings.email_behavior.archive_mode.month', "A folder per month") },
|
||||
]}
|
||||
/>
|
||||
{archiveMode !== 'single' && (
|
||||
@@ -167,7 +169,7 @@ export function ReadingSettings() {
|
||||
<FolderSync size={14} color={c.text} />
|
||||
)}
|
||||
<Text style={styles.inlineBtnText}>
|
||||
{reorganizing ? 'Reorganizing…' : 'Reorganize existing archive'}
|
||||
{reorganizing ? t('settings.email_behavior.archive_mode.reorganizing', "Reorganizing…") : t('settings.email_behavior.archive_mode.reorganize', "Reorganize existing archive")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
{reorganizeResult && (
|
||||
@@ -178,31 +180,31 @@ export function ReadingSettings() {
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
|
||||
<SettingItem label="Permanently Delete Junk" description="Skip the trash when deleting spam.">
|
||||
<SettingItem label={t('settings.email_behavior.permanently_delete_junk.label', "Permanently Delete Junk")} description={t('settings.email_behavior.permanently_delete_junk.description', "Permanently delete emails from the Junk/Spam folder instead of moving them to Trash")}>
|
||||
<ToggleSwitch checked={permanentlyDeleteJunk} onChange={(v) => update('permanentlyDeleteJunk', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Show Preview" description="Preview text under each subject.">
|
||||
<SettingItem label={t('settings.email_behavior.show_preview.label', "Show Preview Text")} description={t('settings.email_behavior.show_preview.description', "Display email preview in the list")}>
|
||||
<ToggleSwitch checked={showPreview} onChange={(v) => update('showPreview', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Disable Thread Grouping" description="Show emails individually instead of threaded.">
|
||||
<SettingItem label={t('settings.email_behavior.disable_threading.label', "Disable Conversation Grouping")} description={t('settings.email_behavior.disable_threading.description', "Show emails as individual messages instead of grouped by conversation")}>
|
||||
<ToggleSwitch checked={disableThreading} onChange={(v) => update('disableThreading', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Include Group Inboxes" description="Also show group and shared inboxes in the unified All Inboxes view.">
|
||||
<SettingItem label={t('settings.appearance.unified_mailbox.include_group.label', "Include group inboxes")} description={t('settings.appearance.unified_mailbox.include_group.description', "Also merge shared/group inboxes into the unified view.")}>
|
||||
<ToggleSwitch checked={includeGroupInUnified} onChange={(v) => update('includeGroupInUnified', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Plain Text Mode" description="Compose and read in plain text only.">
|
||||
<SettingItem label={t('settings.email_behavior.plain_text_mode.label', "Plain Text Only")} description={t('settings.email_behavior.plain_text_mode.description_mobile', "Compose and read in plain text only.")}>
|
||||
<ToggleSwitch checked={plainTextMode} onChange={(v) => update('plainTextMode', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Auto-select Reply Identity" description="Pick the best identity when replying.">
|
||||
<SettingItem label={t('settings.email_behavior.auto_select_reply_identity.label', "Reply From Received Address")} description={t('settings.email_behavior.auto_select_reply_identity.description_mobile', "When replying, send from the address the message was originally sent to.")}>
|
||||
<ToggleSwitch checked={autoSelectReplyIdentity} onChange={(v) => update('autoSelectReplyIdentity', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Attachment Reminder" description="Warn when the word 'attached' is present but no file is attached.">
|
||||
<SettingItem label={t('settings.email_behavior.attachment_reminder.label', "Attachment Reminder")} description={t('settings.email_behavior.attachment_reminder.description', "Warn before sending when your message mentions attachments but none are attached")}>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
onChange={(v) => update('attachmentReminderEnabled', v)}
|
||||
@@ -211,8 +213,8 @@ export function ReadingSettings() {
|
||||
|
||||
{attachmentReminderEnabled && (
|
||||
<View style={styles.group}>
|
||||
<Text style={styles.subLabel}>Trigger Keywords</Text>
|
||||
<Text style={styles.subDesc}>Add words that should trigger the reminder.</Text>
|
||||
<Text style={styles.subLabel}>{t('settings.email_behavior.attachment_reminder.keywords_label', "Trigger keywords")}</Text>
|
||||
<Text style={styles.subDesc}>{t('settings.email_behavior.attachment_reminder.keywords_description', "Words or phrases that trigger the reminder when found in your message")}</Text>
|
||||
<View style={styles.chipRow}>
|
||||
{attachmentReminderKeywords.map((kw) => (
|
||||
<View key={kw} style={styles.chip}>
|
||||
@@ -235,7 +237,7 @@ export function ReadingSettings() {
|
||||
<TextInput
|
||||
value={newKeyword}
|
||||
onChangeText={setNewKeyword}
|
||||
placeholder="Add a keyword"
|
||||
placeholder={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={styles.keywordInput}
|
||||
onSubmitEditing={() => {
|
||||
@@ -256,35 +258,35 @@ export function ReadingSettings() {
|
||||
setNewKeyword('');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.addKeywordText}>Add</Text>
|
||||
<Text style={styles.addKeywordText}>{t('settings.email_behavior.attachment_reminder.add', "Add")}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<SettingItem label="Hide Inline Image Attachments" description="Don't list inline images in the attachment list.">
|
||||
<SettingItem label={t('settings.email_behavior.hide_inline_image_attachments.label', "Hide inline images from attachments")} description={t('settings.email_behavior.hide_inline_image_attachments.description', "Images embedded in the message body are not listed as separate attachments")}>
|
||||
<ToggleSwitch checked={hideInlineImageAttachments} onChange={(v) => update('hideInlineImageAttachments', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Attachment Click Action" description="What happens when you tap an attachment.">
|
||||
<SettingItem label={t('settings.email_behavior.attachment_click_action.label', "Attachment Click Action")} description={t('settings.email_behavior.attachment_click_action.description_mobile', "What happens when you tap an attachment.")}>
|
||||
<Select
|
||||
value={mailAttachmentAction}
|
||||
onChange={(v) => update('mailAttachmentAction', v as MailAttachmentAction)}
|
||||
options={[
|
||||
{ value: 'preview', label: 'Preview' },
|
||||
{ value: 'download', label: 'Download' },
|
||||
{ value: 'preview', label: t('settings.email_behavior.attachment_click_action.preview', "Preview when possible") },
|
||||
{ value: 'download', label: t('settings.email_behavior.attachment_click_action.download', "Download immediately") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Attachment Position" description="Where attachments appear in messages.">
|
||||
<SettingItem label={t('settings.email_behavior.attachment_position.label', "Attachment Position")} description={t('settings.email_behavior.attachment_position.description', "Where to display attachments in the email header")}>
|
||||
<Select
|
||||
value={attachmentPosition}
|
||||
onChange={(v) => update('attachmentPosition', v as AttachmentPosition)}
|
||||
options={[
|
||||
{ value: 'beside-sender', label: 'Beside sender' },
|
||||
{ value: 'below-header', label: 'Below header' },
|
||||
{ value: 'beside-sender', label: t('settings.email_behavior.attachment_position.beside-sender', "Next to sender") },
|
||||
{ value: 'below-header', label: t('settings.email_behavior.attachment_position.below-header', "Below header") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
@@ -324,7 +326,7 @@ export function ReadingSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label="Emails Per Page" description="How many emails to load at a time.">
|
||||
<SettingItem label={t('settings.email_behavior.emails_per_page.label', "Emails Per Page")} description={t('settings.email_behavior.emails_per_page.description', "Number of emails to load at once")}>
|
||||
<Select
|
||||
value={String(emailsPerPage)}
|
||||
onChange={(v) => update('emailsPerPage', Number(v))}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Button from '../Button';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useSettingsStore, type SidebarApp } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
export function SidebarAppsSettings() {
|
||||
const c = useColors();
|
||||
@@ -16,6 +17,7 @@ export function SidebarAppsSettings() {
|
||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||
const update = useSettingsStore((s) => s.updateSetting);
|
||||
const apps = useSettingsStore((s) => s.sidebarApps);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const keepLoaded = useSettingsStore((s) => s.keepAppsLoaded);
|
||||
const addSidebarApp = useSettingsStore((s) => s.addSidebarApp);
|
||||
const updateSidebarApp = useSettingsStore((s) => s.updateSidebarApp);
|
||||
@@ -44,16 +46,16 @@ export function SidebarAppsSettings() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SettingsSection title="Sidebar Apps" description="Embedded web apps accessible from the sidebar.">
|
||||
<SettingItem label="Keep apps loaded" description="Maintain app state when switching away.">
|
||||
<SettingsSection title={t('settings.sidebar_apps.title', "Sidebar Apps")} description={t('settings.sidebar_apps.description', "Manage custom apps and links in your sidebar")}>
|
||||
<SettingItem label={t('settings.sidebar_apps.keep_loaded', "Keep Apps Loaded")} description={t('settings.sidebar_apps.keep_loaded_description_mobile', "Keep inline apps running in the background when switching away.")}>
|
||||
<ToggleSwitch checked={keepLoaded} onChange={(v) => update('keepAppsLoaded', v)} />
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Manage Apps" description="Add, edit, or remove sidebar apps.">
|
||||
<SettingsSection title={t('settings.sidebar_apps.manage_title', "Custom Apps")} description={t('settings.sidebar_apps.manage_description', "Add, edit, or remove custom apps from your sidebar")}>
|
||||
<View style={{ gap: spacing.md }}>
|
||||
{apps.length === 0 && !isAdding && (
|
||||
<Text style={styles.emptyText}>No apps added yet.</Text>
|
||||
<Text style={styles.emptyText}>{t('settings.sidebar_apps.empty', "No apps added yet.")}</Text>
|
||||
)}
|
||||
|
||||
{apps.map((app) => {
|
||||
@@ -83,15 +85,15 @@ export function SidebarAppsSettings() {
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.modeBadgeText,
|
||||
{ color: app.openMode === 'inline' ? '#60a5fa' : c.mutedForeground },
|
||||
{ color: app.openMode === 'inline' ? c.primary : c.mutedForeground },
|
||||
]}>
|
||||
{app.openMode === 'inline' ? 'Inline' : 'Tab'}
|
||||
{app.openMode === 'inline' ? t('settings.sidebar_apps.open_mode.inline', "Inline") : t('settings.sidebar_apps.open_mode.tab', "Tab")}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(app.id)}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => setEditingId(app.id)} accessibilityRole="button" accessibilityLabel={t('common.edit', "Edit")}>
|
||||
<Pencil size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
<Pressable style={styles.iconBtn} onPress={() => removeApp(app.id)}>
|
||||
<Pressable style={styles.iconBtn} onPress={() => removeApp(app.id)} accessibilityRole="button" accessibilityLabel={t('common.delete', "Delete")}>
|
||||
<Trash2 size={14} color={c.mutedForeground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -108,7 +110,7 @@ export function SidebarAppsSettings() {
|
||||
{!isAdding && editingId === null && (
|
||||
<Pressable style={styles.addBtn} onPress={() => setIsAdding(true)}>
|
||||
<Plus size={16} color={c.text} />
|
||||
<Text style={styles.addBtnText}>Add app</Text>
|
||||
<Text style={styles.addBtnText}>{t('settings.sidebar_apps.add', "Add app")}</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
@@ -126,6 +128,7 @@ interface AppFormProps {
|
||||
function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
const c = useColors();
|
||||
const formStyles = React.useMemo(() => makeFormStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const [name, setName] = useState(initial?.name ?? '');
|
||||
const [url, setUrl] = useState(initial?.url ?? '');
|
||||
const [openMode, setOpenMode] = useState<'tab' | 'inline'>(initial?.openMode ?? 'tab');
|
||||
@@ -136,18 +139,18 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
return (
|
||||
<View style={formStyles.form}>
|
||||
<View>
|
||||
<Text style={formStyles.label}>Name</Text>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.name', "Name")}</Text>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="App name"
|
||||
placeholder={t('settings.sidebar_apps.form.name_placeholder', "App name")}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
style={formStyles.input}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={formStyles.label}>URL</Text>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.url', "URL")}</Text>
|
||||
<TextInput
|
||||
value={url}
|
||||
onChangeText={setUrl}
|
||||
@@ -160,7 +163,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={formStyles.label}>Open mode</Text>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.open_mode', "Open mode")}</Text>
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
||||
<Pressable
|
||||
onPress={() => setOpenMode('tab')}
|
||||
@@ -168,7 +171,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
>
|
||||
<ExternalLink size={14} color={openMode === 'tab' ? c.primary : c.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'tab' && { color: c.primary }]}>
|
||||
New tab
|
||||
{t('settings.sidebar_apps.open_mode.new_tab', "New tab")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
@@ -177,25 +180,25 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
>
|
||||
<PanelRight size={14} color={openMode === 'inline' ? c.primary : c.text} />
|
||||
<Text style={[formStyles.modeBtnText, openMode === 'inline' && { color: c.primary }]}>
|
||||
Inline
|
||||
{t('settings.sidebar_apps.open_mode.inline', "Inline")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={formStyles.label}>Show on mobile</Text>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.show_on_mobile', "Show on mobile")}</Text>
|
||||
<ToggleSwitch checked={showOnMobile} onChange={setShowOnMobile} />
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm, justifyContent: 'flex-end' }}>
|
||||
<Button variant="ghost" size="sm" onPress={onCancel}>Cancel</Button>
|
||||
<Button variant="ghost" size="sm" onPress={onCancel}>{t('common.cancel', "Cancel")}</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!canSave}
|
||||
onPress={() => onSave({ name: name.trim(), url: url.trim(), icon: 'Globe', openMode, showOnMobile })}
|
||||
>
|
||||
{initial ? 'Update' : 'Add'}
|
||||
{initial ? t('common.update', "Update") : t('common.add', "Add")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -16,10 +16,12 @@ import Button from '../Button';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useTemplatesStore, type EmailTemplate } from '../../stores/templates-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
|
||||
export function TemplateSettings() {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const tr = useLocaleStore((s) => s.t);
|
||||
const templates = useTemplatesStore((s) => s.templates);
|
||||
const hydrated = useTemplatesStore((s) => s.hydrated);
|
||||
const hydrate = useTemplatesStore((s) => s.hydrate);
|
||||
@@ -68,7 +70,7 @@ export function TemplateSettings() {
|
||||
const saveDraft = () => {
|
||||
const name = draftName.trim();
|
||||
if (!name) {
|
||||
Alert.alert('Template name required');
|
||||
Alert.alert(tr('settings.templates.validation.empty', "Template name is required"));
|
||||
return;
|
||||
}
|
||||
if (!editing) return;
|
||||
@@ -82,11 +84,11 @@ export function TemplateSettings() {
|
||||
|
||||
const confirmDelete = (t: EmailTemplate) => {
|
||||
Alert.alert(
|
||||
'Delete template',
|
||||
`Permanently delete "${t.name}"?`,
|
||||
tr('settings.templates.delete_title', "Delete template"),
|
||||
tr('settings.templates.delete_confirm', 'Permanently delete "{name}"?', { name: t.name }),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Delete', style: 'destructive', onPress: () => deleteTemplate(t.id) },
|
||||
{ text: tr('common.cancel', 'Cancel'), style: 'cancel' },
|
||||
{ text: tr('common.delete', 'Delete'), style: 'destructive', onPress: () => deleteTemplate(t.id) },
|
||||
],
|
||||
);
|
||||
};
|
||||
@@ -94,40 +96,43 @@ export function TemplateSettings() {
|
||||
const exportTemplatesAsShare = async () => {
|
||||
const json = exportAll();
|
||||
try {
|
||||
await Share.share({ message: json, title: 'Templates export' });
|
||||
await Share.share({ message: json, title: tr('settings.templates.export', 'Export') });
|
||||
} catch (err) {
|
||||
Alert.alert('Export failed', err instanceof Error ? err.message : 'Unable to share');
|
||||
Alert.alert(tr('settings.templates.export_failed', 'Export failed'), err instanceof Error ? err.message : tr('settings.templates.share_unavailable', 'Unable to share'));
|
||||
}
|
||||
};
|
||||
|
||||
const performImport = () => {
|
||||
const result = importTemplates(importText);
|
||||
if (result.error) {
|
||||
Alert.alert('Import failed', result.error);
|
||||
Alert.alert(tr('settings.templates.import_failed', 'Import failed'), result.error);
|
||||
return;
|
||||
}
|
||||
setImportText('');
|
||||
setImportVisible(false);
|
||||
Alert.alert('Imported', `${result.count} template${result.count === 1 ? '' : 's'} added.`);
|
||||
Alert.alert(
|
||||
tr('settings.templates.imported', 'Imported'),
|
||||
tr('settings.templates.imported_count', '{count, plural, one {# template} other {# templates}} added.', { count: result.count }),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SettingsSection title="Templates" description="Reusable snippets for common replies. Tap one to edit; long-press to delete.">
|
||||
<SettingsSection title={tr('settings.templates.title', 'Email Templates')} description={tr('settings.templates.description_mobile', 'Reusable snippets for common replies. Tap one to edit; long-press to delete.')}>
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.countRow}>
|
||||
<FileText size={16} color={c.mutedForeground} />
|
||||
<Text style={styles.count}>
|
||||
{templates.length === 1 ? '1 template' : `${templates.length} templates`}
|
||||
{tr('settings.templates.count', '{count, plural, one {# template} other {# templates}}', { count: templates.length })}
|
||||
</Text>
|
||||
</View>
|
||||
<Button variant="default" size="sm" onPress={openCreate} icon={<Plus size={14} color={c.primaryForeground} />}>
|
||||
New
|
||||
{tr('settings.templates.add', 'New Template')}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{templates.length === 0 ? (
|
||||
<Text style={styles.emptyHint}>No templates yet. Tap "New" to add one.</Text>
|
||||
<Text style={styles.emptyHint}>{tr('settings.templates.no_templates', 'No templates yet')}</Text>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{templates.map((t) => (
|
||||
@@ -143,7 +148,7 @@ export function TemplateSettings() {
|
||||
<Text style={styles.listRowSubject} numberOfLines={1}>{t.subject}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Pressable onPress={() => confirmDelete(t)} hitSlop={8} style={styles.listRowDelete}>
|
||||
<Pressable onPress={() => confirmDelete(t)} hitSlop={8} style={styles.listRowDelete} accessibilityRole="button" accessibilityLabel={tr('common.delete', 'Delete')}>
|
||||
<Trash2 size={16} color={c.error} />
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
@@ -152,7 +157,7 @@ export function TemplateSettings() {
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Export / Import" description="Back up or restore your templates as JSON.">
|
||||
<SettingsSection title={tr('settings.templates.export_import', 'Export & Import')} description={tr('settings.templates.export_import_description', 'Back up your templates or transfer them to another device')}>
|
||||
<View style={styles.actions}>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -161,7 +166,7 @@ export function TemplateSettings() {
|
||||
icon={<Download size={14} color={c.text} />}
|
||||
onPress={() => { void exportTemplatesAsShare(); }}
|
||||
>
|
||||
Share JSON
|
||||
{tr('settings.templates.export', 'Export')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -169,7 +174,7 @@ export function TemplateSettings() {
|
||||
icon={<Upload size={14} color={c.text} />}
|
||||
onPress={() => setImportVisible(true)}
|
||||
>
|
||||
Import
|
||||
{tr('settings.templates.import', 'Import')}
|
||||
</Button>
|
||||
</View>
|
||||
</SettingsSection>
|
||||
@@ -180,42 +185,42 @@ export function TemplateSettings() {
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
{editing?.id ? 'Edit template' : 'New template'}
|
||||
{editing?.id ? tr('settings.templates.edit', 'Edit Template') : tr('settings.templates.add', 'New Template')}
|
||||
</Text>
|
||||
<Pressable onPress={closeEditor} hitSlop={8}>
|
||||
<Pressable onPress={closeEditor} hitSlop={8} accessibilityRole="button" accessibilityLabel={tr('common.close', 'Close')}>
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalBody}>
|
||||
<Text style={styles.fieldLabel}>Name</Text>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.name', 'Template Name')}</Text>
|
||||
<TextInput
|
||||
value={draftName}
|
||||
onChangeText={setDraftName}
|
||||
placeholder="Quick reply"
|
||||
placeholder={tr('settings.templates.name_placeholder', 'e.g., Follow-up email')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>Subject</Text>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.subject', 'Subject')}</Text>
|
||||
<TextInput
|
||||
value={draftSubject}
|
||||
onChangeText={setDraftSubject}
|
||||
placeholder="(optional)"
|
||||
placeholder={tr('settings.templates.subject_placeholder', 'Email subject line')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>Body</Text>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.body', 'Body')}</Text>
|
||||
<TextInput
|
||||
value={draftBody}
|
||||
onChangeText={setDraftBody}
|
||||
placeholder="Hi {recipient_name}, …"
|
||||
placeholder={tr('settings.templates.body_placeholder', 'Email body content...')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
multiline
|
||||
style={[styles.input, styles.bodyInput]}
|
||||
/>
|
||||
</ScrollView>
|
||||
<View style={styles.modalActions}>
|
||||
<Button variant="outline" size="sm" onPress={closeEditor}>Cancel</Button>
|
||||
<Button variant="default" size="sm" onPress={saveDraft}>Save</Button>
|
||||
<Button variant="outline" size="sm" onPress={closeEditor}>{tr('common.cancel', 'Cancel')}</Button>
|
||||
<Button variant="default" size="sm" onPress={saveDraft}>{tr('common.save', 'Save')}</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -226,8 +231,8 @@ export function TemplateSettings() {
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Import templates</Text>
|
||||
<Pressable onPress={() => setImportVisible(false)} hitSlop={8}>
|
||||
<Text style={styles.modalTitle}>{tr('settings.templates.import', 'Import')}</Text>
|
||||
<Pressable onPress={() => setImportVisible(false)} hitSlop={8} accessibilityRole="button" accessibilityLabel={tr('common.close', 'Close')}>
|
||||
<X size={20} color={c.text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
@@ -245,9 +250,9 @@ export function TemplateSettings() {
|
||||
/>
|
||||
</ScrollView>
|
||||
<View style={styles.modalActions}>
|
||||
<Button variant="outline" size="sm" onPress={() => setImportVisible(false)}>Cancel</Button>
|
||||
<Button variant="outline" size="sm" onPress={() => setImportVisible(false)}>{tr('common.cancel', 'Cancel')}</Button>
|
||||
<Button variant="default" size="sm" onPress={performImport} disabled={!importText.trim()}>
|
||||
Import
|
||||
{tr('settings.templates.import', 'Import')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user