diff --git a/components/files/file-browser.tsx b/components/files/file-browser.tsx index 84b5afc4f..8a47b7d32 100644 --- a/components/files/file-browser.tsx +++ b/components/files/file-browser.tsx @@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"; import { cn, formatFileSize } from "@/lib/utils"; import { NewFolderDialog } from "@/components/files/new-folder-dialog"; import { RenameDialog } from "@/components/files/rename-dialog"; +import { fileNameProblem } from "@/lib/file-name-rules"; import { FileUploadArea } from "@/components/files/file-upload-area"; import { loadFilesSettings } from "@/components/files/files-settings-dialog"; import type { FolderLayout } from "@/components/files/files-settings-dialog"; @@ -400,6 +401,12 @@ export function FileBrowser({ onShare, }: FileBrowserProps) { const t = useTranslations("files"); + const fileNameRules = useMemo(() => client?.getFileNameRules?.() ?? null, [client]); + const validateFileName = useCallback((name: string) => { + const problem = fileNameProblem(name, fileNameRules); + if (!problem) return null; + return problem.kind === "chars" ? t("name_forbidden_chars", { chars: problem.chars }) : t("name_reserved"); + }, [fileNameRules, t]); const [showNewFolder, setShowNewFolder] = useState(false); const [renameTarget, setRenameTarget] = useState(null); const [shareTargetId, setShareTargetId] = useState(null); @@ -2003,6 +2010,7 @@ export function FileBrowser({ setShowNewFolder(false); }} onCancel={() => setShowNewFolder(false)} + validate={validateFileName} /> )} @@ -2017,6 +2025,7 @@ export function FileBrowser({ setShowNewTextFile(false); }} onCancel={() => setShowNewTextFile(false)} + validate={validateFileName} /> )} @@ -2029,6 +2038,7 @@ export function FileBrowser({ setRenameTarget(null); }} onCancel={() => setRenameTarget(null)} + validate={validateFileName} /> )} diff --git a/components/files/new-folder-dialog.tsx b/components/files/new-folder-dialog.tsx index 6456a3d35..b6ca1181b 100644 --- a/components/files/new-folder-dialog.tsx +++ b/components/files/new-folder-dialog.tsx @@ -8,17 +8,20 @@ import { Input } from "@/components/ui/input"; interface NewFolderDialogProps { onConfirm: (name: string) => Promise; onCancel: () => void; + /** Returns an error message for a name the server would refuse. */ + validate?: (name: string) => string | null; } -export function NewFolderDialog({ onConfirm, onCancel }: NewFolderDialogProps) { +export function NewFolderDialog({ onConfirm, onCancel, validate }: NewFolderDialogProps) { const t = useTranslations("files"); const [name, setName] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const nameError = name.trim() ? validate?.(name.trim()) ?? null : null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = name.trim(); - if (!trimmed) return; + if (!trimmed || validate?.(trimmed)) return; setIsSubmitting(true); try { @@ -41,13 +44,15 @@ export function NewFolderDialog({ onConfirm, onCancel }: NewFolderDialogProps) { value={name} onChange={(e) => setName(e.target.value)} placeholder={t("new_folder_name")} - className="mb-4" + className={nameError ? "mb-1" : "mb-4"} + aria-invalid={!!nameError} /> + {nameError &&

{nameError}

}
-
diff --git a/components/files/rename-dialog.tsx b/components/files/rename-dialog.tsx index 7528bb3dd..f4b52fa87 100644 --- a/components/files/rename-dialog.tsx +++ b/components/files/rename-dialog.tsx @@ -11,17 +11,20 @@ interface RenameDialogProps { label?: string; onConfirm: (newName: string) => Promise; onCancel: () => void; + /** Returns an error message for a name the server would refuse. */ + validate?: (name: string) => string | null; } -export function RenameDialog({ currentName, title, label, onConfirm, onCancel }: RenameDialogProps) { +export function RenameDialog({ currentName, title, label, onConfirm, onCancel, validate }: RenameDialogProps) { const t = useTranslations("files"); const [name, setName] = useState(currentName); const [isSubmitting, setIsSubmitting] = useState(false); + const nameError = name.trim() ? validate?.(name.trim()) ?? null : null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = name.trim(); - if (!trimmed) return; + if (!trimmed || validate?.(trimmed)) return; setIsSubmitting(true); try { @@ -44,13 +47,15 @@ export function RenameDialog({ currentName, title, label, onConfirm, onCancel }: value={name} onChange={(e) => setName(e.target.value)} placeholder={label || t("new_name")} - className="mb-4" + className={nameError ? "mb-1" : "mb-4"} + aria-invalid={!!nameError} /> + {nameError &&

{nameError}

}
-
diff --git a/lib/__tests__/file-name-rules.test.ts b/lib/__tests__/file-name-rules.test.ts new file mode 100644 index 000000000..98314d397 --- /dev/null +++ b/lib/__tests__/file-name-rules.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { acceptedFileName, fileNameProblem, fileNameRulesFrom } from '../file-name-rules'; + +// The filenode capability Stalwart 0.16.23 publishes. +const STALWART = fileNameRulesFrom({ + maxSizeFileNodeName: 255, + forbiddenNameChars: '/<>:"\\|?*', + forbiddenNodeNames: ['.', '..', 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1'], +}); + +describe('file name rules', () => { + it('has no rules for servers that publish none (Stalwart before 0.16.6)', () => { + expect(fileNameRulesFrom({ maxSizeFileNodeName: 255 })).toBeNull(); + expect(fileNameProblem('a:b', null)).toBeNull(); + expect(acceptedFileName('a:b', null)).toBe('a:b'); + }); + + it('reports forbidden characters and reserved names', () => { + expect(fileNameProblem('bad:name?.txt', STALWART)).toEqual({ kind: 'chars', chars: ': ?' }); + expect(fileNameProblem('con', STALWART)).toEqual({ kind: 'reserved' }); + expect(fileNameProblem('CON.txt', STALWART)).toBeNull(); + expect(fileNameProblem('report.pdf', STALWART)).toBeNull(); + }); + + it('turns an uploaded name into one the server accepts', () => { + expect(acceptedFileName('bad:name?.txt', STALWART)).toBe('bad_name_.txt'); + expect(acceptedFileName('NUL', STALWART)).toBe('NUL_'); + expect(acceptedFileName('fine.txt', STALWART)).toBe('fine.txt'); + }); +}); diff --git a/lib/file-name-rules.ts b/lib/file-name-rules.ts new file mode 100644 index 000000000..eeebe0a6f --- /dev/null +++ b/lib/file-name-rules.ts @@ -0,0 +1,45 @@ +/** + * FileNode naming rules a server publishes in its filenode capability + * (`forbiddenNameChars`, `forbiddenNodeNames`; Stalwart 0.16.6+). A name that + * breaks them is refused ("Name contains a forbidden character." / "Name is + * reserved and cannot be used."), which used to surface only after an upload + * had started and then aborted the rest of the batch. + */ +export interface FileNameRules { + forbiddenChars: string; + forbiddenNames: string[]; +} + +export type FileNameProblem = { kind: 'chars'; chars: string } | { kind: 'reserved' }; + +export function fileNameRulesFrom(capability: unknown): FileNameRules | null { + if (!capability || typeof capability !== 'object') return null; + const cap = capability as { forbiddenNameChars?: unknown; forbiddenNodeNames?: unknown }; + const forbiddenChars = typeof cap.forbiddenNameChars === 'string' ? cap.forbiddenNameChars : ''; + const forbiddenNames = Array.isArray(cap.forbiddenNodeNames) + ? cap.forbiddenNodeNames.filter((n): n is string => typeof n === 'string') + : []; + if (!forbiddenChars && forbiddenNames.length === 0) return null; + return { forbiddenChars, forbiddenNames }; +} + +export function fileNameProblem(name: string, rules: FileNameRules | null): FileNameProblem | null { + if (!rules) return null; + const bad = [...new Set([...name].filter(c => rules.forbiddenChars.includes(c)))]; + if (bad.length) return { kind: 'chars', chars: bad.join(' ') }; + // Matched against the whole name, ignoring case: "CON" is reserved, "CON.txt" is not. + if (rules.forbiddenNames.some(n => n.toLowerCase() === name.toLowerCase())) return { kind: 'reserved' }; + return null; +} + +/** + * A variant of `name` the server accepts: forbidden characters become "_", + * a reserved name gets "_" appended. Used for uploads, where the name comes + * from the local file system and cannot be asked for again. + */ +export function acceptedFileName(name: string, rules: FileNameRules | null): string { + if (!rules) return name; + let out = [...name].map(c => (rules.forbiddenChars.includes(c) ? '_' : c)).join(''); + if (rules.forbiddenNames.some(n => n.toLowerCase() === out.toLowerCase())) out = `${out}_`; + return out; +} diff --git a/lib/jmap/client-interface.ts b/lib/jmap/client-interface.ts index 1966d122f..9e2287a87 100644 --- a/lib/jmap/client-interface.ts +++ b/lib/jmap/client-interface.ts @@ -1,5 +1,6 @@ import type { Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, CalendarParticipantIdentity, CalendarEventNotification, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CreateCalendarOptions, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeRights, Principal, PushSubscription, EmailPushConfig, ScheduledEmail, SendEmailResult, SharedAccount } from "./types"; import type { SieveScript, SieveCapabilities } from "./sieve-types"; +import type { FileNameRules } from "@/lib/file-name-rules"; import type { SortLevel } from "@/lib/message-list-order"; /** What `migrateKeyword` managed to do. */ @@ -58,6 +59,8 @@ export interface IJMAPClient { // ── Capabilities ────────────────────────────────────────────── getCapabilities(): Record; hasAccountCapability(capability: string, accountId?: string): boolean; + /** FileNode naming rules the server publishes (Stalwart 0.16.6+), if any. */ + getFileNameRules?(accountId?: string): FileNameRules | null; getMaxSizeUpload(): number; getMaxCallsInRequest(): number; getMaxObjectsInGet(): number; diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 3148b459d..4ce9e7f70 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -15,6 +15,7 @@ import { DEFAULT_CALENDAR_COMPONENTS, mkCalendarCollection, newCalendarCollectio import { sanitizeDisplayName, splitMailbox } from "@/lib/rfc5322-mailbox"; import { decodeFileNodeName } from "./filenode-name"; import { contactFromWire, contactToWire } from "./contact-wire"; +import { acceptedFileName, fileNameRulesFrom, type FileNameRules } from "@/lib/file-name-rules"; import { getEffectiveTimeZone, toLocalDateTime } from "@/lib/timezone"; import { buildEmailSort, compareEmails, hasKeywordLevels, type KeywordSortPolarity, type SortLevel } from "@/lib/message-list-order"; @@ -4632,6 +4633,12 @@ export class JMAPClient implements IJMAPClient { return capability in this.capabilities; } + /** The value of an account capability (primary account by default). */ + getAccountCapability(capability: string, accountId?: string): unknown { + const id = accountId || this.accountId; + return this.session?.accounts?.[id]?.accountCapabilities?.[capability]; + } + /** Check whether a capability is present on the primary account. */ hasAccountCapability(capability: string, accountId?: string): boolean { const id = accountId || this.accountId; @@ -7492,12 +7499,7 @@ export class JMAPClient implements IJMAPClient { : this.resolveFileNodeId(fileNodeId); const accountId = node.accountId; const rawId = node.id as string; - // `forbiddenNameChars` arrived with the draft (Stalwart 0.16.6) that split - // mayWrite into the finer rights; older servers reject those names. - const fileCapability = this.session?.accounts?.[accountId]?.accountCapabilities?.["urn:ietf:params:jmap:filenode"] as - Record | undefined; - const legacyRights = !!fileCapability && !("forbiddenNameChars" in fileCapability); - const wireRights = rights && legacyRights ? toLegacyRights(rights) : rights; + const wireRights = rights && this.isLegacyFileNodeServer(accountId) ? toLegacyRights(rights) : rights; const response = await this.request([ ["FileNode/set", { accountId, @@ -7534,6 +7536,25 @@ export class JMAPClient implements IJMAPClient { return { accountId: primary, id }; } + private fileNodeCapability(accountId: string): Record | undefined { + return this.getAccountCapability("urn:ietf:params:jmap:filenode", accountId) as Record | undefined; + } + + /** + * Stalwart before 0.16.6 implements an older File Storage draft (old + * rights names, 30-character MIME types, no naming rules). The draft update + * added `forbiddenNameChars` to the capability, which tells them apart. + */ + private isLegacyFileNodeServer(accountId: string): boolean { + const cap = this.fileNodeCapability(accountId); + return !!cap && !("forbiddenNameChars" in cap); + } + + /** Naming rules for new FileNodes in `accountId`, if the server publishes any. */ + getFileNameRules(accountId?: string): FileNameRules | null { + return fileNameRulesFrom(this.fileNodeCapability(accountId || this.getFilesAccountId())); + } + /** Namespace a node created in `accountId` the way the listings do. */ private namespaceFileNode(node: FileNode, accountId: string): FileNode { if (accountId === this.getFilesAccountId()) return node; @@ -7566,7 +7587,9 @@ export class JMAPClient implements IJMAPClient { props: Record, what: string, ): Promise { - const baseName = String(props.name ?? ''); + // Names from the local file system (uploads) may hold characters the + // server refuses; store an accepted variant instead of failing the batch. + const baseName = acceptedFileName(String(props.name ?? ''), this.getFileNameRules(accountId)); for (let attempt = 1; ; attempt++) { const name = attempt === 1 ? baseName : numberedFileName(baseName, attempt); const response = await this.request( @@ -7610,8 +7633,10 @@ export class JMAPClient implements IJMAPClient { async createFileNode(name: string, blobId: string, type: string, size: number, parentId: string | null): Promise { const parent = this.resolveFileNodeId(parentId); - //fall back for long MIME types - const safeType = type.length > 30 ? 'application/octet-stream' : type; + // Servers before 0.16.6 refuse MIME types over 30 characters (most OOXML + // types); later ones take up to 255. + const maxTypeLength = this.isLegacyFileNodeServer(parent.accountId) ? 30 : 255; + const safeType = type.length > maxTypeLength ? 'application/octet-stream' : type; const fileProps: Record = { name, type: safeType, blobId, size }; if (parent.id !== null) { fileProps.parentId = parent.id; diff --git a/locales/ar/common.json b/locales/ar/common.json index a6c736d7b..915db2c8b 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -3325,6 +3325,8 @@ "rename": "إعادة تسمية", "rename_title": "إعادة تسمية", "new_name": "الاسم الجديد", + "name_forbidden_chars": "لا يسمح الخادم بهذه الأحرف في الأسماء: {chars}", + "name_reserved": "هذا الاسم محجوز على الخادم.", "delete": "حذف", "delete_confirm_title": "حذف المورد", "delete_confirm_message": "هل أنت متأكد أنك تريد حذف \"{name}\"؟ لا يمكن التراجع عن هذا.", diff --git a/locales/ca/common.json b/locales/ca/common.json index 3c1e7c7c7..1abd4f014 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -3325,6 +3325,8 @@ "rename": "Canvia el nom", "rename_title": "Canvia el nom", "new_name": "Nom nou", + "name_forbidden_chars": "El servidor no permet aquests caràcters als noms: {chars}", + "name_reserved": "Aquest nom està reservat al servidor.", "delete": "Suprimeix", "delete_confirm_title": "Suprimeix el recurs", "delete_confirm_message": "Segur que voleu suprimir «{name}»? Això no es pot desfer.", diff --git a/locales/cs/common.json b/locales/cs/common.json index bf994926a..8e7a05209 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -3283,6 +3283,8 @@ "rename": "Přejmenovat", "rename_title": "Přejmenovat", "new_name": "Nový název", + "name_forbidden_chars": "Server nepovoluje v názvech tyto znaky: {chars}", + "name_reserved": "Tento název je na serveru vyhrazen.", "delete": "Odstranit", "delete_confirm_title": "Odstranit položku", "delete_confirm_message": "Opravdu chcete odstranit \"{name}\"? Tuto akci nelze vrátit zpět.", diff --git a/locales/da/common.json b/locales/da/common.json index f04ede91b..6e8ebc125 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -3325,6 +3325,8 @@ "rename": "Omdøb", "rename_title": "Omdøb", "new_name": "Nyt navn", + "name_forbidden_chars": "Serveren tillader ikke disse tegn i navne: {chars}", + "name_reserved": "Dette navn er reserveret på serveren.", "delete": "Slet", "delete_confirm_title": "Slet ressource", "delete_confirm_message": "Er du sikker på, at du vil slette \"{name}\"? Dette kan ikke fortrydes.", diff --git a/locales/de/common.json b/locales/de/common.json index 0dbed8760..cb0cfe760 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -3283,6 +3283,8 @@ "rename": "Umbenennen", "rename_title": "Umbenennen", "new_name": "Neuer Name", + "name_forbidden_chars": "Diese Zeichen sind in Namen auf dem Server nicht erlaubt: {chars}", + "name_reserved": "Dieser Name ist auf dem Server reserviert.", "delete": "Löschen", "delete_confirm_title": "Ressource löschen", "delete_confirm_message": "Möchten Sie \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.", diff --git a/locales/en/common.json b/locales/en/common.json index 851a0e4fd..ec1ed2ea7 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -3325,6 +3325,8 @@ "rename": "Rename", "rename_title": "Rename", "new_name": "New name", + "name_forbidden_chars": "The server does not allow these characters in names: {chars}", + "name_reserved": "This name is reserved on the server.", "delete": "Delete", "delete_confirm_title": "Delete resource", "delete_confirm_message": "Are you sure you want to delete \"{name}\"? This cannot be undone.", diff --git a/locales/es/common.json b/locales/es/common.json index aa5a72a05..a76b95330 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -3283,6 +3283,8 @@ "rename": "Renombrar", "rename_title": "Renombrar", "new_name": "Nuevo nombre", + "name_forbidden_chars": "El servidor no permite estos caracteres en los nombres: {chars}", + "name_reserved": "Este nombre está reservado en el servidor.", "delete": "Eliminar", "delete_confirm_title": "Eliminar recurso", "delete_confirm_message": "¿Está seguro de que desea eliminar \"{name}\"? Esta acción no se puede deshacer.", diff --git a/locales/fa/common.json b/locales/fa/common.json index 40a96f9c8..03c400adc 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -3325,6 +3325,8 @@ "rename": "تغییر نام", "rename_title": "تغییر نام", "new_name": "نام جدید", + "name_forbidden_chars": "سرور این نویسه‌ها را در نام‌ها مجاز نمی‌داند: {chars}", + "name_reserved": "این نام در سرور رزرو شده است.", "delete": "حذف", "delete_confirm_title": "حذف منبع", "delete_confirm_message": "آیا از حذف \"{name}\" اطمینان دارید؟ این عملیات قابل بازگشت نیست.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 6fd321684..031c3fbb4 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -3283,6 +3283,8 @@ "rename": "Renommer", "rename_title": "Renommer", "new_name": "Nouveau nom", + "name_forbidden_chars": "Le serveur n'autorise pas ces caractères dans les noms : {chars}", + "name_reserved": "Ce nom est réservé sur le serveur.", "delete": "Supprimer", "delete_confirm_title": "Supprimer la ressource", "delete_confirm_message": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.", diff --git a/locales/he/common.json b/locales/he/common.json index 27d3d8741..dea351914 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -3206,6 +3206,8 @@ "rename": "שנה שם", "rename_title": "שנה שם", "new_name": "שם חדש", + "name_forbidden_chars": "השרת אינו מתיר את התווים האלה בשמות: {chars}", + "name_reserved": "השם הזה שמור בשרת.", "delete": "לִמְחוֹק", "delete_confirm_title": "מחק משאב", "delete_confirm_message": "האם אתה בטוח שברצונך למחוק את \"{name}\"? לא ניתן לבטל זאת.", diff --git a/locales/hu/common.json b/locales/hu/common.json index 78bc4e487..a9aa10076 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -3325,6 +3325,8 @@ "rename": "Átnevezés", "rename_title": "Átnevezés", "new_name": "Új név", + "name_forbidden_chars": "A kiszolgáló nem engedi ezeket a karaktereket a nevekben: {chars}", + "name_reserved": "Ez a név a kiszolgálón foglalt.", "delete": "Törlés", "delete_confirm_title": "Erőforrás törlése", "delete_confirm_message": "Biztosan törölni szeretnéd a \"{name}\" elemet? Ez nem vonható vissza.", diff --git a/locales/it/common.json b/locales/it/common.json index 5fe5dd088..7ead46d4e 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -3283,6 +3283,8 @@ "rename": "Rinomina", "rename_title": "Rinomina", "new_name": "Nuovo nome", + "name_forbidden_chars": "Il server non consente questi caratteri nei nomi: {chars}", + "name_reserved": "Questo nome è riservato sul server.", "delete": "Elimina", "delete_confirm_title": "Elimina risorsa", "delete_confirm_message": "Sei sicuro di voler eliminare \"{name}\"? Questa azione non può essere annullata.", diff --git a/locales/ja/common.json b/locales/ja/common.json index 83658b1a4..fb35af8b9 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -3283,6 +3283,8 @@ "rename": "名前を変更", "rename_title": "名前を変更", "new_name": "新しい名前", + "name_forbidden_chars": "サーバーでは名前に次の文字を使用できません: {chars}", + "name_reserved": "この名前はサーバーで予約されています。", "delete": "削除", "delete_confirm_title": "リソースを削除", "delete_confirm_message": "\"{name}\"を削除してもよろしいですか?この操作は元に戻せません。", diff --git a/locales/ko/common.json b/locales/ko/common.json index 034de1970..1f6a7f8bf 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -3283,6 +3283,8 @@ "rename": "이름 바꾸기", "rename_title": "이름 바꾸기", "new_name": "새 이름", + "name_forbidden_chars": "서버에서 이름에 다음 문자를 허용하지 않습니다: {chars}", + "name_reserved": "이 이름은 서버에서 예약되어 있습니다.", "delete": "삭제", "delete_confirm_title": "항목 삭제", "delete_confirm_message": "정말 \"{name}\"을(를) 삭제할까요? 이 작업은 되돌릴 수 없어요.", diff --git a/locales/lv/common.json b/locales/lv/common.json index 4e86b9867..990899f33 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -3283,6 +3283,8 @@ "rename": "Pārdēvēt", "rename_title": "Pārdēvēt", "new_name": "Jauns nosaukums", + "name_forbidden_chars": "Serveris neatļauj šīs rakstzīmes nosaukumos: {chars}", + "name_reserved": "Šis nosaukums serverī ir rezervēts.", "delete": "Dzēst", "delete_confirm_title": "Dzēst resursu", "delete_confirm_message": "Vai tiešām vēlaties dzēst «{name}»? Šo darbību nevar atcelt.", diff --git a/locales/mn/common.json b/locales/mn/common.json index b5b949591..428eae047 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -3325,6 +3325,8 @@ "rename": "Нэрээ өөрчлөх", "rename_title": "Нэрээ өөрчлөх", "new_name": "Шинэ нэр", + "name_forbidden_chars": "Сервер нэрэнд эдгээр тэмдэгтийг зөвшөөрдөггүй: {chars}", + "name_reserved": "Энэ нэр сервер дээр нөөцлөгдсөн.", "delete": "Устгах", "delete_confirm_title": "Нөөцийг устгах", "delete_confirm_message": "Та \"{name}\"-г устгахдаа итгэлтэй байна уу? Үүнийг буцаах боломжгүй.", diff --git a/locales/nb/common.json b/locales/nb/common.json index 2a1f89edf..f5276bd01 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -3325,6 +3325,8 @@ "rename": "Gi nytt navn", "rename_title": "Gi nytt navn", "new_name": "Nytt navn", + "name_forbidden_chars": "Serveren tillater ikke disse tegnene i navn: {chars}", + "name_reserved": "Dette navnet er reservert på serveren.", "delete": "Slett", "delete_confirm_title": "Slett fil eller mappe", "delete_confirm_message": "Er du sikker på at du vil slette «{name}»? Dette kan ikke angres.", diff --git a/locales/nl/common.json b/locales/nl/common.json index 5bb97790e..c8963900e 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -3283,6 +3283,8 @@ "rename": "Hernoemen", "rename_title": "Hernoemen", "new_name": "Nieuwe naam", + "name_forbidden_chars": "De server staat deze tekens niet toe in namen: {chars}", + "name_reserved": "Deze naam is gereserveerd op de server.", "delete": "Verwijderen", "delete_confirm_title": "Bron verwijderen", "delete_confirm_message": "Weet u zeker dat u \"{name}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.", diff --git a/locales/pl/common.json b/locales/pl/common.json index c4ccfdae9..7e882dc3a 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -3283,6 +3283,8 @@ "rename": "Zmień nazwę", "rename_title": "Zmień nazwę", "new_name": "Nowa nazwa", + "name_forbidden_chars": "Serwer nie zezwala na te znaki w nazwach: {chars}", + "name_reserved": "Ta nazwa jest zarezerwowana na serwerze.", "delete": "Usuń", "delete_confirm_title": "Usuń zasób", "delete_confirm_message": "Czy na pewno chcesz usunąć \"{name}\"? Tej operacji nie można cofnąć.", diff --git a/locales/pt/common.json b/locales/pt/common.json index 3c8fec9af..29fb3c13a 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -3283,6 +3283,8 @@ "rename": "Renomear", "rename_title": "Renomear", "new_name": "Novo nome", + "name_forbidden_chars": "O servidor não permite estes caracteres nos nomes: {chars}", + "name_reserved": "Este nome está reservado no servidor.", "delete": "Eliminar", "delete_confirm_title": "Eliminar recurso", "delete_confirm_message": "Tem a certeza de que deseja eliminar \"{name}\"? Esta ação não pode ser desfeita.", diff --git a/locales/ro/common.json b/locales/ro/common.json index 1a2e67460..6bfbb8fa3 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -3325,6 +3325,8 @@ "rename": "Redenumire", "rename_title": "Redenumire", "new_name": "Nume nou", + "name_forbidden_chars": "Serverul nu permite aceste caractere în nume: {chars}", + "name_reserved": "Acest nume este rezervat pe server.", "delete": "Șterge", "delete_confirm_title": "Ștergeți resursa", "delete_confirm_message": "Sunteți sigur că doriți să ștergeți „{name}”? Această acțiune nu poate fi anulată.", diff --git a/locales/ru/common.json b/locales/ru/common.json index 9a370ddfd..8dfc3ffcd 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -3283,6 +3283,8 @@ "rename": "Переименовать", "rename_title": "Переименовать", "new_name": "Новое название", + "name_forbidden_chars": "Сервер не допускает эти символы в именах: {chars}", + "name_reserved": "Это имя зарезервировано на сервере.", "delete": "Удалить", "delete_confirm_title": "Удалить ресурс", "delete_confirm_message": "Вы уверены, что хотите удалить «{name}»? Это действие нельзя отменить.", diff --git a/locales/sk/common.json b/locales/sk/common.json index 475d81b7e..c9ebc9922 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -3325,6 +3325,8 @@ "rename": "Premenovať", "rename_title": "Premenovať", "new_name": "Nový názov", + "name_forbidden_chars": "Server nepovoľuje v názvoch tieto znaky: {chars}", + "name_reserved": "Tento názov je na serveri vyhradený.", "delete": "Odstrániť", "delete_confirm_title": "Odstrániť položku", "delete_confirm_message": "Naozaj chcete odstrániť \"{name}\"? Túto akciu nie je možné vrátiť.", diff --git a/locales/tr/common.json b/locales/tr/common.json index acc0b446d..bf7241ff7 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -3325,6 +3325,8 @@ "rename": "Yeniden Adlandır", "rename_title": "Yeniden Adlandır", "new_name": "Yeni ad", + "name_forbidden_chars": "Sunucu adlarda şu karakterlere izin vermiyor: {chars}", + "name_reserved": "Bu ad sunucuda ayrılmış.", "delete": "Sil", "delete_confirm_title": "Kaynağı sil", "delete_confirm_message": "\"{name}\" silinsin mi? Bu geri alınamaz.", diff --git a/locales/uk/common.json b/locales/uk/common.json index cc9f0abed..88839f064 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -3283,6 +3283,8 @@ "rename": "Перейменувати", "rename_title": "Перейменувати", "new_name": "Нова назва", + "name_forbidden_chars": "Сервер не дозволяє ці символи в назвах: {chars}", + "name_reserved": "Ця назва зарезервована на сервері.", "delete": "Видалити", "delete_confirm_title": "Видалити ресурс", "delete_confirm_message": "Ви впевнені, що хочете видалити \"{name}\"? Це неможливо скасувати.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 507bdc3f3..3c3952650 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -3325,6 +3325,8 @@ "rename": "重新命名", "rename_title": "重新命名", "new_name": "新名稱", + "name_forbidden_chars": "伺服器不允許名稱中包含這些字元:{chars}", + "name_reserved": "此名稱已被伺服器保留。", "delete": "刪除", "delete_confirm_title": "刪除項目", "delete_confirm_message": "您確定要刪除「{name}」嗎?此動作無法復原。", diff --git a/locales/zh/common.json b/locales/zh/common.json index c6989b8cf..27379cea8 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -3283,6 +3283,8 @@ "rename": "重命名", "rename_title": "重命名", "new_name": "新名称", + "name_forbidden_chars": "服务器不允许名称中包含这些字符:{chars}", + "name_reserved": "此名称已被服务器保留。", "delete": "删除", "delete_confirm_title": "删除资源", "delete_confirm_message": "您确定要删除\"{name}\"吗?此操作无法撤消。",