feat: Ability to add/remove tags from any list of messages #189

This commit is contained in:
rustmailer
2026-03-26 21:22:21 +08:00
parent 5d0039cb74
commit 3f11c5dbbf
30 changed files with 805 additions and 66 deletions
+24 -12
View File
@@ -40,7 +40,7 @@ use crate::{
attachment::AttachmentMetadata,
content::{AttachmentDetail, AttachmentInfo},
search::{SearchFilter, SortBy},
tags::TagCount,
tags::{TagAction, TagCount, TagsRequest},
},
rest::response::DataPage,
settings::{cli::SETTINGS, dir::DATA_DIR_MANAGER},
@@ -611,20 +611,24 @@ impl DuckDBManager {
Ok(contacts)
}
pub fn update_envelope_tags(
&self,
updates: HashMap<u64, Vec<String>>,
tags: Vec<String>,
) -> BichonResult<()> {
pub fn update_envelope_tags(&self, request: TagsRequest) -> BichonResult<()> {
let mut conn = self.conn()?;
let tags_json = serde_json::to_string(&tags)
let tags_json = serde_json::to_string(&request.tags)
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
let set_clause = match &request.action {
TagAction::Overwrite => "SET tags = ?::JSON::VARCHAR[]",
TagAction::Add => "SET tags = list_distinct(list_concat(tags, ?::JSON::VARCHAR[]))",
TagAction::Remove => {
"SET tags = list_filter(tags, x -> NOT list_contains(?::JSON::VARCHAR[], x))"
}
};
let tx = conn
.transaction()
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
for (account_id, ids) in updates {
for (account_id, ids) in request.updates {
if ids.is_empty() {
continue;
}
@@ -633,23 +637,31 @@ impl DuckDBManager {
let placeholders = vec!["?"; chunk.len()].join(", ");
let query = format!(
"UPDATE envelopes
SET tags = CAST(json(?) AS VARCHAR[])
{}
WHERE account_id = ?
AND id IN ({})",
placeholders
set_clause, placeholders
);
let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::new();
let mut params: Vec<Box<dyn duckdb::ToSql>> = Vec::with_capacity(chunk.len() + 2);
params.push(Box::new(tags_json.clone()));
params.push(Box::new(account_id));
for id in chunk {
params.push(Box::new(id.clone()));
}
let param_refs: Vec<&dyn duckdb::ToSql> =
params.iter().map(|p| p.as_ref()).collect();
tx.execute(&query, duckdb::params_from_iter(param_refs))
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
.map_err(|e| {
raise_error!(
format!("Update failed. Account: {}, Error: {}", account_id, e),
ErrorCode::InternalError
)
})?;
}
}
+5 -9
View File
@@ -28,7 +28,7 @@ use crate::modules::{
attachment::AttachmentMetadata,
content::{AttachmentDetail, AttachmentInfo},
search::SortBy,
tags::TagCount,
tags::{TagCount, TagsRequest},
},
};
use crate::{
@@ -204,16 +204,12 @@ impl EnvelopeIndexManager {
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
}
pub async fn update_envelope_tags(
&self,
updates: HashMap<u64, Vec<String>>, // HashMap<account_id, envelope_ids>
tags: Vec<String>,
) -> BichonResult<()> {
if updates.is_empty() {
tracing::warn!("update_envelope_tags: updates is empty, nothing to update");
pub async fn update_envelope_tags(&self, request: TagsRequest) -> BichonResult<()> {
if request.updates.is_empty() {
tracing::warn!("update_envelope_tags: request is empty, nothing to update");
return Ok(());
}
tokio::task::spawn_blocking(move || duckdb()?.update_envelope_tags(updates, tags))
tokio::task::spawn_blocking(move || duckdb()?.update_envelope_tags(request))
.await
.map_err(|e| raise_error!(format!("{:?}", e), ErrorCode::InternalError))?
}
+11 -3
View File
@@ -16,16 +16,16 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use std::collections::HashMap;
use poem_openapi::Object;
use poem_openapi::{Enum, Object};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
pub struct UpdateTagsRequest {
pub struct TagsRequest {
pub updates: HashMap<u64, Vec<String>>, // account_id -> envelope_ids
pub tags: Vec<String>,
pub action: TagAction,
}
#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)]
@@ -33,3 +33,11 @@ pub struct TagCount {
pub tag: String,
pub count: u64,
}
#[derive(Enum, Default, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TagAction {
Add,
Remove,
#[default]
Overwrite,
}
+3 -3
View File
@@ -33,7 +33,7 @@ use crate::modules::message::delete::delete_messages_impl;
use crate::modules::message::list::{get_thread_messages, list_messages_impl};
use crate::modules::message::search::{search_messages_impl, SearchRequest};
use crate::modules::message::tags::TagCount;
use crate::modules::message::tags::UpdateTagsRequest;
use crate::modules::message::tags::TagsRequest;
use crate::modules::rest::api::ApiTags;
use crate::modules::rest::response::DataPage;
use crate::modules::rest::ApiResult;
@@ -376,7 +376,7 @@ impl MessageApi {
)]
async fn update_envelope_tags(
&self,
req: Json<UpdateTagsRequest>,
req: Json<TagsRequest>,
context: ClientContext,
) -> ApiResult<()> {
let req = req.0;
@@ -392,7 +392,7 @@ impl MessageApi {
}
ENVELOPE_INDEX_MANAGER
.update_envelope_tags(req.updates, req.tags)
.update_envelope_tags(req)
.await?;
Ok(())
}
+22 -2
View File
@@ -19,7 +19,7 @@
import { useRef } from 'react'
import { X, Trash2, Upload } from 'lucide-react'
import { X, Trash2, Upload, TagIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -66,6 +66,11 @@ export function MailBulkActions({ children }: MailBulkActionsProps) {
setOpen('restore')
}
const handleUpdateTags = () => {
setOpen('update-tags')
}
const handleKeyDown = (e: React.KeyboardEvent) => {
const buttons = toolbarRef.current?.querySelectorAll('button')
if (!buttons || buttons.length === 0) return
@@ -174,7 +179,22 @@ export function MailBulkActions({ children }: MailBulkActionsProps) {
{t('restore_message.restore_to_imap', 'Restore Mail')}
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-5" />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
size="sm"
onClick={handleUpdateTags}
className="gap-1"
>
<TagIcon className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
{t('search.bulkActions.manageTags')}
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-5" />
<Tooltip>
<TooltipTrigger asChild>
@@ -0,0 +1,259 @@
//
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
//
// This file is part of the Bichon Email Archiving Project
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Plus, Tag as TagIcon, X, Loader2, Check, AlertTriangle } from 'lucide-react';
import { useState } from 'react';
import { useAvailableTags } from '@/hooks/use-available-tags';
import { TagAction, useUpdateTags } from '@/hooks/use-update-tags';
import { toast } from '@/hooks/use-toast';
import { validateTag } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
import { useSearchContext } from './context';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
}
export function UpdateTagsDialog({ open, onOpenChange }: Props) {
const { tags: availableTags } = useAvailableTags();
const queryClient = useQueryClient();
const { mutate, isPending } = useUpdateTags();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [commandOpen, setCommandOpen] = useState(false);
const [action, setAction] = useState<TagAction>('Overwrite');
const { t } = useTranslation();
const { selected } = useSearchContext()
const handleAddTag = (tag: string) => {
const normalized = tag.toLowerCase().trim();
const result = validateTag(normalized);
if (!result.valid) {
toast({
title: t('search.updateTags.invalidTitle'),
description: result.error,
variant: 'destructive',
});
return;
}
if (normalized && !selectedTags.includes(normalized)) {
setSelectedTags(prev => [...prev, normalized]);
}
setInputValue('');
setCommandOpen(false);
};
const handleRemoveTag = (tag: string) => {
setSelectedTags(prev => prev.filter(t => t !== tag));
};
const handleSubmit = () => {
if (inputValue.trim()) {
const normalized = inputValue.toLowerCase().trim();
const result = validateTag(normalized);
if (!result.valid) {
toast({
title: t('search.updateTags.invalidTitle'),
description: result.error,
variant: 'destructive',
});
return;
}
if (!selectedTags.includes(normalized)) {
setSelectedTags(prev => [...prev, normalized]);
}
setInputValue('');
}
const updates: Record<number, string[]> = {};
selected.forEach((tagSet, accountId) => {
updates[accountId] = Array.from(tagSet);
});
let finalTags = inputValue.trim()
? [...selectedTags, inputValue.toLowerCase().trim()]
: selectedTags;
if (finalTags.length === 0 && action !== 'Overwrite') {
return;
}
mutate(
{
updates,
tags: finalTags,
action
},
{
onSuccess: () => {
toast({
title: t('search.updateTags.updatedTitle'),
description: (
<div className="flex items-center gap-2">
<Check className="h-4 w-4 text-green-500" />
<span>{t('search.updateTags.updatedDesc')}</span>
</div>
),
});
queryClient.invalidateQueries({ queryKey: ['all-tags'] });
onOpenChange(false);
},
onError: (error: any) => {
toast({
title: t('search.updateTags.updateFailedTitle'),
description: error?.message || t('search.updateTags.tryAgain'),
variant: 'destructive',
});
},
}
);
};
const filteredSuggestions = availableTags.filter(
tag => !selectedTags.includes(tag) && tag.includes(inputValue.toLowerCase())
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md min-h-[50vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TagIcon className="h-5 w-5" />
{t('search.updateTags.title')}
</DialogTitle>
</DialogHeader>
<Tabs value={action} onValueChange={(v) => setAction(v as TagAction)} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="Add" className="text-xs">{t('search.updateTags.actionAdd', "Add")}</TabsTrigger>
<TabsTrigger value="Remove" className="text-xs">{t('search.updateTags.actionRemove', "remove")}</TabsTrigger>
<TabsTrigger value="Overwrite" className="text-xs">{t('search.updateTags.actionOverwrite', "overwrite")}</TabsTrigger>
</TabsList>
</Tabs>
<div className="space-y-5 py-4">
<div className="flex flex-wrap gap-2">
{selectedTags.length === 0 ? (
<p className="text-sm text-muted-foreground">{t('search.updateTags.none')}</p>
) : (
selectedTags.map(tag => (
<Badge key={tag} variant="secondary" className="gap-1 pr-1 h-7">
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="rounded-sm hover:bg-destructive/20 hover:text-destructive transition-colors"
>
<X className="h-3 w-3" />
</button>
</Badge>
))
)}
</div>
<Command shouldFilter={false} onKeyDown={(e) => e.stopPropagation()} >
<div className="space-y-2">
<div className="relative">
<CommandInput
placeholder={t('search.updateTags.searchPlaceholder')}
value={inputValue}
onValueChange={setInputValue}
onFocus={() => setCommandOpen(true)}
className="h-9 pr-10"
onKeyDown={(e) => {
if (e.key === 'Enter' && inputValue.trim()) {
e.preventDefault();
e.stopPropagation();
handleAddTag(inputValue);
}
}}
/>
{inputValue.trim() && (
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1 h-7 w-7 p-0"
onClick={() => handleAddTag(inputValue)}
>
<Plus className="h-3.5 w-3.5" />
</Button>
)}
</div>
{inputValue.trim() && filteredSuggestions.length === 0 && (
<div className="px-1 text-xs text-muted-foreground animate-in fade-in duration-200">
{t('search.updateTags.createHint', { tag: inputValue })}
</div>
)}
{commandOpen && inputValue && filteredSuggestions.length > 0 && (
<CommandList className="max-h-64 overflow-auto rounded-md border bg-popover shadow-md">
<CommandGroup>
{filteredSuggestions.map(tag => (
<CommandItem
key={tag}
onSelect={() => handleAddTag(tag)}
className="cursor-pointer"
>
<Check className="mr-2 h-4 w-4 opacity-0" />
{tag}
</CommandItem>
))}
</CommandGroup>
</CommandList>
)}
</div>
</Command>
</div>
<div className="flex justify-between items-center">
<p className="text-xs text-muted-foreground">
{t('search.updateTags.selectedCount', { count: selectedTags.length })}
</p>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
{t('search.addTags.cancel')}
</Button>
<Button onClick={handleSubmit} disabled={isPending} variant={action === 'Remove' ? 'destructive' : 'default'}>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t(`search.updateTags.saving${action}`)}
</>
) : (
t(`search.updateTags.submit${action}`)
)}
</Button>
</div>
</div>
{action == "Overwrite" && <div className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-500">
<AlertTriangle className="h-5 w-5 shrink-0" />
<p className="text-xs leading-relaxed">
{t('search.updateTags.overwriteWarning')}
</p>
</div>}
</DialogContent>
</Dialog>
);
}
+1 -1
View File
@@ -21,7 +21,7 @@ import React from 'react'
import { EmailEnvelope } from '@/api'
import { SortingState } from '@tanstack/react-table'
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'restore' | 'delete-mailbox'
export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'update-tags' | 'restore' | 'delete-mailbox'
interface SearchContextType {
open: SearchDialogType | null
@@ -107,6 +107,7 @@ export function EditTagsDialog({ open, onOpenChange }: Props) {
tags: inputValue.trim()
? [...selectedTags, inputValue.toLowerCase().trim()]
: selectedTags,
action: "Overwrite"
},
{
onSuccess: () => {
+8 -1
View File
@@ -28,12 +28,13 @@ import { MailDisplayDrawer } from './mail-display-dialog';
import { EnvelopeDeleteDialog } from './delete-dialog';
import SearchProvider, { SearchDialogType } from './context';
import useDialogState from '@/hooks/use-dialog-state';
import { EditTagsDialog } from './add-tag-dialog';
import { EditTagsDialog } from './edit-tag-dialog';
import { useTranslation } from 'react-i18next';
import { RestoreMessageDialog } from './restore-message-dialog';
import { MailListTable } from './mail-list-table';
import { SortingState } from '@tanstack/react-table';
import { MailBoxDeleteDialog } from './delete-mailbox-dialog';
import { UpdateTagsDialog } from './bulk-add-tag-dialog';
export default function Search() {
const { t } = useTranslation()
@@ -154,6 +155,12 @@ export default function Search() {
onOpenChange={() => setOpen('edit-tags')}
/>
<UpdateTagsDialog
key='edit-tags-dialog'
open={open === 'update-tags'}
onOpenChange={() => setOpen('update-tags')}
/>
<RestoreMessageDialog
key='restore-mail-dialog'
open={open === 'restore'}
+16 -10
View File
@@ -146,12 +146,18 @@ export function MailListTable({
<span className="text-[11px] truncate font-medium leading-none">
{mailbox_name}
</span>
{safeTags.length > 0 && (
<span className="text-[9px] text-primary/70 truncate leading-none mt-0.5">
{visibleTags.join(' · ')}
{safeTags.length > 2 && ` · +${safeTags.length - 2}`}
</span>
)}
<div className="flex flex-wrap gap-1 mt-1">
{visibleTags.map((tag, i) => (
<span key={i} className="px-1.5 py-0.5 rounded-sm bg-primary/10 text-primary text-[9px] font-medium leading-none border border-primary/20 whitespace-nowrap">
{tag}
</span>
))}
{safeTags.length > 2 && (
<span className="px-1.5 py-0.5 rounded-sm bg-gray-100 text-gray-500 text-[9px] font-medium leading-none border border-gray-200">
+{safeTags.length - 2}
</span>
)}
</div>
</div>
</div>
);
@@ -242,7 +248,7 @@ export function MailListTable({
header: t('search.subject'),
cell: ({ row }) => <LongText className='text-xs'>{row.original.subject}</LongText>,
meta: { className: 'text-left text-xs' },
maxSize: 600,
maxSize: 450,
},
{
id: "text_preview",
@@ -317,14 +323,14 @@ export function MailListTable({
)
},
meta: { className: 'text-left text-xs' },
minSize: 100,
maxSize: 100,
minSize: 130,
maxSize: 130,
},
{
id: 'actions',
header: t('users.columns.actions'),
cell: DataTableRowActions,
meta: { className: 'text-left text-xs' },
meta: { className: 'text-right text-xs' },
minSize: 50,
maxSize: 60,
},
+1 -1
View File
@@ -98,7 +98,7 @@ export function SearchTable({ columns, data, onRowClick, setSortBy, setSortOrder
return (
<div className="flex flex-1 flex-col gap-0.5">
{children && (<>{children(table)}</>)}
<ScrollArea className='h-[calc(100vh-15rem)] rounded-md border' orientation='both'>
<ScrollArea className='h-[calc(100vh-13rem)] rounded-md border' orientation='both'>
<ShadcnTable>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
+4 -6
View File
@@ -21,9 +21,12 @@ import { update_tags } from '@/api/search/api';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from './use-toast';
export type TagAction = 'Add' | 'Remove' | 'Overwrite';
export interface UpdateTagsParams {
updates: Record<number, string[]>;
tags: string[];
action: TagAction;
}
export function useUpdateTags() {
@@ -31,12 +34,7 @@ export function useUpdateTags() {
return useMutation({
mutationFn: async (params: UpdateTagsParams) => {
const { updates, tags } = params;
const payload = {
updates,
tags
};
return update_tags(payload);
return update_tags(params);
},
onSuccess: () => {
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "حفظ",
"saving": "جارٍ الحفظ..."
},
"updateTags": {
"overwriteWarning": "تحذير: سيؤدي هذا إلى الكتابة فوق جميع العلامات الموجودة في العناصر المختارة.",
"title": "تحديث العلامات",
"none": "لم يتم اختيار أي علامات",
"searchPlaceholder": "ابحث أو أنشئ علامة جديدة...",
"createHint": "اضغط Enter لإنشاء علامة جديدة: \"{{tag}}\"",
"selectedCount": "سيتم تطبيق {{count}} من العلامات",
"cancel": "إلغاء",
"invalidTitle": "علامة غير صالحة",
"updatedTitle": "تم تحديث العلامات",
"updatedDesc": "تم تحديث العلامات بنجاح.",
"updateFailedTitle": "فشل التحديث",
"tryAgain": "يرجى المحاولة مرة أخرى لاحقاً.",
"actionAdd": "إضافة",
"actionRemove": "إزالة",
"actionOverwrite": "استبدال",
"submitAdd": "إضافة علامات",
"submitRemove": "إزالة علامات",
"submitOverwrite": "استبدال العلامات",
"savingAdd": "جاري الإضافة...",
"savingRemove": "جاري الإزالة...",
"savingOverwrite": "جاري الاستبدال..."
},
"thread": {
"title": "المحادثة · {{count}} رسالة",
"error": "فشل تحميل المحادثة",
@@ -496,7 +519,8 @@
"delete": "حذف",
"deleteDesc": "حذف الرسائل المحددة",
"selected": "تم تحديد {{count}}",
"clear": "مسح التحديد"
"clear": "مسح التحديد",
"manageTags": "إدارة العلامات"
},
"delete": {
"title": "حذف الرسائل",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Gem",
"saving": "Gemmer..."
},
"updateTags": {
"overwriteWarning": "Advarsel: Dette vil overskrive alle eksisterende tags på de valgte elementer.",
"title": "Opdater tags",
"none": "Ingen tags valgt",
"searchPlaceholder": "Søg eller opret nyt tag...",
"createHint": "Tryk på Enter for at oprette nyt tag: \"{{tag}}\"",
"selectedCount": "{{count}} tag(s) vil blive anvendt",
"cancel": "Annuller",
"invalidTitle": "Ugyldigt tag",
"updatedTitle": "Tags opdateret",
"updatedDesc": "Tags er blevet opdateret korrekt.",
"updateFailedTitle": "Opdatering fejlede",
"tryAgain": "Prøv venligst igen senere.",
"actionAdd": "Tilføj",
"actionRemove": "Fjern",
"actionOverwrite": "Overskriv",
"submitAdd": "Tilføj tags",
"submitRemove": "Fjern tags",
"submitOverwrite": "Overskriv tags",
"savingAdd": "Tilføjer...",
"savingRemove": "Fjerner...",
"savingOverwrite": "Overskriver..."
},
"thread": {
"title": "Tråd · {{count}} meddelelser",
"error": "Kunne ikke indlæse tråd",
@@ -496,7 +519,8 @@
"delete": "Slet",
"deleteDesc": "Slet valgte meddelelser",
"selected": "{{count}} valgt",
"clear": "Ryd valg"
"clear": "Ryd valg",
"manageTags": "Administrer tags"
},
"delete": {
"title": "Slet meddelelser",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Speichern",
"saving": "Wird gespeichert..."
},
"updateTags": {
"overwriteWarning": "Warnung: Dies überschreibt alle vorhandenen Tags der ausgewählten Elemente.",
"title": "Tags aktualisieren",
"none": "Keine Tags ausgewählt",
"searchPlaceholder": "Tag suchen oder erstellen...",
"createHint": "Eingabetaste drücken, um neuen Tag zu erstellen: \"{{tag}}\"",
"selectedCount": "{{count}} Tag(s) werden angewendet",
"cancel": "Abbrechen",
"invalidTitle": "Ungültiger Tag",
"updatedTitle": "Tags aktualisiert",
"updatedDesc": "Tags wurden erfolgreich aktualisiert.",
"updateFailedTitle": "Aktualisierung fehlgeschlagen",
"tryAgain": "Bitte versuchen Sie es später erneut.",
"actionAdd": "Hinzufügen",
"actionRemove": "Entfernen",
"actionOverwrite": "Überschreiben",
"submitAdd": "Tags hinzufügen",
"submitRemove": "Tags entfernen",
"submitOverwrite": "Tags überschreiben",
"savingAdd": "Wird hinzugefügt...",
"savingRemove": "Wird entfernt...",
"savingOverwrite": "Wird überschrieben..."
},
"thread": {
"title": "Konversations-Thread · {{count}} Nachrichten",
"error": "Fehler beim Laden des Threads",
@@ -496,7 +519,8 @@
"delete": "Löschen",
"deleteDesc": "Ausgewählte Nachrichten löschen",
"selected": "{{count}} ausgewählt",
"clear": "Auswahl aufheben"
"clear": "Auswahl aufheben",
"manageTags": "Tags verwalten"
},
"delete": {
"title": "Nachrichten löschen",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Save",
"saving": "Saving..."
},
"updateTags": {
"overwriteWarning": "Warning: This will overwrite all existing tags on the selected items.",
"title": "Update Tags",
"none": "No tags selected",
"searchPlaceholder": "Search or create new tag...",
"createHint": "Press Enter to create new tag: \"{{tag}}\"",
"selectedCount": "{{count}} tag(s) to be applied",
"cancel": "Cancel",
"invalidTitle": "Invalid Tag",
"updatedTitle": "Tags Updated",
"updatedDesc": "Tags have been successfully updated.",
"updateFailedTitle": "Update Failed",
"tryAgain": "Please try again later.",
"actionAdd": "Add",
"actionRemove": "Remove",
"actionOverwrite": "Overwrite",
"submitAdd": "Add Tags",
"submitRemove": "Remove Tags",
"submitOverwrite": "Overwrite Tags",
"savingAdd": "Adding...",
"savingRemove": "Removing...",
"savingOverwrite": "Overwriting..."
},
"thread": {
"title": "Thread · {{count}} messages",
"error": "Failed to load thread",
@@ -496,7 +519,8 @@
"delete": "Delete",
"deleteDesc": "Delete selected messages",
"selected": "{{count}} selected",
"clear": "Clear selection"
"clear": "Clear selection",
"manageTags": "Manage Tags"
},
"delete": {
"title": "Delete messages",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Guardar",
"saving": "Guardando..."
},
"updateTags": {
"overwriteWarning": "Advertencia: esto sobrescribirá todas las etiquetas existentes en los elementos seleccionados.",
"title": "Actualizar etiquetas",
"none": "No hay etiquetas seleccionadas",
"searchPlaceholder": "Buscar o crear nueva etiqueta...",
"createHint": "Presione Enter para crear una nueva etiqueta: \"{{tag}}\"",
"selectedCount": "{{count}} etiqueta(s) a aplicar",
"cancel": "Cancelar",
"invalidTitle": "Etiqueta inválida",
"updatedTitle": "Etiquetas actualizadas",
"updatedDesc": "Las etiquetas se han actualizado con éxito.",
"updateFailedTitle": "Error al actualizar",
"tryAgain": "Por favor, inténtelo de nuevo más tarde.",
"actionAdd": "Añadir",
"actionRemove": "Eliminar",
"actionOverwrite": "Sobrescribir",
"submitAdd": "Añadir etiquetas",
"submitRemove": "Eliminar etiquetas",
"submitOverwrite": "Sobrescribir etiquetas",
"savingAdd": "Añadiendo...",
"savingRemove": "Eliminando...",
"savingOverwrite": "Sobrescribiendo..."
},
"thread": {
"title": "Hilo de conversación · {{count}} mensajes",
"error": "Error al cargar el hilo",
@@ -496,7 +519,8 @@
"delete": "Eliminar",
"deleteDesc": "Eliminar seleccionados",
"selected": "{{count}} seleccionado(s)",
"clear": "Limpiar selección"
"clear": "Limpiar selección",
"manageTags": "Administrar etiquetas"
},
"delete": {
"title": "Eliminar mensajes",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Tallenna",
"saving": "Tallennetaan..."
},
"updateTags": {
"overwriteWarning": "Varoitus: Tämä korvaa kaikki valittujen kohteiden olemassa olevat tunnisteet.",
"title": "Päivitä tunnisteet",
"none": "Ei valittuja tunnisteita",
"searchPlaceholder": "Hae tai luo uusi tunniste...",
"createHint": "Paina Enter luodaksesi uuden tunnisteen: \"{{tag}}\"",
"selectedCount": "{{count}} tunniste(tta) otetaan käyttöön",
"cancel": "Peruuta",
"invalidTitle": "Virheellinen tunniste",
"updatedTitle": "Tunnisteet päivitetty",
"updatedDesc": "Tunnisteet on päivitetty onnistuneesti.",
"updateFailedTitle": "Päivitys epäonnistui",
"tryAgain": "Yritä myöhemmin uudelleen.",
"actionAdd": "Lisää",
"actionRemove": "Poista",
"actionOverwrite": "Korvaa",
"submitAdd": "Lisää tunnisteet",
"submitRemove": "Poista tunnisteet",
"submitOverwrite": "Korvaa tunnisteet",
"savingAdd": "Lisätään...",
"savingRemove": "Poistetaan...",
"savingOverwrite": "Korvataan..."
},
"thread": {
"title": "Keskusteluketju · {{count}} viestiä",
"error": "Keskusteluketjun lataus epäonnistui",
@@ -496,7 +519,8 @@
"delete": "Poista",
"deleteDesc": "Poista valitut viestit",
"selected": "{{count}} valittu",
"clear": "Tyhjennä valinnat"
"clear": "Tyhjennä valinnat",
"manageTags": "Hallitse tunnisteita"
},
"delete": {
"title": "Poista viestit",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Enregistrer",
"saving": "Enregistrement en cours..."
},
"updateTags": {
"overwriteWarning": "Avertissement : cela écrasera tous les tags existants sur les éléments sélectionnés.",
"title": "Modifier les tags",
"none": "Aucun tag sélectionné",
"searchPlaceholder": "Rechercher ou créer un tag...",
"createHint": "Appuyez sur Entrée pour créer le tag : \"{{tag}}\"",
"selectedCount": "{{count}} tag(s) à appliquer",
"cancel": "Annuler",
"invalidTitle": "Tag invalide",
"updatedTitle": "Tags mis à jour",
"updatedDesc": "Les tags ont été mis à jour avec succès.",
"updateFailedTitle": "Échec de la mise à jour",
"tryAgain": "Veuillez réessayer plus tard.",
"actionAdd": "Ajouter",
"actionRemove": "Retirer",
"actionOverwrite": "Écraser",
"submitAdd": "Ajouter les tags",
"submitRemove": "Retirer les tags",
"submitOverwrite": "Écraser les tags",
"savingAdd": "Ajout...",
"savingRemove": "Retrait...",
"savingOverwrite": "Écrasement..."
},
"thread": {
"title": "Fil de discussion · {{count}} messages",
"error": "Échec du chargement du fil de discussion",
@@ -496,7 +519,8 @@
"delete": "Supprimer",
"deleteDesc": "Supprimer les messages sélectionnés",
"selected": "{{count}} sélectionné(s)",
"clear": "Effacer la sélection"
"clear": "Effacer la sélection",
"manageTags": "Gérer les tags"
},
"delete": {
"title": "Supprimer les messages",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Salva",
"saving": "Salvataggio in corso..."
},
"updateTags": {
"overwriteWarning": "Avviso: questo sovrascriverà tutti i tag esistenti sugli elementi selezionati.",
"title": "Aggiorna tag",
"none": "Nessun tag selezionato",
"searchPlaceholder": "Cerca o crea un nuovo tag...",
"createHint": "Premi Invio per creare un nuovo tag: \"{{tag}}\"",
"selectedCount": "{{count}} tag da applicare",
"cancel": "Annulla",
"invalidTitle": "Tag non valido",
"updatedTitle": "Tag aggiornati",
"updatedDesc": "I tag sono stati aggiornati con successo.",
"updateFailedTitle": "Aggiornamento fallito",
"tryAgain": "Riprova più tardi.",
"actionAdd": "Aggiungi",
"actionRemove": "Rimuovi",
"actionOverwrite": "Sovrascrivi",
"submitAdd": "Aggiungi tag",
"submitRemove": "Rimuovi tag",
"submitOverwrite": "Sovrascrivi tag",
"savingAdd": "Aggiunta...",
"savingRemove": "Rimozione...",
"savingOverwrite": "Sovrascrittura..."
},
"thread": {
"title": "Thread · {{count}} messaggi",
"error": "Caricamento thread fallito",
@@ -496,7 +519,8 @@
"delete": "Elimina",
"deleteDesc": "Elimina messaggi selezionati",
"selected": "{{count}} selezionati",
"clear": "Annulla selezione"
"clear": "Annulla selezione",
"manageTags": "Gestisci tag"
},
"delete": {
"title": "Elimina messaggi",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "保存",
"saving": "保存中..."
},
"updateTags": {
"overwriteWarning": "警告:この操作により、選択した項目の既存のタグがすべて上書きされます。",
"title": "タグを更新",
"none": "タグが選択されていません",
"searchPlaceholder": "タグを検索または作成...",
"createHint": "Enterキーを押して新しいタグを作成:\"{{tag}}\"",
"selectedCount": "{{count}} 個のタグを適用します",
"cancel": "キャンセル",
"invalidTitle": "無効なタグ",
"updatedTitle": "タグを更新しました",
"updatedDesc": "タグが正常に更新されました。",
"updateFailedTitle": "更新に失敗しました",
"tryAgain": "後でもう一度お試しください。",
"actionAdd": "追加",
"actionRemove": "削除",
"actionOverwrite": "上書き",
"submitAdd": "タグを追加",
"submitRemove": "タグを削除",
"submitOverwrite": "タグを上書き",
"savingAdd": "追加中...",
"savingRemove": "削除中...",
"savingOverwrite": "上書き中..."
},
"thread": {
"title": "スレッド · {{count}}件のメッセージ",
"error": "スレッドの読み込みに失敗しました",
@@ -496,7 +519,8 @@
"delete": "削除",
"deleteDesc": "選択したメッセージを削除",
"selected": "{{count}} 件選択済み",
"clear": "選択を解除"
"clear": "選択を解除",
"manageTags": "タグを管理"
},
"delete": {
"title": "メッセージを削除",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "저장",
"saving": "저장 중..."
},
"updateTags": {
"overwriteWarning": "경고: 이 작업은 선택한 항목의 모든 기존 태그를 덮어씁니다.",
"title": "태그 업데이트",
"none": "선택된 태그 없음",
"searchPlaceholder": "태그 검색 또는 생성...",
"createHint": "Enter를 눌러 새 태그 생성: \"{{tag}}\"",
"selectedCount": "{{count}}개의 태그가 적용됩니다",
"cancel": "취소",
"invalidTitle": "유효하지 않은 태그",
"updatedTitle": "태그 업데이트됨",
"updatedDesc": "태그가 성공적으로 업데이트되었습니다.",
"updateFailedTitle": "업데이트 실패",
"tryAgain": "나중에 다시 시도해 주세요.",
"actionAdd": "추가",
"actionRemove": "제거",
"actionOverwrite": "덮어쓰기",
"submitAdd": "태그 추가",
"submitRemove": "태그 제거",
"submitOverwrite": "태그 덮어쓰기",
"savingAdd": "추가 중...",
"savingRemove": "제거 중...",
"savingOverwrite": "덮어쓰는 중..."
},
"thread": {
"title": "스레드 · {{count}}개 메시지",
"error": "스레드 로드 실패",
@@ -496,7 +519,8 @@
"delete": "삭제",
"deleteDesc": "선택한 메시지 삭제",
"selected": "{{count}}개 선택됨",
"clear": "선택 해제"
"clear": "선택 해제",
"manageTags": "태그 관리"
},
"delete": {
"title": "메시지 삭제",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Opslaan",
"saving": "Bezig met opslaan..."
},
"updateTags": {
"overwriteWarning": "Waarschuwing: dit overschrijft alle bestaande tags op de geselecteerde items.",
"title": "Tags bijwerken",
"none": "Geen tags geselecteerd",
"searchPlaceholder": "Zoek of maak nieuwe tag...",
"createHint": "Druk op Enter om nieuwe tag te maken: \"{{tag}}\"",
"selectedCount": "{{count}} tag(s) worden toegepast",
"cancel": "Annuleren",
"invalidTitle": "Ongeldige tag",
"updatedTitle": "Tags bijgewerkt",
"updatedDesc": "Tags zijn succesvol bijgewerkt.",
"updateFailedTitle": "Bijwerken mislukt",
"tryAgain": "Probeer het later opnieuw.",
"actionAdd": "Toevoegen",
"actionRemove": "Verwijderen",
"actionOverwrite": "Overschrijven",
"submitAdd": "Tags toevoegen",
"submitRemove": "Tags verwijderen",
"submitOverwrite": "Tags overschrijven",
"savingAdd": "Toevoegen...",
"savingRemove": "Verwijderen...",
"savingOverwrite": "Overschrijven..."
},
"thread": {
"title": "Draad · {{count}} berichten",
"error": "Laden van draad mislukt",
@@ -496,7 +519,8 @@
"delete": "Verwijderen",
"deleteDesc": "Geselecteerde berichten verwijderen",
"selected": "{{count}} geselecteerd",
"clear": "Selectie wissen"
"clear": "Selectie wissen",
"manageTags": "Tags beheren"
},
"delete": {
"title": "Berichten verwijderen",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Lagre",
"saving": "Lagrer..."
},
"updateTags": {
"overwriteWarning": "Advarsel: Dette vil overskrive alle eksisterende tagger på de valgte elementene.",
"title": "Oppdater tagger",
"none": "Ingen tagger valgt",
"searchPlaceholder": "Søk eller opprett ny tagg...",
"createHint": "Trykk Enter for å opprette ny tagg: \"{{tag}}\"",
"selectedCount": "{{count}} tag(g/er) vil bli brukt",
"cancel": "Avbryt",
"invalidTitle": "Ugyldig tagg",
"updatedTitle": "Tagger oppdatert",
"updatedDesc": "Tagger har blitt oppdatert.",
"updateFailedTitle": "Oppdatering mislyktes",
"tryAgain": "Vennligst prøv igjen senere.",
"actionAdd": "Legg til",
"actionRemove": "Fjern",
"actionOverwrite": "Overskriv",
"submitAdd": "Legg til tagger",
"submitRemove": "Fjern tagger",
"submitOverwrite": "Overskriv tagger",
"savingAdd": "Legger til...",
"savingRemove": "Fjerner...",
"savingOverwrite": "Overskriver..."
},
"thread": {
"title": "Tråd · {{count}} meldinger",
"error": "Kunne ikke laste tråd",
@@ -496,7 +519,8 @@
"delete": "Slett",
"deleteDesc": "Slett valgte meldinger",
"selected": "{{count}} valgt",
"clear": "Fjern valg"
"clear": "Fjern valg",
"manageTags": "Administrer tagger"
},
"delete": {
"title": "Slett meldinger",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Zapisz",
"saving": "Zapisywanie..."
},
"updateTags": {
"overwriteWarning": "Ostrzeżenie: to nadpisze wszystkie istniejące tagi w wybranych elementach.",
"title": "Aktualizuj tagi",
"none": "Nie wybrano żadnych tagów",
"searchPlaceholder": "Szukaj lub utwórz nowy tag...",
"createHint": "Naciśnij Enter, aby utworzyć nowy tag: \"{{tag}}\"",
"selectedCount": "Zostanie zastosowanych tagów: {{count}}",
"cancel": "Anuluj",
"invalidTitle": "Nieprawidłowy tag",
"updatedTitle": "Tagi zaktualizowane",
"updatedDesc": "Tagi zostały pomyślnie zaktualizowane.",
"updateFailedTitle": "Aktualizacja nie powiodła się",
"tryAgain": "Spróbuj ponownie później.",
"actionAdd": "Dodaj",
"actionRemove": "Usuń",
"actionOverwrite": "Nadpisz",
"submitAdd": "Dodaj tagi",
"submitRemove": "Usuń tagi",
"submitOverwrite": "Nadpisz tagi",
"savingAdd": "Dodawanie...",
"savingRemove": "Usuwanie...",
"savingOverwrite": "Nadpisywanie..."
},
"thread": {
"title": "Wątek · {{count}} wiadomości",
"error": "Nie udało się załadować wątku",
@@ -496,7 +519,8 @@
"delete": "Usuń",
"deleteDesc": "Usuń wybrane wiadomości",
"selected": "Wybrano {{count}}",
"clear": "Wyczyść zaznaczenie"
"clear": "Wyczyść zaznaczenie",
"manageTags": "Zarządzaj tagami"
},
"delete": {
"title": "Usuń wiadomości",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Salvar",
"saving": "Salvando..."
},
"updateTags": {
"overwriteWarning": "Aviso: isto substituirá todas as etiquetas existentes nos itens selecionados.",
"title": "Atualizar etiquetas",
"none": "Nenhuma etiqueta selecionada",
"searchPlaceholder": "Procurar ou criar nova etiqueta...",
"createHint": "Pressione Enter para criar nova etiqueta: \"{{tag}}\"",
"selectedCount": "{{count}} etiqueta(s) a aplicar",
"cancel": "Cancelar",
"invalidTitle": "Etiqueta inválida",
"updatedTitle": "Etiquetas atualizadas",
"updatedDesc": "Etiquetas atualizadas com sucesso.",
"updateFailedTitle": "Falha na atualização",
"tryAgain": "Por favor, tente novamente mais tarde.",
"actionAdd": "Adicionar",
"actionRemove": "Remover",
"actionOverwrite": "Substituir",
"submitAdd": "Adicionar etiquetas",
"submitRemove": "Remover etiquetas",
"submitOverwrite": "Substituir etiquetas",
"savingAdd": "Adicionando...",
"savingRemove": "Removendo...",
"savingOverwrite": "Substituindo..."
},
"thread": {
"title": "Tópico · {{count}} Mensagens",
"error": "Falha ao carregar o tópico",
@@ -496,7 +519,8 @@
"delete": "Excluir",
"deleteDesc": "Excluir mensagens selecionadas",
"selected": "{{count}} selecionadas",
"clear": "Limpar seleção"
"clear": "Limpar seleção",
"manageTags": "Gerenciar etiquetas"
},
"delete": {
"title": "Excluir mensagens",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Сохранить",
"saving": "Сохранение..."
},
"updateTags": {
"overwriteWarning": "Предупреждение: это перезапишет все существующие теги в выбранных элементах.",
"title": "Обновить теги",
"none": "Теги не выбраны",
"searchPlaceholder": "Поиск или создание тега...",
"createHint": "Нажмите Enter, чтобы создать тег: \"{{tag}}\"",
"selectedCount": "Будет применено тегов: {{count}}",
"cancel": "Отмена",
"invalidTitle": "Недопустимый тег",
"updatedTitle": "Теги обновлены",
"updatedDesc": "Теги были успешно обновлены.",
"updateFailedTitle": "Ошибка обновления",
"tryAgain": "Пожалуйста, попробуйте позже.",
"actionAdd": "Добавить",
"actionRemove": "Удалить",
"actionOverwrite": "Перезаписать",
"submitAdd": "Добавить теги",
"submitRemove": "Удалить теги",
"submitOverwrite": "Перезаписать теги",
"savingAdd": "Добавление...",
"savingRemove": "Удаление...",
"savingOverwrite": "Перезапись..."
},
"thread": {
"title": "Цепочка · {{count}} сообщений",
"error": "Не удалось загрузить цепочку",
@@ -496,7 +519,8 @@
"delete": "Удалить",
"deleteDesc": "Удалить выбранные",
"selected": "Выбрано: {{count}}",
"clear": "Очистить выбор"
"clear": "Очистить выбор",
"manageTags": "Управление тегами"
},
"delete": {
"title": "Удалить сообщения",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "Spara",
"saving": "Sparar..."
},
"updateTags": {
"overwriteWarning": "Varning: Detta kommer att skriva över alla befintliga taggar på de valda objekten.",
"title": "Uppdatera taggar",
"none": "Inga taggar valda",
"searchPlaceholder": "Sök eller skapa ny tagg...",
"createHint": "Tryck på Enter för att skapa ny tagg: \"{{tag}}\"",
"selectedCount": "{{count}} tagg(ar) kommer att tillämpas",
"cancel": "Avbryt",
"invalidTitle": "Ogiltig tagg",
"updatedTitle": "Taggar uppdaterade",
"updatedDesc": "Taggarna har uppdaterats.",
"updateFailedTitle": "Uppdateringen misslyckades",
"tryAgain": "Försök igen senare.",
"actionAdd": "Lägg till",
"actionRemove": "Ta bort",
"actionOverwrite": "Skriv över",
"submitAdd": "Lägg till taggar",
"submitRemove": "Ta bort taggar",
"submitOverwrite": "Skriv över taggar",
"savingAdd": "Lägger till...",
"savingRemove": "Tar bort...",
"savingOverwrite": "Skriver över..."
},
"thread": {
"title": "Tråd · {{count}} meddelanden",
"error": "Kunde inte ladda tråd",
@@ -496,7 +519,8 @@
"delete": "Ta bort",
"deleteDesc": "Ta bort valda meddelanden",
"selected": "{{count}} valda",
"clear": "Rensa val"
"clear": "Rensa val",
"manageTags": "Hantera taggar"
},
"delete": {
"title": "Ta bort meddelanden",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "儲存",
"saving": "正在儲存..."
},
"updateTags": {
"overwriteWarning": "警告:此操作將覆蓋所選項目的所有現有標籤。",
"title": "更新標籤",
"none": "未選擇標籤",
"searchPlaceholder": "搜尋或建立新標籤...",
"createHint": "按 Enter 鍵建立新標籤:\"{{tag}}\"",
"selectedCount": "將套用 {{count}} 個標籤",
"cancel": "取消",
"invalidTitle": "標籤無效",
"updatedTitle": "標籤已更新",
"updatedDesc": "標籤已成功更新。",
"updateFailedTitle": "更新失敗",
"tryAgain": "請稍後再試。",
"actionAdd": "新增",
"actionRemove": "移除",
"actionOverwrite": "覆蓋",
"submitAdd": "新增標籤",
"submitRemove": "移除標籤",
"submitOverwrite": "覆蓋標籤",
"savingAdd": "正在新增...",
"savingRemove": "正在移除...",
"savingOverwrite": "正在覆蓋..."
},
"thread": {
"title": "串流 · {{count}} 則訊息",
"error": "載入串流失敗",
@@ -496,7 +519,8 @@
"delete": "刪除",
"deleteDesc": "刪除選中的訊息",
"selected": "已選擇 {{count}} 項",
"clear": "清除選取"
"clear": "清除選取",
"manageTags": "管理標籤"
},
"delete": {
"title": "刪除訊息",
+25 -1
View File
@@ -482,6 +482,29 @@
"save": "保存",
"saving": "保存中..."
},
"updateTags": {
"overwriteWarning": "警告:此操作将覆盖所选项目的所有现有标签。",
"title": "更新标签",
"none": "未选择标签",
"searchPlaceholder": "搜索或创建新标签...",
"createHint": "按回车键创建新标签:\"{{tag}}\"",
"selectedCount": "将应用 {{count}} 个标签",
"saving": "正在更新...",
"invalidTitle": "标签无效",
"updatedTitle": "标签已更新",
"updatedDesc": "标签已成功更新。",
"updateFailedTitle": "更新失败",
"tryAgain": "请稍后再试。",
"actionAdd": "追加",
"actionRemove": "移除",
"actionOverwrite": "覆盖",
"submitAdd": "追加标签",
"submitRemove": "移除标签",
"submitOverwrite": "覆盖标签",
"savingAdd": "正在追加...",
"savingRemove": "正在移除...",
"savingOverwrite": "正在覆盖..."
},
"thread": {
"title": "会话 · 共 {{count}} 封邮件",
"error": "加载会话失败",
@@ -496,7 +519,8 @@
"delete": "删除",
"deleteDesc": "删除选中的消息",
"selected": "已选择 {{count}} 项",
"clear": "清除选择"
"clear": "清除选择",
"manageTags": "管理标签"
},
"delete": {
"title": "删除消息",