feat: update dialog confirmation handling across components and improve accessibility with ARIA attributes

This commit is contained in:
Ivan 2026-08-15 16:16:38 -05:00
parent ae7eecf82d
commit 5c257a101b
No known key found for this signature in database
32 changed files with 696 additions and 162 deletions

Binary file not shown.

View file

@ -2,12 +2,22 @@
<template>
<Transition name="confirm-dialog">
<div v-if="pendingConfirm" class="fixed inset-0 z-9999 flex items-center justify-center p-4">
<div
v-if="pendingConfirm"
class="fixed inset-0 z-9999 flex items-center justify-center p-4"
role="alertdialog"
aria-modal="true"
:aria-labelledby="titleId"
:aria-describedby="messageId"
>
<div class="fixed inset-0 bg-black/50 backdrop-blur-xs shadow-2xl" @click="cancel"></div>
<div
ref="dialogPanel"
class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"
tabindex="-1"
@click.stop
@keydown.esc.prevent="cancel"
>
<div class="p-8">
<div class="flex items-start mb-6">
@ -17,10 +27,13 @@
<MaterialDesignIcon icon-name="alert-circle" class="w-6 h-6" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-xl font-black text-gray-900 dark:text-white mb-2">
{{ $t("common.confirm_action") }}
<h3 :id="titleId" class="text-xl font-black text-gray-900 dark:text-white mb-2">
{{ pendingConfirm.title || $t("common.confirm_action") }}
</h3>
<p class="text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">
<p
:id="messageId"
class="text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed"
>
{{ pendingConfirm.message }}
</p>
</div>
@ -61,21 +74,48 @@ export default {
return {
pendingConfirm: null,
resolvePromise: null,
titleId: "confirm-dialog-title",
messageId: "confirm-dialog-message",
};
},
mounted() {
GlobalEmitter.on("confirm", this.show);
window.addEventListener("keydown", this.onWindowKeydown);
},
beforeUnmount() {
GlobalEmitter.off("confirm", this.show);
window.removeEventListener("keydown", this.onWindowKeydown);
},
methods: {
show({ message, resolve }) {
show({ message, title, resolve }) {
if (typeof this.resolvePromise === "function") {
this.resolvePromise(false);
}
this.pendingConfirm = { message };
this.pendingConfirm = {
message,
title: typeof title === "string" && title.trim() ? title.trim() : "",
};
this.resolvePromise = resolve;
this.$nextTick(() => {
const panel = this.$refs.dialogPanel;
if (panel && typeof panel.focus === "function") {
panel.focus();
}
});
},
onWindowKeydown(event) {
if (!this.pendingConfirm) {
return;
}
if (event.key === "Escape") {
event.preventDefault();
this.cancel();
return;
}
if (event.key === "Enter") {
event.preventDefault();
this.confirm();
}
},
confirm() {
if (this.resolvePromise) {

View file

@ -23,6 +23,8 @@
<button
type="button"
class="rounded-lg p-1 text-sem-fg-muted hover:bg-sem-surface/60 sm:hidden"
:aria-label="$t('common.back')"
:title="$t('common.back')"
@click="selectedNodeHash = null"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-5" />
@ -233,6 +235,8 @@ import {
import { renderNomadPageByPath, isolateNomadLinksInHtml } from "../../js/NomadPageRenderer.js";
import { handleRichHtmlLinkClick } from "../../js/NomadRichHtmlLinks.js";
import ArchiveSidebar from "./ArchiveSidebar.vue";
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
export default {
name: "ArchivesPage",
@ -457,7 +461,11 @@ export default {
async deleteSelected() {
if (this.selectedArchives.length === 0) return;
if (!confirm(`Are you sure you want to delete ${this.selectedArchives.length} selected snapshots?`)) {
if (
!(await DialogUtils.confirm(
this.$t("archives.delete_selected_confirm", { count: this.selectedArchives.length })
))
) {
return;
}
@ -480,11 +488,11 @@ export default {
}
} catch (e) {
console.error("Failed to delete archives:", e);
alert("Failed to delete snapshots. Please try again.");
ToastUtils.error(this.$t("archives.failed_delete"));
}
},
async deleteArchive(archive) {
if (!confirm("Are you sure you want to delete this snapshot?")) {
if (!(await DialogUtils.confirm(this.$t("archives.delete_snapshot_confirm")))) {
return;
}
@ -507,7 +515,7 @@ export default {
}
} catch (e) {
console.error("Failed to delete archive:", e);
alert("Failed to delete snapshot. Please try again.");
ToastUtils.error(this.$t("archives.failed_delete"));
}
},
viewArchive(archive) {

View file

@ -29,9 +29,11 @@
class="size-8 text-red-600 dark:text-red-400"
/>
</div>
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2">LXST is disabled</h2>
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
{{ $t("call.lxst_disabled_title") }}
</h2>
<p class="text-sm text-gray-500 dark:text-zinc-400 mb-6">
Telephony is currently disabled. Enable it to make and receive calls.
{{ $t("call.lxst_disabled_body") }}
</p>
<button
type="button"
@ -39,7 +41,7 @@
@click="updateConfig({ telephone_enabled: true })"
>
<MaterialDesignIcon icon-name="phone" class="size-5" />
Enable LXST
{{ $t("call.enable_lxst") }}
</button>
</div>
</div>
@ -598,7 +600,7 @@
<input
v-model="destinationHash"
type="text"
placeholder="Identity Hash or Name"
:placeholder="$t('call.identity_or_name')"
class="input-field"
@keydown.enter.prevent="handleCallInputEnter"
@keydown.up.prevent="handleCallInputUp"
@ -1440,7 +1442,7 @@
<input
v-model="recordingSearch"
type="text"
placeholder="Search recordings..."
:placeholder="$t('call.search_recordings')"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onRecordingSearchInput"
/>
@ -1691,20 +1693,20 @@
v-model="contactForm.lxmf_address"
type="text"
class="input-field font-mono text-xs"
placeholder="Optional"
:placeholder="$t('common.optional')"
/>
</div>
<div>
<label
class="block text-xs font-bold text-gray-500 dark:text-zinc-400 uppercase tracking-wider mb-1.5 ml-1"
>
LXST Address
{{ $t("identities.lxst_address") }}
</label>
<input
v-model="contactForm.lxst_address"
type="text"
class="input-field font-mono text-xs"
placeholder="Optional"
:placeholder="$t('common.optional')"
/>
</div>
</div>
@ -1757,6 +1759,7 @@ import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import LxmfUserIcon from "../LxmfUserIcon.vue";
import Toggle from "../forms/Toggle.vue";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import {
WEB_AUDIO_MIC_TOAST_KEY,
classifyGetUserMediaError,
@ -3277,13 +3280,13 @@ export default {
if (this.config) {
this.config.call_recording_enabled = value;
}
ToastUtils.success(value ? "Call recording enabled" : "Call recording disabled");
ToastUtils.success(value ? this.$t("call.recording_enabled") : this.$t("call.recording_disabled"));
} catch {
ToastUtils.error(this.$t("call.failed_to_update_recording_status"));
}
},
async clearHistory() {
if (!confirm(this.$t("common.delete_confirm"))) return;
if (!(await DialogUtils.confirm(this.$t("common.delete_confirm")))) return;
try {
await window.api.delete("/api/v1/telephone/history");
this.callHistory = [];
@ -3303,7 +3306,7 @@ export default {
}
},
async blockIdentity(hash) {
if (!confirm(`Are you sure you want to banish this identity?`)) return;
if (!(await DialogUtils.confirm(this.$t("call.banish_identity_confirm")))) return;
try {
await window.api.post("/api/v1/blocked-destinations", {
destination_hash: hash,
@ -3339,7 +3342,7 @@ export default {
}
},
async deleteRingtone(ringtone) {
if (!confirm(this.$t("common.delete_confirm"))) return;
if (!(await DialogUtils.confirm(this.$t("common.delete_confirm")))) return;
try {
await window.api.delete(`/api/v1/telephone/ringtones/${ringtone.id}`);
ToastUtils.success(this.$t("call.ringtone_deleted"));
@ -3578,11 +3581,11 @@ export default {
this.isContactModalOpen = false;
this.getContacts();
} catch (e) {
ToastUtils.error(e.response?.data?.message || "Failed to save contact");
ToastUtils.error(e.response?.data?.message || this.$t("call.failed_to_save_contact"));
}
},
async deleteContact(contactId) {
if (!confirm("Are you sure you want to delete this contact?")) return;
if (!(await DialogUtils.confirm(this.$t("call.delete_contact_confirm")))) return;
try {
await window.api.delete(`/api/v1/telephone/contacts/${contactId}`);
ToastUtils.success(this.$t("call.contact_deleted"));
@ -3629,7 +3632,7 @@ export default {
ToastUtils.success(this.$t("call.greeting_generated_successfully"));
await this.getVoicemailStatus();
} catch (e) {
ToastUtils.error(e.response?.data?.message || "Failed to generate greeting");
ToastUtils.error(e.response?.data?.message || this.$t("call.failed_to_generate_greeting"));
} finally {
this.isGeneratingGreeting = false;
}
@ -3651,14 +3654,14 @@ export default {
ToastUtils.success(this.$t("call.greeting_uploaded_successfully"));
await this.getVoicemailStatus();
} catch (e) {
ToastUtils.error(e.response?.data?.message || "Failed to upload greeting");
ToastUtils.error(e.response?.data?.message || this.$t("call.failed_to_upload_greeting"));
} finally {
this.isUploadingGreeting = false;
event.target.value = "";
}
},
async deleteGreeting() {
if (!confirm("Are you sure you want to delete your custom greeting?")) return;
if (!(await DialogUtils.confirm(this.$t("call.delete_greeting_confirm")))) return;
try {
await window.api.delete("/api/v1/telephone/voicemail/greeting");
@ -3796,7 +3799,7 @@ export default {
}
},
async deleteRecording(recordingId) {
if (!confirm("Are you sure you want to delete this recording?")) return;
if (!(await DialogUtils.confirm(this.$t("call.delete_recording_confirm")))) return;
try {
await window.api.delete(`/api/v1/telephone/recordings/${recordingId}`);
this.getRecordings();
@ -3873,7 +3876,7 @@ export default {
await window.api.post(`/api/v1/telephone/call/${hashToCall}`);
} catch (e) {
this.initiationStatus = null;
ToastUtils.error(e.response?.data?.message || "Failed to initiate call");
ToastUtils.error(e.response?.data?.message || this.$t("call.failed_to_initiate_call"));
}
},
handleCallInputUp() {

View file

@ -7,7 +7,7 @@
<input
:value="contactsSearch"
type="text"
placeholder="Search contacts..."
:placeholder="$t('contacts.search_placeholder')"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onSearchInput"
/>
@ -21,7 +21,7 @@
@click="$emit('add')"
>
<MaterialDesignIcon icon-name="plus" class="size-5" />
Add
{{ $t("common.add") }}
</button>
</div>
@ -29,8 +29,8 @@
<div class="bg-gray-200 dark:bg-zinc-800 p-6 rounded-full inline-block mb-4">
<MaterialDesignIcon icon-name="account-multiple" class="size-12 text-gray-400" />
</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">No Contacts</h3>
<p class="text-gray-500 dark:text-zinc-400">Add contacts to quickly call them.</p>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ $t("contacts.no_contacts") }}</h3>
<p class="text-gray-500 dark:text-zinc-400">{{ $t("call.no_contacts_hint") }}</p>
</div>
<div v-else class="space-y-4">
@ -64,15 +64,21 @@
<span
v-if="contact.preferred_ringtone_id"
class="text-[9px] px-1.5 py-0.5 rounded-sm bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border border-amber-100 dark:border-amber-800/50 flex items-center gap-1"
title="Custom Ringtone Set"
:title="$t('call.custom_ringtone_set')"
>
<MaterialDesignIcon icon-name="music" class="size-2.5" />
{{ contact.preferred_ringtone_id === -1 ? "Random" : "Custom" }}
{{
contact.preferred_ringtone_id === -1
? $t("call.random")
: $t("call.custom")
}}
</span>
<div class="flex items-center gap-1">
<button
type="button"
class="p-1.5 text-gray-400 hover:text-blue-500 transition-colors"
:aria-label="$t('common.edit')"
:title="$t('common.edit')"
@click="$emit('edit', contact)"
>
<MaterialDesignIcon icon-name="pencil" class="size-4" />
@ -80,6 +86,8 @@
<button
type="button"
class="p-1.5 text-gray-400 hover:text-red-500 transition-colors"
:aria-label="$t('common.delete')"
:title="$t('common.delete')"
@click="$emit('delete', contact.id)"
>
<MaterialDesignIcon icon-name="delete" class="size-4" />

View file

@ -7,7 +7,7 @@
<input
:value="voicemailSearch"
type="text"
placeholder="Search voicemails..."
:placeholder="$t('call.search_voicemails')"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onSearchInput"
/>
@ -27,7 +27,7 @@
<div class="flex items-center gap-2">
<MaterialDesignIcon icon-name="cog" class="size-5 text-blue-500" />
<h3 class="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider">
Voicemail Settings
{{ $t("call.voicemail_settings") }}
</h3>
</div>
<MaterialDesignIcon
@ -79,7 +79,7 @@
:value="config.voicemail_greeting"
rows="3"
class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-900"
placeholder="Enter greeting text..."
:placeholder="$t('call.enter_greeting_text')"
@input="$emit('patch-config', { voicemail_greeting: $event.target.value })"
></textarea>

View file

@ -749,9 +749,12 @@ export default {
const duplicates = this.contacts.filter((c) => c.name === contact.name && c.id !== contact.id);
const confirmMsg =
duplicates.length > 0
? `${this.$t("contacts.remove_contact_confirm")}\n\n(${duplicates.length} additional duplicate${duplicates.length > 1 ? "s" : ""} named "${contact.name}" will also be removed)`
? this.$t("contacts.remove_duplicates_confirm", {
count: duplicates.length,
name: contact.name,
})
: this.$t("contacts.remove_contact_confirm");
if (!window.confirm(confirmMsg)) return;
if (!(await DialogUtils.confirm(confirmMsg))) return;
try {
const ids = [contact.id, ...duplicates.map((c) => c.id)];
for (const id of ids) {

View file

@ -952,6 +952,7 @@ import ContextMenuItem from "../contextmenu/ContextMenuItem.vue";
import ContextMenuPanel from "../contextmenu/ContextMenuPanel.vue";
import DOMPurify from "dompurify";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import TileCache from "../../js/TileCache";
import { mapViewStateKey } from "../../js/mapStateKeys.js";
import GlobalState from "../../js/GlobalState";
@ -1560,7 +1561,7 @@ export default {
}
},
async deleteMBTiles(filename) {
if (!confirm(`Are you sure you want to delete ${filename}?`)) return;
if (!(await DialogUtils.confirm(this.$t("map.delete_file_confirm", { name: filename })))) return;
try {
await window.api.delete(`/api/v1/map/mbtiles/${filename}`);
await this.loadMBTilesList();
@ -3837,24 +3838,24 @@ export default {
this.stopMeasuring();
},
clearDrawings() {
if (confirm("Clear all drawings from the map?")) {
this.drawSource.clear();
if (this.select) {
this.select.getFeatures().clear();
}
this.selectedFeature = null;
this.syncDrawFeatureInfoOverlay();
// clear tooltips if any
const overlays = this.map.getOverlays().getArray();
for (let i = overlays.length - 1; i >= 0; i--) {
const overlay = overlays[i];
if (overlay.get("isMeasureTooltip")) {
this.map.removeOverlay(overlay);
}
}
this.saveMapState();
async clearDrawings() {
if (!(await DialogUtils.confirm(this.$t("map.clear_drawings_confirm")))) {
return;
}
this.drawSource.clear();
if (this.select) {
this.select.getFeatures().clear();
}
this.selectedFeature = null;
this.syncDrawFeatureInfoOverlay();
const overlays = this.map.getOverlays().getArray();
for (let i = overlays.length - 1; i >= 0; i--) {
const overlay = overlays[i];
if (overlay.get("isMeasureTooltip")) {
this.map.removeOverlay(overlay);
}
}
this.saveMapState();
},
// Measurement methods
@ -4667,7 +4668,7 @@ export default {
},
async deleteDrawing(drawing) {
if (!confirm(`Delete drawing "${drawing.name}"?`)) return;
if (!(await DialogUtils.confirm(this.$t("map.delete_drawing_confirm", { name: drawing.name })))) return;
try {
await window.api.delete(`/api/v1/map/drawings/${drawing.id}`);
this.savedDrawings = this.savedDrawings.filter((d) => d.id !== drawing.id);
@ -5149,10 +5150,12 @@ export default {
const t = this.telemetryList.find((t) => t.destination_hash === hash);
if (t) t.is_tracking = response.data.is_tracking;
ToastUtils.success(response.data.is_tracking ? "Live tracking enabled" : "Live tracking disabled");
ToastUtils.success(
response.data.is_tracking ? this.$t("map.tracking_enabled") : this.$t("map.tracking_disabled")
);
} catch (e) {
console.error("Failed to toggle tracking", e);
ToastUtils.error("Failed to update tracking status");
ToastUtils.error(this.$t("map.failed_update_tracking"));
}
},
async toggleDiscoveredNodes() {

View file

@ -9,7 +9,9 @@
<div v-if="loading" class="flex justify-center py-4">
<v-icon icon="mdi-loading" class="animate-spin text-gray-400" size="20"></v-icon>
</div>
<div v-else-if="messages.length === 0" class="text-center py-4 text-xs text-gray-400">No messages yet</div>
<div v-else-if="messages.length === 0" class="text-center py-4 text-xs text-gray-400">
{{ $t("messages.no_messages_yet") }}
</div>
<div
v-for="msg in messages"
:key="msg.hash"
@ -30,9 +32,11 @@
class="flex items-center gap-1 mb-1 pb-1 border-b border-white/10 opacity-80"
>
<v-icon icon="mdi-satellite-variant" size="10"></v-icon>
<span class="text-[8px] font-bold uppercase tracking-wider"
>{{ msg.is_outbound ? "Sent" : "Received" }} Telemetry</span
>
<span class="text-[8px] font-bold uppercase tracking-wider">{{
msg.is_outbound
? $t("messages.telemetry_label_sent")
: $t("messages.telemetry_label_received")
}}</span>
</div>
<div
@ -40,7 +44,9 @@
class="flex items-center gap-1 mb-1 pb-1 border-b border-white/10 opacity-80"
>
<v-icon icon="mdi-crosshairs-question" size="10"></v-icon>
<span class="text-[8px] font-bold uppercase tracking-wider">Location Request</span>
<span class="text-[8px] font-bold uppercase tracking-wider">{{
$t("messages.telemetry_location_request")
}}</span>
</div>
<div v-if="msg.content" class="leading-normal">{{ msg.content }}</div>
@ -82,11 +88,14 @@
v-model="newMessage"
type="text"
class="flex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100"
placeholder="Type a message..."
:placeholder="$t('messages.send_placeholder')"
@keydown.enter="sendMessage"
/>
<button
type="button"
:disabled="!newMessage.trim() || sending"
:aria-label="$t('messages.send')"
:title="$t('messages.send')"
class="p-1.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 dark:disabled:bg-zinc-700 text-white rounded-md transition-colors"
@click="sendMessage"
>

View file

@ -321,11 +321,7 @@ export default {
}
},
async onBlockDestination() {
if (
!(await DialogUtils.confirm(
"Are you sure you want to banish this user? They will not be able to send you messages or establish links."
))
) {
if (!(await DialogUtils.confirm(this.$t("messages.banish_confirm")))) {
return;
}

View file

@ -1340,7 +1340,7 @@
v-if="!translateTargetSelectOptions.length"
class="text-xs text-amber-700/90 dark:text-amber-300/90 -mt-0.5"
>
No translation languages available yet. Check the translator tool in Tools.
{{ $t("messages.translate_no_languages") }}
</p>
</div>
</div>
@ -3975,12 +3975,7 @@ export default {
},
async onBanishHeaderClick() {
if (!this.selectedPeer) return;
if (
!(await DialogUtils.confirm(
this.$t("messages.banish_confirm") ||
"Are you sure you want to banish this user? They will not be able to send you messages or establish links."
))
) {
if (!(await DialogUtils.confirm(this.$t("messages.banish_confirm")))) {
return;
}
try {
@ -5425,12 +5420,7 @@ export default {
async deleteChatItem(chatItem, shouldConfirm = true) {
try {
// ask user to confirm deleting message
if (
shouldConfirm &&
!(await DialogUtils.confirm(
"Are you sure you want to delete this message? This can not be undone!"
))
) {
if (shouldConfirm && !(await DialogUtils.confirm(this.$t("messages.delete_message_confirm")))) {
return;
}
@ -5558,7 +5548,7 @@ export default {
if (totalMessageSize > 1000 * 900) {
if (
!(await DialogUtils.confirm(
`Your message exceeds 900KB (It's ${this.formatBytes(totalMessageSize)}). It may be rejected by the recipient unless they have increased their delivery limit. Do you want to try sending anyway?`
this.$t("messages.send_oversized_confirm", { size: this.formatBytes(totalMessageSize) })
))
) {
return null;
@ -5884,11 +5874,7 @@ export default {
);
if (failedItems.length === 0) return;
if (
!(await DialogUtils.confirm(
`Are you sure you want to retry sending all ${failedItems.length} failed/cancelled messages?`
))
) {
if (!(await DialogUtils.confirm(this.$t("messages.retry_failed_confirm", { count: failedItems.length })))) {
return;
}
@ -6197,10 +6183,12 @@ export default {
destination_hash: hash,
is_tracking: response.data.is_tracking,
});
ToastUtils.success(response.data.is_tracking ? "Live tracking enabled" : "Live tracking disabled");
ToastUtils.success(
response.data.is_tracking ? this.$t("map.tracking_enabled") : this.$t("map.tracking_disabled")
);
} catch (e) {
console.error("Failed to toggle tracking", e);
ToastUtils.error("Failed to update tracking status");
ToastUtils.error(this.$t("map.failed_update_tracking"));
}
},
formatTimeAgo: function (datetimeString) {
@ -6280,7 +6268,7 @@ export default {
try {
await window.api.post(`/api/v1/telephone/call/${this.selectedPeer.destination_hash}`);
} catch (e) {
const message = e.response?.data?.message ?? "Failed to start call";
const message = e.response?.data?.message ?? this.$t("call.failed_to_initiate_call");
DialogUtils.alert(message);
}
},

View file

@ -1207,8 +1207,8 @@ export default {
async onBulkDelete(destination_hashes) {
try {
const confirmed = await DialogUtils.confirm(
"Are you sure you want to delete these conversations? All messages will be lost.",
"Delete Conversations"
this.$t("messages.delete_conversations_confirm"),
this.$t("messages.delete_conversations_title")
);
if (!confirmed) return;

View file

@ -143,6 +143,7 @@
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import Toggle from "../forms/Toggle.vue";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import NotificationSoundUtils from "../../js/NotificationSoundUtils";
export default {
@ -225,7 +226,7 @@ export default {
}
},
async deleteSound(sound) {
if (!confirm(this.$t("common.delete_confirm"))) {
if (!(await DialogUtils.confirm(this.$t("common.delete_confirm")))) {
return;
}
try {

View file

@ -363,9 +363,9 @@ export default {
};
this.sidebandPlugins = response.data?.plugins || [];
},
onSidebandMasterToggle() {
async onSidebandMasterToggle() {
if (this.sidebandConfig.service_plugins_enabled) {
const ok = window.confirm(this.$t("plugins.sideband.danger_confirm"));
const ok = await DialogUtils.confirm(this.$t("plugins.sideband.danger_confirm"));
if (!ok) {
this.sidebandConfig.service_plugins_enabled = false;
}
@ -461,9 +461,9 @@ export default {
this.busyPluginId = null;
}
},
confirmRemove(plugin) {
async confirmRemove(plugin) {
const prompt = this.$t("plugins.settings.confirm_remove", { name: plugin.name || plugin.id });
if (!window.confirm(prompt)) {
if (!(await DialogUtils.confirm(prompt))) {
return;
}
void this.removePlugin(plugin.id);

View file

@ -3818,7 +3818,7 @@ export default {
this.getTrustedTelemetryPeers();
ToastUtils.success(this.$t("app.telemetry_trust_revoked", { name: peer.name }));
} catch (e) {
ToastUtils.error("Failed to revoke telemetry trust");
ToastUtils.error(this.$t("app.telemetry_trust_failed"));
console.error(e);
}
},
@ -4900,9 +4900,9 @@ export default {
try {
const newConfig = await patchServerConfig({ telephone_enabled: value }, window.api);
this.config = newConfig;
ToastUtils.success(value ? "Telephone enabled" : "Telephone disabled");
ToastUtils.success(value ? this.$t("call.telephony_enabled") : this.$t("call.telephony_disabled"));
} catch {
ToastUtils.error("Failed to update telephone setting");
ToastUtils.error(this.$t("call.failed_to_update_call_settings"));
}
},
async onDesktopOpenCallsInSeparateWindowChange(value) {
@ -4976,11 +4976,7 @@ export default {
}, 1000);
},
async flushArchivedPages() {
if (
!(await DialogUtils.confirm(
"Are you sure you want to delete all archived pages? This cannot be undone."
))
) {
if (!(await DialogUtils.confirm(this.$t("settings.flush_archived_pages_confirm")))) {
return;
}
WebSocketConnection.send(

View file

@ -4,7 +4,7 @@
<section v-show="visible" class="settings-section break-inside-avoid">
<header class="settings-section__header">
<div>
<div class="settings-section__eyebrow">Personalise</div>
<div class="settings-section__eyebrow">{{ $t("app.appearance") }}</div>
<h2>{{ $t("app.appearance") }}</h2>
<p>{{ $t("app.appearance_description") }}</p>
</div>
@ -49,7 +49,9 @@
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Message Font Size</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.message_font_size") }}
</div>
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
{{ config.message_font_size || 14 }}px
</div>
@ -71,7 +73,9 @@
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Icon Size</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.message_icon_size") }}
</div>
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
{{ config.message_icon_size || 28 }}px
</div>
@ -257,7 +261,9 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Outbound Color</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("settings.outbound_bubble_color") }}
</div>
<div class="flex gap-2">
<input
:value="config.message_outbound_bubble_color"
@ -275,7 +281,9 @@
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Failed Color</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("settings.failed_bubble_color") }}
</div>
<div class="flex gap-2">
<input
:value="config.message_failed_bubble_color"
@ -293,7 +301,9 @@
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Waiting Color</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("settings.waiting_bubble_color") }}
</div>
<div class="flex gap-2">
<input
:value="config.message_waiting_bubble_color"
@ -313,14 +323,16 @@
<div class="space-y-2">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Inbound Color (Optional)</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("settings.inbound_bubble_color") }}
</div>
<button
v-if="config.message_inbound_bubble_color"
type="button"
class="text-[10px] text-red-500 font-bold uppercase hover:underline"
@click="onInboundBubbleReset"
>
Reset to default
{{ $t("settings.inbound_bubble_reset") }}
</button>
</div>
<div class="flex gap-2">
@ -335,12 +347,13 @@
v-if="!config.message_inbound_bubble_color"
class="flex-1 flex items-center px-3 text-xs text-gray-400 bg-gray-50 dark:bg-zinc-900 rounded-xl border border-dashed border-gray-200 dark:border-zinc-800 italic"
>
Using theme default. Click to customize ->
{{ $t("settings.inbound_bubble_default_hint") }}
<button
type="button"
class="ml-2 px-2 py-1 bg-blue-500 text-white rounded-lg not-italic font-bold"
@click="onInboundBubbleCustomize"
>
Customize
{{ $t("settings.inbound_bubble_customize") }}
</button>
</div>
<input

View file

@ -400,6 +400,7 @@
<script>
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import DownloadUtils from "../../js/DownloadUtils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToolsPageHeader from "./ToolsPageHeader.vue";
@ -521,7 +522,7 @@ export default {
}
},
async deleteBot(botId) {
if (!confirm(this.$t("common.delete_confirm"))) return;
if (!(await DialogUtils.confirm(this.$t("common.delete_confirm")))) return;
try {
await window.api.post("/api/v1/bots/delete", { bot_id: botId });
ToastUtils.success(this.$t("bots.bot_deleted"));

View file

@ -183,6 +183,7 @@ import WifiTransport from "../../js/rnode/transports/WifiTransport.js";
import { diagnose } from "../../js/rnode/Diagnostics.js";
import ToastUtils from "../../js/ToastUtils.js";
import DialogUtils from "../../js/DialogUtils.js";
import ToolsPageHeader from "./ToolsPageHeader.vue";
import { rnodeIntegrityKeyForSrc } from "../../js/rnode/rnodeIntegrityKey.js";
@ -584,7 +585,7 @@ export default {
}
},
async wipeEeprom() {
if (!confirm(this.$t("tools.rnode_flasher.alerts.eeprom_wipe_confirm"))) {
if (!(await DialogUtils.confirm(this.$t("tools.rnode_flasher.alerts.eeprom_wipe_confirm")))) {
return;
}
try {

View file

@ -178,6 +178,7 @@
<script>
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import ToolsPageHeader from "./ToolsPageHeader.vue";
export default {
@ -365,7 +366,7 @@ export default {
}
},
async deleteUpload(name) {
if (!window.confirm(this.$t("tools.repository_server.delete_confirm", { name }))) {
if (!(await DialogUtils.confirm(this.$t("tools.repository_server.delete_confirm", { name })))) {
return;
}
try {

View file

@ -3,32 +3,24 @@ import GlobalEmitter from "./GlobalEmitter";
class DialogUtils {
static alert(message, type = "info") {
if (window.electron) {
// running inside electron, use ipc alert
window.electron.alert(message);
}
// always show toast as well (or instead of browser alert)
GlobalEmitter.emit("toast", { message, type });
}
static confirm(message) {
if (window.electron) {
// running inside electron, use ipc confirm
return window.electron.confirm(message);
} else {
// running inside normal browser, use custom confirm dialog
return new Promise((resolve) => {
GlobalEmitter.emit("confirm", { message, resolve });
});
}
static confirm(message, title) {
return new Promise((resolve) => {
const payload = { message, resolve };
if (typeof title === "string" && title.trim()) {
payload.title = title.trim();
}
GlobalEmitter.emit("confirm", payload);
});
}
// Always use the in-app confirm dialog, even inside electron, for
// callers that want a themed dialog instead of the native OS prompt.
static confirmCustom(message) {
return new Promise((resolve) => {
GlobalEmitter.emit("confirm", { message, resolve });
});
static confirmCustom(message, title) {
return DialogUtils.confirm(message, title);
}
static async prompt(message, defaultValue = "", options = {}) {

View file

@ -596,7 +596,9 @@
"copy": "Kopieren",
"copy_to_clipboard": "In die Zwischenablage kopieren",
"clear": "Löschen",
"prompt_title": "Wert eingeben"
"prompt_title": "Wert eingeben",
"optional": "Optional",
"back": "Zurück"
},
"stickers": {
"settings_title": "Sticker",
@ -1095,6 +1097,7 @@
"failed_add_contact": "Kontakt konnte nicht hinzugefügt werden",
"remove_contact": "Kontakt entfernen",
"remove_contact_confirm": "Diesen Kontakt entfernen?",
"remove_duplicates_confirm": "Diesen Kontakt entfernen?\n\n{count} weitere Duplikat(e) namens \"{name}\" werden ebenfalls entfernt.",
"contact_removed": "Kontakt entfernt",
"failed_remove_contact": "Kontakt konnte nicht entfernt werden",
"share_contact": "Kontakt-URI teilen",
@ -1595,6 +1598,12 @@
"source_updated": "Kartenquelle aktualisiert",
"failed_set_active": "Aktive Karte setzen fehlgeschlagen",
"file_deleted": "Datei gelöscht",
"delete_file_confirm": "{name} löschen?",
"clear_drawings_confirm": "Alle Zeichnungen von der Karte löschen?",
"delete_drawing_confirm": "Zeichnung \"{name}\" löschen?",
"tracking_enabled": "Live-Verfolgung aktiviert",
"tracking_disabled": "Live-Verfolgung deaktiviert",
"failed_update_tracking": "Verfolgungsstatus konnte nicht aktualisiert werden",
"failed_delete_file": "Datei löschen fehlgeschlagen",
"storage_saved": "Speicherverzeichnis gespeichert",
"failed_save_storage": "Verzeichnis speichern fehlgeschlagen",
@ -1929,6 +1938,11 @@
"marked_all_read": "Alle Konversationen als gelesen markiert",
"failed_mark_read": "Als gelesen markieren fehlgeschlagen",
"conversations_deleted": "Unterhaltungen gelöscht",
"delete_conversations_confirm": "Diese Unterhaltungen löschen? Alle Nachrichten gehen verloren.",
"delete_conversations_title": "Unterhaltungen löschen",
"delete_message_confirm": "Diese Nachricht löschen? Das kann nicht rückgängig gemacht werden.",
"retry_failed_confirm": "Alle {count} fehlgeschlagenen oder abgebrochenen Nachrichten erneut senden?",
"send_oversized_confirm": "Diese Nachricht ist {size}. Empfänger mit dem Standardlimit von 900 KB können sie ablehnen. Trotzdem senden?",
"failed_delete_conversations": "Unterhaltungen konnten nicht gelöscht werden",
"failed_export_folders": "Ordner konnten nicht exportiert werden",
"folders_imported": "Ordner importiert",
@ -2271,7 +2285,10 @@
"hide_nodes": "Knoten ausblenden",
"show_snapshots": "Schnappschüsse anzeigen",
"hide_snapshots": "Schnappschüsse ausblenden",
"delete_snapshot": "Schnappschuss löschen"
"delete_snapshot": "Schnappschuss löschen",
"delete_selected_confirm": "{count} ausgewählte Schnappschüsse löschen?",
"delete_snapshot_confirm": "Diesen Schnappschuss löschen?",
"failed_delete": "Schnappschuss konnte nicht gelöscht werden. Bitte erneut versuchen."
},
"docs": {
"title": "Dokumentation",
@ -3458,6 +3475,25 @@
"failed_load_audio_edit": "Fehler beim Laden des Audios zur Bearbeitung",
"ringtone_saved": "Klingelton erfolgreich gespeichert",
"failed_save_ringtone": "Fehler beim Speichern des bearbeiteten Klingeltons",
"lxst_disabled_title": "LXST ist deaktiviert",
"lxst_disabled_body": "Telefonie ist derzeit deaktiviert. Aktivieren Sie sie, um Anrufe zu tätigen und entgegenzunehmen.",
"enable_lxst": "LXST aktivieren",
"identity_or_name": "Identitätshash oder Name",
"search_recordings": "Aufnahmen suchen...",
"search_voicemails": "Voicemails suchen...",
"banish_identity_confirm": "Diese Identität verbannen?",
"delete_contact_confirm": "Diesen Kontakt löschen?",
"delete_greeting_confirm": "Die eigene Begrüßung löschen?",
"delete_recording_confirm": "Diese Aufnahme löschen?",
"recording_enabled": "Anrufaufzeichnung aktiviert",
"recording_disabled": "Anrufaufzeichnung deaktiviert",
"failed_to_save_contact": "Kontakt konnte nicht gespeichert werden",
"failed_to_upload_greeting": "Begrüßung konnte nicht hochgeladen werden",
"telephony_enabled": "Telefonie aktiviert",
"telephony_disabled": "Telefonie deaktiviert",
"no_contacts_hint": "Fügen Sie Kontakte hinzu, um sie schnell anzurufen.",
"custom": "Benutzerdefiniert",
"custom_ringtone_set": "Eigener Klingelton gesetzt",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Erlauben Sie den Mikrofonzugriff, wenn der Browser danach fragt, und klicken Sie danach erneut auf Geräte aktualisieren.",
@ -3703,6 +3739,14 @@
"shortcut_saved": "Verknüpfung gespeichert",
"shortcut_deleted": "Verknüpfung gelöscht",
"archived_pages_flushed": "Archivierte Seiten geleert.",
"flush_archived_pages_confirm": "Alle archivierten Seiten löschen? Das kann nicht rückgängig gemacht werden.",
"outbound_bubble_color": "Ausgehende Farbe",
"failed_bubble_color": "Fehlerfarbe",
"waiting_bubble_color": "Wartefarbe",
"inbound_bubble_color": "Eingehende Farbe (optional)",
"inbound_bubble_reset": "Auf Standard zurücksetzen",
"inbound_bubble_customize": "Anpassen",
"inbound_bubble_default_hint": "Es gilt die Standardfarbe des Themas.",
"failed_enable_transport": "Transportmodus aktivieren fehlgeschlagen!",
"failed_disable_transport": "Transportmodus deaktivieren fehlgeschlagen!",
"failed_reload_reticulum": "Reticulum neu laden fehlgeschlagen!",

View file

@ -596,7 +596,9 @@
"loading": "Loading...",
"ok": "OK",
"clear": "Clear",
"prompt_title": "Enter a value"
"prompt_title": "Enter a value",
"optional": "Optional",
"back": "Back"
},
"stickers": {
"settings_title": "Stickers",
@ -1542,6 +1544,12 @@
"source_updated": "Map source updated",
"failed_set_active": "Failed to set active map",
"file_deleted": "File deleted",
"delete_file_confirm": "Delete {name}?",
"clear_drawings_confirm": "Clear all drawings from the map?",
"delete_drawing_confirm": "Delete drawing \"{name}\"?",
"tracking_enabled": "Live tracking enabled",
"tracking_disabled": "Live tracking disabled",
"failed_update_tracking": "Failed to update tracking status",
"failed_delete_file": "Failed to delete file",
"storage_saved": "Storage directory saved",
"failed_save_storage": "Failed to save directory",
@ -1872,6 +1880,11 @@
"marked_all_read": "All conversations marked as read",
"failed_mark_read": "Failed to mark as read",
"conversations_deleted": "Conversations deleted",
"delete_conversations_confirm": "Delete these conversations? All messages will be lost.",
"delete_conversations_title": "Delete conversations",
"delete_message_confirm": "Delete this message? This cannot be undone.",
"retry_failed_confirm": "Retry sending all {count} failed or cancelled messages?",
"send_oversized_confirm": "This message is {size}. Recipients with the default 900 KB delivery limit may reject it. Send anyway?",
"failed_delete_conversations": "Failed to delete conversations",
"failed_export_folders": "Failed to export folders",
"folders_imported": "Folders imported",
@ -2061,6 +2074,14 @@
"keyboard_shortcuts_title": "Keyboard Shortcuts",
"keyboard_shortcuts_description": "Customize quick keyboard actions. Collapsed by default on phones.",
"archived_pages_flushed": "Archived pages flushed.",
"flush_archived_pages_confirm": "Delete all archived pages? This cannot be undone.",
"outbound_bubble_color": "Outbound color",
"failed_bubble_color": "Failed color",
"waiting_bubble_color": "Waiting color",
"inbound_bubble_color": "Inbound color (optional)",
"inbound_bubble_reset": "Reset to default",
"inbound_bubble_customize": "Customize",
"inbound_bubble_default_hint": "Using the theme default.",
"failed_enable_transport": "Failed to enable transport mode!",
"failed_disable_transport": "Failed to disable transport mode!",
"failed_update_reticulum_instance": "Failed to update Reticulum instance settings!",
@ -2488,7 +2509,10 @@
"hide_nodes": "Hide nodes",
"show_snapshots": "Show snapshots",
"hide_snapshots": "Hide snapshots",
"delete_snapshot": "Delete snapshot"
"delete_snapshot": "Delete snapshot",
"delete_selected_confirm": "Delete {count} selected snapshots?",
"delete_snapshot_confirm": "Delete this snapshot?",
"failed_delete": "Failed to delete snapshot. Try again."
},
"docs": {
"title": "Documentation",
@ -4067,7 +4091,26 @@
"failed_to_send_to_voicemail": "Failed to send call to voicemail",
"failed_load_audio_edit": "Failed to load audio for editing",
"ringtone_saved": "Ringtone saved successfully",
"failed_save_ringtone": "Failed to save edited ringtone"
"failed_save_ringtone": "Failed to save edited ringtone",
"lxst_disabled_title": "LXST is disabled",
"lxst_disabled_body": "Telephony is currently disabled. Enable it to make and receive calls.",
"enable_lxst": "Enable LXST",
"identity_or_name": "Identity hash or name",
"search_recordings": "Search recordings...",
"search_voicemails": "Search voicemails...",
"banish_identity_confirm": "Banish this identity?",
"delete_contact_confirm": "Delete this contact?",
"delete_greeting_confirm": "Delete your custom greeting?",
"delete_recording_confirm": "Delete this recording?",
"recording_enabled": "Call recording enabled",
"recording_disabled": "Call recording disabled",
"failed_to_save_contact": "Failed to save contact",
"failed_to_upload_greeting": "Failed to upload greeting",
"telephony_enabled": "Telephony enabled",
"telephony_disabled": "Telephony disabled",
"no_contacts_hint": "Add contacts to call them quickly.",
"custom": "Custom",
"custom_ringtone_set": "Custom ringtone set"
},
"contacts": {
"title": "Contacts",
@ -4090,6 +4133,7 @@
"failed_add_contact": "Failed to add contact",
"remove_contact": "Remove Contact",
"remove_contact_confirm": "Remove this contact?",
"remove_duplicates_confirm": "Remove this contact?\n\n{count} additional duplicate(s) named \"{name}\" will also be removed.",
"contact_removed": "Contact removed",
"failed_remove_contact": "Failed to remove contact",
"share_contact": "Share Contact URI",

View file

@ -596,7 +596,9 @@
"loading": "Cargando...",
"ok": "Aceptar",
"clear": "Limpiar",
"prompt_title": "Introduce un valor"
"prompt_title": "Introduce un valor",
"optional": "Opcional",
"back": "Atrás"
},
"stickers": {
"settings_title": "Pegatinas",
@ -1542,6 +1544,12 @@
"source_updated": "Fuente de mapa actualizada",
"failed_set_active": "No se pudo establecer el mapa activo",
"file_deleted": "Archivo eliminado",
"delete_file_confirm": "¿Eliminar {name}?",
"clear_drawings_confirm": "¿Borrar todos los dibujos del mapa?",
"delete_drawing_confirm": "¿Eliminar el dibujo \"{name}\"?",
"tracking_enabled": "Seguimiento en vivo activado",
"tracking_disabled": "Seguimiento en vivo desactivado",
"failed_update_tracking": "No se pudo actualizar el seguimiento",
"failed_delete_file": "Error al eliminar archivo",
"storage_saved": "Directorio de almacenamiento guardado",
"failed_save_storage": "No se pudo guardar el directorio",
@ -1872,6 +1880,11 @@
"marked_all_read": "Todas las conversaciones marcadas como leídas",
"failed_mark_read": "Error al marcar como leído",
"conversations_deleted": "Conversaciones eliminadas",
"delete_conversations_confirm": "¿Eliminar estas conversaciones? Se perderán todos los mensajes.",
"delete_conversations_title": "Eliminar conversaciones",
"delete_message_confirm": "¿Eliminar este mensaje? Esto no se puede deshacer.",
"retry_failed_confirm": "¿Reenviar los {count} mensajes fallidos o cancelados?",
"send_oversized_confirm": "Este mensaje ocupa {size}. Los destinatarios con el límite predeterminado de 900 KB pueden rechazarlo. ¿Enviar de todos modos?",
"failed_delete_conversations": "Fallado para borrar conversaciones",
"failed_export_folders": "Error al exportar carpetas",
"folders_imported": "Carpetas importadas",
@ -2037,6 +2050,14 @@
"shortcut_saved": "Guardado a mano",
"shortcut_deleted": "Atajo eliminado",
"archived_pages_flushed": "Las páginas archivadas se desplomaron.",
"flush_archived_pages_confirm": "¿Eliminar todas las páginas archivadas? Esto no se puede deshacer.",
"outbound_bubble_color": "Color de salida",
"failed_bubble_color": "Color de error",
"waiting_bubble_color": "Color en espera",
"inbound_bubble_color": "Color de entrada (opcional)",
"inbound_bubble_reset": "Restablecer",
"inbound_bubble_customize": "Personalizar",
"inbound_bubble_default_hint": "Se usa el color del tema.",
"failed_enable_transport": "¡Failed to enable transport mode!",
"failed_disable_transport": "¡Failed to disable transport mode!",
"failed_reload_reticulum": "¡Failed to reload Reticulum!",
@ -2488,7 +2509,10 @@
"hide_nodes": "Ocultar nodos",
"show_snapshots": "Mostrar instantáneas",
"hide_snapshots": "Ocultar instantáneas",
"delete_snapshot": "Eliminar instantánea"
"delete_snapshot": "Eliminar instantánea",
"delete_selected_confirm": "¿Eliminar {count} instantáneas seleccionadas?",
"delete_snapshot_confirm": "¿Eliminar esta instantánea?",
"failed_delete": "No se pudo eliminar la instantánea. Inténtalo de nuevo."
},
"docs": {
"title": "Documentación",
@ -3675,6 +3699,25 @@
"failed_load_audio_edit": "No se puede cargar audio para editar",
"ringtone_saved": "Ringtone se salvó con éxito",
"failed_save_ringtone": "Error al guardar el tono editado",
"lxst_disabled_title": "LXST está desactivado",
"lxst_disabled_body": "La telefonía está desactivada. Actívala para hacer y recibir llamadas.",
"enable_lxst": "Activar LXST",
"identity_or_name": "Hash de identidad o nombre",
"search_recordings": "Buscar grabaciones...",
"search_voicemails": "Buscar buzón de voz...",
"banish_identity_confirm": "¿Desterrar esta identidad?",
"delete_contact_confirm": "¿Eliminar este contacto?",
"delete_greeting_confirm": "¿Eliminar tu saludo personalizado?",
"delete_recording_confirm": "¿Eliminar esta grabación?",
"recording_enabled": "Grabación de llamadas activada",
"recording_disabled": "Grabación de llamadas desactivada",
"failed_to_save_contact": "No se pudo guardar el contacto",
"failed_to_upload_greeting": "No se pudo subir el saludo",
"telephony_enabled": "Telefonía activada",
"telephony_disabled": "Telefonía desactivada",
"no_contacts_hint": "Añade contactos para llamarlos rápido.",
"custom": "Personalizado",
"custom_ringtone_set": "Tono personalizado configurado",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Permita el acceso al micrófono cuando el navegador lo solicite y luego pulse Actualizar dispositivos de nuevo.",
@ -3701,6 +3744,7 @@
"failed_add_contact": "No se pudo agregar el contacto",
"remove_contact": "Quitar contacto",
"remove_contact_confirm": "¿Retirar este contacto?",
"remove_duplicates_confirm": "¿Quitar este contacto?\n\nTambién se quitarán {count} duplicado(s) llamado(s) \"{name}\".",
"contact_removed": "Contacto eliminado",
"failed_remove_contact": "No se pudo eliminar el contacto",
"share_contact": "Compartir contacto URI",

View file

@ -596,7 +596,9 @@
"loading": "Ladataan...",
"ok": "OK",
"clear": "Tyhjää",
"prompt_title": "Anna arvo"
"prompt_title": "Anna arvo",
"optional": "Valinnainen",
"back": "Takaisin"
},
"stickers": {
"settings_title": "Tarrat",
@ -1542,6 +1544,12 @@
"source_updated": "Kartan lähde päivitetty",
"failed_set_active": "Aktiivisen kartan asetus epäonnistui",
"file_deleted": "Tiedosto poistettu",
"delete_file_confirm": "Poistetaanko {name}?",
"clear_drawings_confirm": "Poistetaanko kaikki piirrokset kartalta?",
"delete_drawing_confirm": "Poistetaanko piirros \"{name}\"?",
"tracking_enabled": "Live-seuranta käytössä",
"tracking_disabled": "Live-seuranta pois käytöstä",
"failed_update_tracking": "Seurantatilan päivitys epäonnistui",
"failed_delete_file": "Tiedoston poisto epäonnistui",
"storage_saved": "Tallennushakemisto asetettu",
"failed_save_storage": "Hakemiston asetus epäonnistui",
@ -1872,6 +1880,11 @@
"marked_all_read": "Kaikki keskustelut merkitty luetuiksi",
"failed_mark_read": "Luetuksi merkitseminen epäonnistui",
"conversations_deleted": "Keskustelut poistettu",
"delete_conversations_confirm": "Poistetaanko nämä keskustelut? Kaikki viestit katoavat.",
"delete_conversations_title": "Poista keskustelut",
"delete_message_confirm": "Poistetaanko tämä viesti? Tätä ei voi perua.",
"retry_failed_confirm": "Lähetetäänkö uudelleen kaikki {count} epäonnistunutta tai peruttua viestiä?",
"send_oversized_confirm": "Tämä viesti on {size}. Vastaanottajat, joilla on oletusraja 900 kt, voivat hylätä sen. Lähetetäänkö silti?",
"failed_delete_conversations": "Keskustelujen poisto epäonnistui",
"failed_export_folders": "Kansioiden vienti epäonnistui",
"folders_imported": "Kansiot tuotu",
@ -2059,6 +2072,14 @@
"shortcut_saved": "Näppäinoikotie tallennettu",
"shortcut_deleted": "Näppäinoikotie poistettu",
"archived_pages_flushed": "Arkisto puhdistettu.",
"flush_archived_pages_confirm": "Poistetaanko kaikki arkistoidut sivut? Tätä ei voi perua.",
"outbound_bubble_color": "Lähtevän väri",
"failed_bubble_color": "Virheväri",
"waiting_bubble_color": "Odotusväri",
"inbound_bubble_color": "Saapuvan väri (valinnainen)",
"inbound_bubble_reset": "Palauta oletus",
"inbound_bubble_customize": "Muokkaa",
"inbound_bubble_default_hint": "Käytössä teeman oletus.",
"failed_enable_transport": "Välitystilan kytkeminen päälle epäonnistui!",
"failed_disable_transport": "Välitystilan kytkeminen pois epäonnistui!",
"failed_reload_reticulum": "Reticulumin lataus epäonnistui!",
@ -2488,7 +2509,10 @@
"hide_nodes": "Piilota solmut",
"show_snapshots": "Näytä tilannevedokset",
"hide_snapshots": "Piilota tilannevedokset",
"delete_snapshot": "Poista tilannevedos"
"delete_snapshot": "Poista tilannevedos",
"delete_selected_confirm": "Poistetaanko {count} valittua tilannevedosta?",
"delete_snapshot_confirm": "Poistetaanko tämä tilannevedos?",
"failed_delete": "Tilannevedoksen poisto epäonnistui. Yritä uudelleen."
},
"docs": {
"title": "Dokumentaatio",
@ -3883,6 +3907,25 @@
"failed_load_audio_edit": "Äänen lataaminen muokkausta varten epäonnistui",
"ringtone_saved": "Soittoääni tallennettu onnistuneesti",
"failed_save_ringtone": "Muokatun soittoäänen tallentaminen epäonnistui",
"lxst_disabled_title": "LXST on pois käytöstä",
"lxst_disabled_body": "Puhelut ovat pois käytöstä. Ota ne käyttöön soittaaksesi ja vastaanottaaksesi.",
"enable_lxst": "Ota LXST käyttöön",
"identity_or_name": "Identiteetin tiiviste tai nimi",
"search_recordings": "Hae nauhoituksia...",
"search_voicemails": "Hae vastaajaviestejä...",
"banish_identity_confirm": "Karkotetaanko tämä identiteetti?",
"delete_contact_confirm": "Poistetaanko tämä yhteystieto?",
"delete_greeting_confirm": "Poistetaanko mukautettu tervehdys?",
"delete_recording_confirm": "Poistetaanko tämä nauhoitus?",
"recording_enabled": "Puhelun nauhoitus käytössä",
"recording_disabled": "Puhelun nauhoitus pois käytöstä",
"failed_to_save_contact": "Yhteystiedon tallennus epäonnistui",
"failed_to_upload_greeting": "Tervehdyksen lataus epäonnistui",
"telephony_enabled": "Puhelut käytössä",
"telephony_disabled": "Puhelut pois käytöstä",
"no_contacts_hint": "Lisää yhteystietoja, jotta voit soittaa heille nopeasti.",
"custom": "Mukautettu",
"custom_ringtone_set": "Mukautettu soittoääni asetettu",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Salli mikrofonin käyttö, kun selain pyytää sitä, ja napsauta sitten Päivitä laitteet uudelleen.",
@ -3909,6 +3952,7 @@
"failed_add_contact": "Yhteyshenkilön lisääminen epäonnistui",
"remove_contact": "Poista yhteystieto",
"remove_contact_confirm": "Poistetaanko tämä yhteystieto?",
"remove_duplicates_confirm": "Poistetaanko tämä yhteystieto?\n\nMyös {count} muuta kaksoiskappaletta nimellä \"{name}\" poistetaan.",
"contact_removed": "Yhteystieto poistettu",
"failed_remove_contact": "Yhteystiedon poisto epäonnistui",
"share_contact": "Jaa yhteystiedon URI",

View file

@ -596,7 +596,9 @@
"loading": "Chargement...",
"ok": "Très bien.",
"clear": "Effacer",
"prompt_title": "Saisir une valeur"
"prompt_title": "Saisir une valeur",
"optional": "Facultatif",
"back": "Retour"
},
"stickers": {
"settings_title": "Autocollants",
@ -1542,6 +1544,12 @@
"source_updated": "Source de la carte mise à jour",
"failed_set_active": "Impossible de définir la carte active",
"file_deleted": "Fichier supprimé",
"delete_file_confirm": "Supprimer {name} ?",
"clear_drawings_confirm": "Effacer tous les dessins de la carte ?",
"delete_drawing_confirm": "Supprimer le dessin \"{name}\" ?",
"tracking_enabled": "Suivi en direct activé",
"tracking_disabled": "Suivi en direct désactivé",
"failed_update_tracking": "Impossible de mettre à jour le suivi",
"failed_delete_file": "Impossible de supprimer le fichier",
"storage_saved": "Répertoire de stockage enregistré",
"failed_save_storage": "Impossible d'enregistrer le répertoire",
@ -1872,6 +1880,11 @@
"marked_all_read": "Toutes les conversations marquées comme lues",
"failed_mark_read": "Échec du marquage comme suit:",
"conversations_deleted": "Conversations supprimées",
"delete_conversations_confirm": "Supprimer ces conversations ? Tous les messages seront perdus.",
"delete_conversations_title": "Supprimer les conversations",
"delete_message_confirm": "Supprimer ce message ? Cette action est irréversible.",
"retry_failed_confirm": "Renvoyer les {count} messages échoués ou annulés ?",
"send_oversized_confirm": "Ce message fait {size}. Les destinataires avec la limite par défaut de 900 Ko peuvent le refuser. Envoyer quand même ?",
"failed_delete_conversations": "Impossible de supprimer les conversations",
"failed_export_folders": "Impossible d'exporter les dossiers",
"folders_imported": "Dossiers importés",
@ -2037,6 +2050,14 @@
"shortcut_saved": "Raccourci enregistré",
"shortcut_deleted": "Raccourci supprimé",
"archived_pages_flushed": "Pages archivées bouffées.",
"flush_archived_pages_confirm": "Supprimer toutes les pages archivées ? Cette action est irréversible.",
"outbound_bubble_color": "Couleur sortante",
"failed_bubble_color": "Couleur d'échec",
"waiting_bubble_color": "Couleur d'attente",
"inbound_bubble_color": "Couleur entrante (facultatif)",
"inbound_bubble_reset": "Réinitialiser",
"inbound_bubble_customize": "Personnaliser",
"inbound_bubble_default_hint": "Couleur du thème par défaut.",
"failed_enable_transport": "Impossible d'activer le mode de transport !",
"failed_disable_transport": "Impossible de désactiver le mode de transport !",
"failed_reload_reticulum": "Impossible de recharger Reticulum !",
@ -2488,7 +2509,10 @@
"hide_nodes": "Masquer les nœuds",
"show_snapshots": "Afficher les instantanés",
"hide_snapshots": "Masquer les instantanés",
"delete_snapshot": "Supprimer l'instantané"
"delete_snapshot": "Supprimer l'instantané",
"delete_selected_confirm": "Supprimer {count} instantanés sélectionnés ?",
"delete_snapshot_confirm": "Supprimer cet instantané ?",
"failed_delete": "Impossible de supprimer l'instantané. Réessaie."
},
"docs": {
"title": "Documentation",
@ -3675,6 +3699,25 @@
"failed_load_audio_edit": "Impossible de charger l'audio pour l'édition",
"ringtone_saved": "Sonnerie enregistrée avec succès",
"failed_save_ringtone": "Impossible d'enregistrer la sonnerie éditée",
"lxst_disabled_title": "LXST est désactivé",
"lxst_disabled_body": "La téléphonie est désactivée. Active-la pour passer et recevoir des appels.",
"enable_lxst": "Activer LXST",
"identity_or_name": "Hash d'identité ou nom",
"search_recordings": "Rechercher des enregistrements...",
"search_voicemails": "Rechercher des messages vocaux...",
"banish_identity_confirm": "Bannir cette identité ?",
"delete_contact_confirm": "Supprimer ce contact ?",
"delete_greeting_confirm": "Supprimer ton message d'accueil personnalisé ?",
"delete_recording_confirm": "Supprimer cet enregistrement ?",
"recording_enabled": "Enregistrement des appels activé",
"recording_disabled": "Enregistrement des appels désactivé",
"failed_to_save_contact": "Impossible d'enregistrer le contact",
"failed_to_upload_greeting": "Impossible d'envoyer le message d'accueil",
"telephony_enabled": "Téléphonie activée",
"telephony_disabled": "Téléphonie désactivée",
"no_contacts_hint": "Ajoute des contacts pour les appeler rapidement.",
"custom": "Personnalisé",
"custom_ringtone_set": "Sonnerie personnalisée définie",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Autorisez l'accès au microphone lorsque le navigateur le demande, puis cliquez à nouveau sur Actualiser les périphériques.",
@ -3701,6 +3744,7 @@
"failed_add_contact": "Impossible d'ajouter le contact",
"remove_contact": "Supprimer le contact",
"remove_contact_confirm": "Enlever ce contact ?",
"remove_duplicates_confirm": "Retirer ce contact ?\n\n{count} doublon(s) nommé(s) \"{name}\" seront aussi retirés.",
"contact_removed": "Contact enlevé",
"failed_remove_contact": "Impossible de supprimer le contact",
"share_contact": "Partager Contacter URI",

View file

@ -596,7 +596,9 @@
"loading": "Caricamento...",
"ok": "OK",
"clear": "Cancella",
"prompt_title": "Inserisci un valore"
"prompt_title": "Inserisci un valore",
"optional": "Facoltativo",
"back": "Indietro"
},
"stickers": {
"settings_title": "Sticker",
@ -1095,6 +1097,7 @@
"failed_add_contact": "Impossibile aggiungere il contatto",
"remove_contact": "Rimuovi contatto",
"remove_contact_confirm": "Rimuovere questo contatto?",
"remove_duplicates_confirm": "Rimuovere questo contatto?\n\nVerranno rimossi anche {count} duplicat(i) di nome \"{name}\".",
"contact_removed": "Contatto rimosso",
"failed_remove_contact": "Impossibile rimuovere il contatto",
"share_contact": "Condividi URI contatto",
@ -1595,6 +1598,12 @@
"source_updated": "Sorgente mappa aggiornata",
"failed_set_active": "Impossibile impostare la mappa attiva",
"file_deleted": "File eliminato",
"delete_file_confirm": "Eliminare {name}?",
"clear_drawings_confirm": "Cancellare tutti i disegni dalla mappa?",
"delete_drawing_confirm": "Eliminare il disegno \"{name}\"?",
"tracking_enabled": "Tracciamento live attivato",
"tracking_disabled": "Tracciamento live disattivato",
"failed_update_tracking": "Impossibile aggiornare il tracciamento",
"failed_delete_file": "Impossibile eliminare il file",
"storage_saved": "Directory di archiviazione salvata",
"failed_save_storage": "Impossibile salvare la directory",
@ -1929,6 +1938,11 @@
"marked_all_read": "Tutte le conversazioni segnate come lette",
"failed_mark_read": "Impossibile segnare come letto",
"conversations_deleted": "Conversazioni eliminate",
"delete_conversations_confirm": "Eliminare queste conversazioni? Tutti i messaggi andranno persi.",
"delete_conversations_title": "Elimina conversazioni",
"delete_message_confirm": "Eliminare questo messaggio? L'azione non si può annullare.",
"retry_failed_confirm": "Reinviare tutti i {count} messaggi non riusciti o annullati?",
"send_oversized_confirm": "Questo messaggio è {size}. I destinatari con il limite predefinito di 900 KB potrebbero rifiutarlo. Inviare comunque?",
"failed_delete_conversations": "Impossibile eliminare le conversazioni",
"failed_export_folders": "Impossibile esportare le cartelle",
"folders_imported": "Cartelle importate",
@ -2090,6 +2104,14 @@
"shortcut_saved": "Scorciatoia salvata",
"shortcut_deleted": "Scorciatoia eliminata",
"archived_pages_flushed": "Pagine archiviate svuotate.",
"flush_archived_pages_confirm": "Eliminare tutte le pagine archiviate? L'azione non si può annullare.",
"outbound_bubble_color": "Colore in uscita",
"failed_bubble_color": "Colore errore",
"waiting_bubble_color": "Colore in attesa",
"inbound_bubble_color": "Colore in ingresso (facoltativo)",
"inbound_bubble_reset": "Ripristina predefinito",
"inbound_bubble_customize": "Personalizza",
"inbound_bubble_default_hint": "Si usa il colore del tema.",
"failed_enable_transport": "Impossibile abilitare la modalità trasporto!",
"failed_disable_transport": "Impossibile disabilitare la modalità trasporto!",
"failed_reload_reticulum": "Impossibile ricaricare Reticulum!",
@ -2541,7 +2563,10 @@
"hide_nodes": "Nascondi nodi",
"show_snapshots": "Mostra snapshot",
"hide_snapshots": "Nascondi snapshot",
"delete_snapshot": "Elimina snapshot"
"delete_snapshot": "Elimina snapshot",
"delete_selected_confirm": "Eliminare {count} snapshot selezionati?",
"delete_snapshot_confirm": "Eliminare questo snapshot?",
"failed_delete": "Impossibile eliminare lo snapshot. Riprova."
},
"docs": {
"title": "Documentazione",
@ -3728,6 +3753,25 @@
"failed_load_audio_edit": "Impossibile caricare l'audio per la modifica",
"ringtone_saved": "Suoneria salvata con successo",
"failed_save_ringtone": "Impossibile salvare la suoneria modificata",
"lxst_disabled_title": "LXST è disattivato",
"lxst_disabled_body": "La telefonia è disattivata. Attivala per fare e ricevere chiamate.",
"enable_lxst": "Attiva LXST",
"identity_or_name": "Hash identità o nome",
"search_recordings": "Cerca registrazioni...",
"search_voicemails": "Cerca messaggi in segreteria...",
"banish_identity_confirm": "Bandire questa identità?",
"delete_contact_confirm": "Eliminare questo contatto?",
"delete_greeting_confirm": "Eliminare il messaggio di benvenuto personalizzato?",
"delete_recording_confirm": "Eliminare questa registrazione?",
"recording_enabled": "Registrazione chiamate attivata",
"recording_disabled": "Registrazione chiamate disattivata",
"failed_to_save_contact": "Impossibile salvare il contatto",
"failed_to_upload_greeting": "Impossibile caricare il messaggio di benvenuto",
"telephony_enabled": "Telefonia attivata",
"telephony_disabled": "Telefonia disattivata",
"no_contacts_hint": "Aggiungi contatti per chiamarli in fretta.",
"custom": "Personalizzato",
"custom_ringtone_set": "Suoneria personalizzata impostata",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Consenti l'accesso al microfono quando il browser lo richiede, poi fai di nuovo clic su Aggiorna dispositivi.",

View file

@ -596,7 +596,9 @@
"loading": "Laden...",
"ok": "OK",
"clear": "Wissen",
"prompt_title": "Voer een waarde in"
"prompt_title": "Voer een waarde in",
"optional": "Optioneel",
"back": "Terug"
},
"stickers": {
"settings_title": "Stickers",
@ -1542,6 +1544,12 @@
"source_updated": "Kaartbron bijgewerkt",
"failed_set_active": "Kon actieve kaart niet instellen",
"file_deleted": "Bestand verwijderd",
"delete_file_confirm": "{name} verwijderen?",
"clear_drawings_confirm": "Alle tekeningen van de kaart wissen?",
"delete_drawing_confirm": "Tekening \"{name}\" verwijderen?",
"tracking_enabled": "Live tracking ingeschakeld",
"tracking_disabled": "Live tracking uitgeschakeld",
"failed_update_tracking": "Trackingstatus bijwerken mislukt",
"failed_delete_file": "Verwijderen van bestand mislukt",
"storage_saved": "Opslagmap opgeslagen",
"failed_save_storage": "Kon map niet opslaan",
@ -1872,6 +1880,11 @@
"marked_all_read": "Alle gesprekken gemarkeerd als gelezen",
"failed_mark_read": "Markeren als gelezen is mislukt",
"conversations_deleted": "Gesprekken verwijderd",
"delete_conversations_confirm": "Deze gesprekken verwijderen? Alle berichten gaan verloren.",
"delete_conversations_title": "Gesprekken verwijderen",
"delete_message_confirm": "Dit bericht verwijderen? Dit kan niet ongedaan worden gemaakt.",
"retry_failed_confirm": "Alle {count} mislukte of geannuleerde berichten opnieuw verzenden?",
"send_oversized_confirm": "Dit bericht is {size}. Ontvangers met de standaardlimiet van 900 KB kunnen het weigeren. Toch verzenden?",
"failed_delete_conversations": "Kon gesprekken niet verwijderen",
"failed_export_folders": "Exporteren van mappen is mislukt",
"folders_imported": "Mappen geïmporteerd",
@ -2037,6 +2050,14 @@
"shortcut_saved": "Sneltoets opgeslagen",
"shortcut_deleted": "Sneltoets verwijderd",
"archived_pages_flushed": "Gearchiveerde pagina's doorgespoeld.",
"flush_archived_pages_confirm": "Alle gearchiveerde pagina's verwijderen? Dit kan niet ongedaan worden gemaakt.",
"outbound_bubble_color": "Uitgaande kleur",
"failed_bubble_color": "Foutkleur",
"waiting_bubble_color": "Wachtkleur",
"inbound_bubble_color": "Inkomende kleur (optioneel)",
"inbound_bubble_reset": "Standaard herstellen",
"inbound_bubble_customize": "Aanpassen",
"inbound_bubble_default_hint": "Thema-standaardkleur wordt gebruikt.",
"failed_enable_transport": "Kon transportmodus niet inschakelen!",
"failed_disable_transport": "Kon transportmodus niet uitschakelen!",
"failed_reload_reticulum": "Herladen van Reticulum is mislukt!",
@ -2488,7 +2509,10 @@
"hide_nodes": "Nodes verbergen",
"show_snapshots": "Snapshots tonen",
"hide_snapshots": "Snapshots verbergen",
"delete_snapshot": "Snapshot verwijderen"
"delete_snapshot": "Snapshot verwijderen",
"delete_selected_confirm": "{count} geselecteerde snapshots verwijderen?",
"delete_snapshot_confirm": "Deze snapshot verwijderen?",
"failed_delete": "Snapshot verwijderen mislukt. Probeer het opnieuw."
},
"docs": {
"title": "Documentatie",
@ -3675,6 +3699,25 @@
"failed_load_audio_edit": "Kon audio niet laden om te bewerken",
"ringtone_saved": "Ringtone is succesvol opgeslagen",
"failed_save_ringtone": "Opslaan van bewerkte ringtone mislukt",
"lxst_disabled_title": "LXST is uitgeschakeld",
"lxst_disabled_body": "Telefonie is uitgeschakeld. Schakel het in om te bellen en gebeld te worden.",
"enable_lxst": "LXST inschakelen",
"identity_or_name": "Identiteitshash of naam",
"search_recordings": "Opnamen zoeken...",
"search_voicemails": "Voicemails zoeken...",
"banish_identity_confirm": "Deze identiteit verbannen?",
"delete_contact_confirm": "Dit contact verwijderen?",
"delete_greeting_confirm": "Je aangepaste begroeting verwijderen?",
"delete_recording_confirm": "Deze opname verwijderen?",
"recording_enabled": "Gespreksopname ingeschakeld",
"recording_disabled": "Gespreksopname uitgeschakeld",
"failed_to_save_contact": "Contact opslaan mislukt",
"failed_to_upload_greeting": "Begroeting uploaden mislukt",
"telephony_enabled": "Telefonie ingeschakeld",
"telephony_disabled": "Telefonie uitgeschakeld",
"no_contacts_hint": "Voeg contacten toe om ze snel te bellen.",
"custom": "Aangepast",
"custom_ringtone_set": "Aangepaste ringtone ingesteld",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Sta microfoontoegang toe wanneer de browser daarom vraagt en klik daarna opnieuw op Apparaten vernieuwen.",
@ -3701,6 +3744,7 @@
"failed_add_contact": "Kon contact niet toevoegen",
"remove_contact": "Contact verwijderen",
"remove_contact_confirm": "Dit contact verwijderen?",
"remove_duplicates_confirm": "Dit contact verwijderen?\n\nOok {count} extra duplicaat/duplicaten met de naam \"{name}\" worden verwijderd.",
"contact_removed": "Contact verwijderd",
"failed_remove_contact": "Kon contact niet verwijderen",
"share_contact": "Contact-URI delen",

View file

@ -596,7 +596,9 @@
"copy": "Копировать",
"copy_to_clipboard": "Копировать в буфер обмена",
"clear": "Очистить",
"prompt_title": "Введите значение"
"prompt_title": "Введите значение",
"optional": "Необязательно",
"back": "Назад"
},
"stickers": {
"settings_title": "Стикеры",
@ -1095,6 +1097,7 @@
"failed_add_contact": "Не удалось добавить контакт",
"remove_contact": "Удалить контакт",
"remove_contact_confirm": "Удалить этот контакт?",
"remove_duplicates_confirm": "Удалить этот контакт?\n\nТакже будут удалены {count} дубликат(ов) с именем \"{name}\".",
"contact_removed": "Контакт удален",
"failed_remove_contact": "Не удалось удалить контакт",
"share_contact": "Поделиться URI контакта",
@ -1595,6 +1598,12 @@
"source_updated": "Источник карты обновлён",
"failed_set_active": "Не удалось установить активную карту",
"file_deleted": "Файл удалён",
"delete_file_confirm": "Удалить {name}?",
"clear_drawings_confirm": "Удалить все рисунки с карты?",
"delete_drawing_confirm": "Удалить рисунок \"{name}\"?",
"tracking_enabled": "Живое отслеживание включено",
"tracking_disabled": "Живое отслеживание выключено",
"failed_update_tracking": "Не удалось обновить статус отслеживания",
"failed_delete_file": "Не удалось удалить файл",
"storage_saved": "Каталог хранения сохранён",
"failed_save_storage": "Не удалось сохранить каталог",
@ -1929,6 +1938,11 @@
"marked_all_read": "Все переписки отмечены как прочитанные",
"failed_mark_read": "Не удалось отметить как прочитанное",
"conversations_deleted": "Разговоры удалены",
"delete_conversations_confirm": "Удалить эти переписки? Все сообщения будут потеряны.",
"delete_conversations_title": "Удалить переписки",
"delete_message_confirm": "Удалить это сообщение? Это нельзя отменить.",
"retry_failed_confirm": "Повторить отправку всех {count} неудачных или отменённых сообщений?",
"send_oversized_confirm": "Размер сообщения {size}. Получатели с лимитом по умолчанию 900 КБ могут отклонить его. Отправить всё равно?",
"failed_delete_conversations": "Не удалось удалить разговоры",
"failed_export_folders": "Не удалось экспортировать папки",
"folders_imported": "Папки импортированы",
@ -2271,7 +2285,10 @@
"hide_nodes": "Скрыть узлы",
"show_snapshots": "Показать снимки",
"hide_snapshots": "Скрыть снимки",
"delete_snapshot": "Удалить снимок"
"delete_snapshot": "Удалить снимок",
"delete_selected_confirm": "Удалить {count} выбранных снимков?",
"delete_snapshot_confirm": "Удалить этот снимок?",
"failed_delete": "Не удалось удалить снимок. Попробуй ещё раз."
},
"docs": {
"title": "Документация",
@ -3458,6 +3475,25 @@
"failed_load_audio_edit": "Не удалось загрузить аудио для редактирования",
"ringtone_saved": "Рингтон успешно сохранён",
"failed_save_ringtone": "Не удалось сохранить отредактированный рингтон",
"lxst_disabled_title": "LXST выключен",
"lxst_disabled_body": "Телефония выключена. Включи её, чтобы звонить и принимать звонки.",
"enable_lxst": "Включить LXST",
"identity_or_name": "Хеш личности или имя",
"search_recordings": "Поиск записей...",
"search_voicemails": "Поиск голосовой почты...",
"banish_identity_confirm": "Изгнать эту личность?",
"delete_contact_confirm": "Удалить этот контакт?",
"delete_greeting_confirm": "Удалить своё приветствие?",
"delete_recording_confirm": "Удалить эту запись?",
"recording_enabled": "Запись звонков включена",
"recording_disabled": "Запись звонков выключена",
"failed_to_save_contact": "Не удалось сохранить контакт",
"failed_to_upload_greeting": "Не удалось загрузить приветствие",
"telephony_enabled": "Телефония включена",
"telephony_disabled": "Телефония выключена",
"no_contacts_hint": "Добавь контакты, чтобы быстро им звонить.",
"custom": "Свой",
"custom_ringtone_set": "Задан свой рингтон",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "Разрешите доступ к микрофону, когда браузер запросит его, затем снова нажмите Обновить устройства.",
@ -3703,6 +3739,14 @@
"shortcut_saved": "Ярлык сохранён",
"shortcut_deleted": "Ярлык удалён",
"archived_pages_flushed": "Архивные страницы очищены.",
"flush_archived_pages_confirm": "Удалить все архивные страницы? Это нельзя отменить.",
"outbound_bubble_color": "Цвет исходящих",
"failed_bubble_color": "Цвет ошибки",
"waiting_bubble_color": "Цвет ожидания",
"inbound_bubble_color": "Цвет входящих (необязательно)",
"inbound_bubble_reset": "Сбросить",
"inbound_bubble_customize": "Настроить",
"inbound_bubble_default_hint": "Используется цвет темы.",
"failed_enable_transport": "Не удалось включить режим транспорта!",
"failed_disable_transport": "Не удалось отключить транспортный режим!",
"failed_reload_reticulum": "Не удалось перезагрузить Reticulum!",

View file

@ -596,7 +596,9 @@
"loading": "正在加载...",
"ok": "确定",
"clear": "清除",
"prompt_title": "输入值"
"prompt_title": "输入值",
"optional": "可选",
"back": "返回"
},
"stickers": {
"settings_title": "贴纸",
@ -1542,6 +1544,12 @@
"source_updated": "地图源已更新",
"failed_set_active": "设置活动地图失败",
"file_deleted": "文件已删除",
"delete_file_confirm": "删除 {name}",
"clear_drawings_confirm": "清除地图上的所有绘图?",
"delete_drawing_confirm": "删除绘图 \"{name}\"",
"tracking_enabled": "已启用实时跟踪",
"tracking_disabled": "已关闭实时跟踪",
"failed_update_tracking": "无法更新跟踪状态",
"failed_delete_file": "删除文件失败",
"storage_saved": "存储目录已保存",
"failed_save_storage": "保存目录失败",
@ -1872,6 +1880,11 @@
"marked_all_read": "已将全部对话标记为已读",
"failed_mark_read": "标记为已读失败",
"conversations_deleted": "对话已删除",
"delete_conversations_confirm": "删除这些会话?所有消息都会丢失。",
"delete_conversations_title": "删除会话",
"delete_message_confirm": "删除这条消息?此操作无法撤销。",
"retry_failed_confirm": "重新发送全部 {count} 条失败或已取消的消息?",
"send_oversized_confirm": "这条消息大小为 {size}。使用默认 900 KB 投递上限的对端可能会拒绝。仍要发送吗?",
"failed_delete_conversations": "删除对话失败",
"failed_export_folders": "导出文件夹失败",
"folders_imported": "文件夹已导入",
@ -2037,6 +2050,14 @@
"shortcut_saved": "快捷键已保存",
"shortcut_deleted": "快捷键已删除",
"archived_pages_flushed": "已冲出存档页面 。",
"flush_archived_pages_confirm": "删除所有已归档页面?此操作无法撤销。",
"outbound_bubble_color": "发出气泡颜色",
"failed_bubble_color": "失败气泡颜色",
"waiting_bubble_color": "等待气泡颜色",
"inbound_bubble_color": "收到气泡颜色(可选)",
"inbound_bubble_reset": "恢复默认",
"inbound_bubble_customize": "自定义",
"inbound_bubble_default_hint": "使用主题默认颜色。",
"failed_enable_transport": "启用传输模式失败 !",
"failed_disable_transport": "禁用运输模式失败 !",
"failed_reload_reticulum": "重新装入 Reticulum 失败 !",
@ -2488,7 +2509,10 @@
"hide_nodes": "隐藏节点",
"show_snapshots": "显示快照",
"hide_snapshots": "隐藏快照",
"delete_snapshot": "删除快照"
"delete_snapshot": "删除快照",
"delete_selected_confirm": "删除所选的 {count} 个快照?",
"delete_snapshot_confirm": "删除此快照?",
"failed_delete": "删除快照失败。请重试。"
},
"docs": {
"title": "文档",
@ -3675,6 +3699,25 @@
"failed_load_audio_edit": "加载用于编辑的音频失败",
"ringtone_saved": "铃声保存成功",
"failed_save_ringtone": "保存编辑的铃声失败",
"lxst_disabled_title": "LXST 已关闭",
"lxst_disabled_body": "电话功能当前已关闭。启用后才能拨打和接听。",
"enable_lxst": "启用 LXST",
"identity_or_name": "身份哈希或名称",
"search_recordings": "搜索录音...",
"search_voicemails": "搜索语音留言...",
"banish_identity_confirm": "放逐此身份?",
"delete_contact_confirm": "删除此联系人?",
"delete_greeting_confirm": "删除自定义问候语?",
"delete_recording_confirm": "删除此录音?",
"recording_enabled": "已启用通话录音",
"recording_disabled": "已关闭通话录音",
"failed_to_save_contact": "保存联系人失败",
"failed_to_upload_greeting": "上传问候语失败",
"telephony_enabled": "已启用电话",
"telephony_disabled": "已关闭电话",
"no_contacts_hint": "添加联系人以便快速呼叫。",
"custom": "自定义",
"custom_ringtone_set": "已设置自定义铃声",
"codec2_unavailable": "Codec2 is not available on this device. Low-bandwidth call profiles are hidden.",
"codec2_profile_remapped": "Codec2 is unavailable so the call profile was switched to Opus.",
"microphone_permission_needed": "请在浏览器提示时允许麦克风访问,然后再次点击刷新设备。",
@ -3701,6 +3744,7 @@
"failed_add_contact": "添加联系人失败",
"remove_contact": "移除联系人",
"remove_contact_confirm": "移除联系人?",
"remove_duplicates_confirm": "移除此联系人?\n\n另外 {count} 个名为 \"{name}\" 的重复项也会被移除。",
"contact_removed": "联系人已移除",
"failed_remove_contact": "移除联系人失败",
"share_contact": "共享联系人 URI",

View file

@ -42,7 +42,7 @@ describe("ConfirmDialog UI", () => {
expect(showFn).toBeDefined();
showFn({ message: "Delete this item?", resolve: vi.fn() });
await wrapper.vm.$nextTick();
expect(wrapper.vm.pendingConfirm).toEqual({ message: "Delete this item?" });
expect(wrapper.vm.pendingConfirm).toEqual({ message: "Delete this item?", title: "" });
expect(wrapper.text()).toContain("common.confirm_action");
expect(wrapper.text()).toContain("Delete this item?");
});
@ -89,7 +89,35 @@ describe("ConfirmDialog UI", () => {
showFn({ message: "Second?", resolve: second });
await wrapper.vm.$nextTick();
expect(first).toHaveBeenCalledWith(false);
expect(wrapper.vm.pendingConfirm).toEqual({ message: "Second?" });
expect(wrapper.vm.pendingConfirm).toEqual({ message: "Second?", title: "" });
expect(second).not.toHaveBeenCalled();
});
it("shows an optional title when provided", async () => {
const wrapper = mountDialog();
const showFn = GlobalEmitter.on.mock.calls.find((c) => c[0] === "confirm")?.[1];
showFn({ message: "All messages will be lost.", title: "Delete conversations", resolve: vi.fn() });
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("Delete conversations");
expect(wrapper.text()).not.toContain("common.confirm_action");
wrapper.unmount();
});
it("cancels on Escape and confirms on Enter", async () => {
const resolve = vi.fn();
const wrapper = mountDialog();
const showFn = GlobalEmitter.on.mock.calls.find((c) => c[0] === "confirm")?.[1];
showFn({ message: "Sure?", resolve });
await wrapper.vm.$nextTick();
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
expect(resolve).toHaveBeenCalledWith(false);
expect(wrapper.vm.pendingConfirm).toBeNull();
const resolveEnter = vi.fn();
showFn({ message: "Sure again?", resolve: resolveEnter });
await wrapper.vm.$nextTick();
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
expect(resolveEnter).toHaveBeenCalledWith(true);
wrapper.unmount();
});
});

View file

@ -7,6 +7,53 @@ vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
import DialogUtils from "../../meshchatx/src/frontend/js/DialogUtils.js";
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
describe("DialogUtils.confirm", () => {
beforeEach(() => {
vi.mocked(GlobalEmitter.emit).mockClear();
delete window.electron;
});
it("uses the in-app confirm dialog even when electron is present", async () => {
window.electron = {
confirm: vi.fn().mockResolvedValue(true),
};
const pending = DialogUtils.confirm("Delete this?");
expect(window.electron.confirm).not.toHaveBeenCalled();
expect(GlobalEmitter.emit).toHaveBeenCalledWith(
"confirm",
expect.objectContaining({
message: "Delete this?",
resolve: expect.any(Function),
})
);
const payload = GlobalEmitter.emit.mock.calls.find((c) => c[0] === "confirm")[1];
payload.resolve(false);
await expect(pending).resolves.toBe(false);
});
it("passes an optional title to the in-app dialog", async () => {
const pending = DialogUtils.confirm("All messages will be lost.", "Delete conversations");
const payload = GlobalEmitter.emit.mock.calls.find((c) => c[0] === "confirm")[1];
expect(payload.title).toBe("Delete conversations");
payload.resolve(true);
await expect(pending).resolves.toBe(true);
});
it("aliases confirmCustom to the same in-app dialog", async () => {
const pending = DialogUtils.confirmCustom("Leave room?");
expect(GlobalEmitter.emit).toHaveBeenCalledWith(
"confirm",
expect.objectContaining({
message: "Leave room?",
resolve: expect.any(Function),
})
);
const payload = GlobalEmitter.emit.mock.calls.find((c) => c[0] === "confirm")[1];
payload.resolve(true);
await expect(pending).resolves.toBe(true);
});
});
describe("DialogUtils.prompt", () => {
beforeEach(() => {
vi.mocked(GlobalEmitter.emit).mockClear();

View file

@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UI snapshot regression > ConfirmDialog.vue > visible confirm dialog 1`] = `"<div="" class="fixed inset-0 z-9999 flex items-center justify-center p-4 confirm-dialog-enter-from confirm-dialog-enter-active"><div="" class="fixed inset-0 bg-black/50 backdrop-blur-xs shadow-2xl"></div><div="" class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"><div="" class="p-8"><div="" class="flex items-start mb-6"><div="" class="shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"><span="" class="mdi-snapshot w-6 h-6" data-icon="alert-circle"></span></div><div="" class="flex-1 min-w-0"><h3="" class="text-xl font-black text-gray-900 dark:text-white mb-2">common.confirm_action</h3><p="" class="text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">Delete this item?</p></div></div><div="" class="flex flex-col sm:flex-row gap-3 sm:justify-end mt-8"><button="" type="button" class="px-6 py-3 text-sm font-bold text-gray-700 dark:text-zinc-300 bg-gray-100 dark:bg-zinc-800 rounded-xl hover:bg-gray-200 dark:hover:bg-zinc-700 transition-all active:scale-95">common.cancel</button><button="" type="button" class="px-6 py-3 text-sm font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl shadow-lg shadow-red-600/20 transition-all active:scale-95">common.confirm</button></div></div></div></div>"`;
exports[`UI snapshot regression > ConfirmDialog.vue > visible confirm dialog 1`] = `"<div="" class="fixed inset-0 z-9999 flex items-center justify-center p-4 confirm-dialog-enter-from confirm-dialog-enter-active" role="alertdialog" aria-modal="true" aria-labelledby="confirm-dialog-title" aria-describedby="confirm-dialog-message"><div="" class="fixed inset-0 bg-black/50 backdrop-blur-xs shadow-2xl"></div><div="" class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all" tabindex="-1"><div="" class="p-8"><div="" class="flex items-start mb-6"><div="" class="shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"><span="" class="mdi-snapshot w-6 h-6" data-icon="alert-circle"></span></div><div="" class="flex-1 min-w-0"><h3="" class="text-xl font-black text-gray-900 dark:text-white mb-2">common.confirm_action</h3><p="" class="text-gray-600 dark:text-zinc-300 whitespace-pre-wrap leading-relaxed">Delete this item?</p></div></div><div="" class="flex flex-col sm:flex-row gap-3 sm:justify-end mt-8"><button="" type="button" class="px-6 py-3 text-sm font-bold text-gray-700 dark:text-zinc-300 bg-gray-100 dark:bg-zinc-800 rounded-xl hover:bg-gray-200 dark:hover:bg-zinc-700 transition-all active:scale-95">common.cancel</button><button="" type="button" class="px-6 py-3 text-sm font-bold text-white bg-red-600 hover:bg-red-700 rounded-xl shadow-lg shadow-red-600/20 transition-all active:scale-95">common.confirm</button></div></div></div></div>"`;
exports[`UI snapshot regression > FormLabel.vue > default label 1`] = `"<label class="block text-sm font-medium text-gray-900 dark:text-zinc-100">Display name</label>"`;