//! Lightweight i18n: one central registry (in the spirit of `icons.rs`). //! //! `EN` is the canonical list of user-visible strings; it doubles as the key //! table. `DE` and `FR` translate the same keys; a key missing from a //! translation falls back to English. //! //! - `t(key)` resolves a key for the current locale. Non-reactive — for //! toasts, event handlers, and one-shot constructions (dialogs). //! - `tr(key)` is the same as a reactive closure, for `view!` children and //! attributes, so switching the language re-renders in place. //! - `error_text(code, fallback)` maps a server error *code* to a //! translation; unknown or absent codes fall back to the server message. //! //! Parameterized strings carry exactly one `{}` placeholder; callers do //! `format!(t(k::X), arg)`. //! //! Every table is sorted by key so lookups can binary-search. //! //! Locale precedence: the signed-in user's profile setting (persisted //! server-side), else the browser language. `init` + `set_locale` drive a //! global signal so every `tr` subscriber updates. use std::sync::OnceLock; use leptos::prelude::*; use wasm_bindgen::JsCast; /// A supported UI language. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Locale { En, De, Fr, } impl Locale { pub const fn code(self) -> &'static str { match self { Locale::En => "en", Locale::De => "de", Locale::Fr => "fr", } } /// Parse a BCP-47-ish tag ("de", "de-AT", "DE") into a supported locale. pub fn from_code(code: &str) -> Option { let lang = code.split(['-', '_']).next()?.to_ascii_lowercase(); match lang.as_str() { "en" => Some(Locale::En), "de" => Some(Locale::De), "fr" => Some(Locale::Fr), _ => None, } } /// The browser's language, restricted to the supported locales. pub fn from_browser() -> Option { let lang = web_sys::window()?.navigator().language()?; Self::from_code(&lang) } } static READ: OnceLock> = OnceLock::new(); static WRITE: OnceLock> = OnceLock::new(); /// Called once during app setup, before the first UI render. pub fn init(locale: Locale) { let (read, write) = signal(locale); let _ = READ.set(read); let _ = WRITE.set(write); apply_doc_lang(locale); } pub fn set_locale(locale: Locale) { if let Some(w) = WRITE.get() { w.set(locale); } apply_doc_lang(locale); } pub fn current() -> Locale { READ.get().map(|r| r.get()).unwrap_or(Locale::En) } /// Like [`current`] but without registering a reactive dependency — for /// code that reads the locale only to compare before writing (e.g. the /// profile effect in `app.rs`), so the read cannot retrigger itself. pub fn current_untracked() -> Locale { READ.get().map(|r| r.get_untracked()).unwrap_or(Locale::En) } /// Keep `` in sync for screen readers and the browser UI. fn apply_doc_lang(locale: Locale) { if let Some(html) = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.document_element()) .and_then(|e| e.dyn_into::().ok()) { let _ = html.set_attribute("lang", locale.code()); } } /// Resolve a key for the current locale (non-reactive). pub fn t(key: &str) -> &'static str { text(key, current()) } /// Resolve a key for the current locale as a reactive closure, for use as a /// `view!` child or attribute value. pub fn tr(key: &str) -> impl Fn() -> String + Clone { move || t(key).to_string() } /// Like [`t`], but substitutes a single `{}` placeholder in the translation /// with `arg`. pub fn t_fmt(key: &str, arg: &str) -> String { t(key).replace("{}", arg) } /// Like [`t_fmt`] for translations with two placeholders, filling the first /// `{}` with `a` and the second with `b`. Positional, so a locale that needs /// the other order has to reword rather than swap. pub fn t_fmt2(key: &str, a: &str, b: &str) -> String { let s = t(key); match s.split_once("{}") { Some((head, tail)) => format!("{head}{a}{}", tail.replacen("{}", b, 1)), None => s.to_string(), } } /// Localize a server error: a known code maps to its translation, anything /// else (no code, unknown code) falls back to the server's English message. pub fn error_text(code: Option<&str>, fallback: &str) -> String { let Some(code) = code.filter(|c| find(EN, c).is_some()) else { return fallback.to_string(); }; // One-shot lookup for a toast message: no reactive context, so read the // locale untracked. text(code, current_untracked()).to_string() } fn text(key: &str, locale: Locale) -> &'static str { // Keys are compile-time constants from `k`; a miss is a bug (the unit // tests pin the tables against each other). match locale { Locale::En => find(EN, key).expect("unknown i18n key"), Locale::De => find(DE, key) .or_else(|| find(EN, key)) .expect("unknown i18n key"), Locale::Fr => find(FR, key) .or_else(|| find(EN, key)) .expect("unknown i18n key"), } } /// The tables are sorted by key, so a lookup is a binary search. The /// `tables_are_sorted` test keeps that invariant true. fn find(table: &'static [(&'static str, &'static str)], key: &str) -> Option<&'static str> { table .binary_search_by(|(k, _)| (*k).cmp(key)) .ok() .map(|i| table[i].1) } /// Declares the translation keys and the canonical English text in one /// place: one row per key, `CONST = "key" => "English"`. /// /// The `k` constants and the `EN` table are generated from the same row, so /// a key cannot exist in one without the other. Rows are sorted by key, /// because [`find`] binary-searches (`tables_are_sorted` pins that). macro_rules! i18n_keys { ($($name:ident = $key:literal => $en:expr),* $(,)?) => { /// Translation keys. Use these, never the raw strings: a typo in a /// key is a compile error, a typo in a string is a runtime panic. pub mod k { $(pub const $name: &str = $key;)* } /// Canonical English strings (key → text). The source of truth for /// which keys exist; DE and FR translate the same list. const EN: &[(&str, &str)] = &[$(($key, $en)),*]; }; } i18n_keys! { ACTIVE_CAN_SIGNIN = "active_can_signin" => "Active (can sign in)", ADD_FOLDER = "add_folder" => "Add folder…", ADD_HERE = "add_here" => "Add this folder", ADMINISTRATOR = "administrator" => "Administrator", ALLOW_EDITING = "allow_editing" => "Allow editing (read-write)", ALLOW_RW_SHARES = "allow_rw_shares" => "Allow writable shares", ALLOW_RW_SHARES_DESC = "allow_rw_shares_desc" => "Let users create read-write share links. Off by default.", AND_MORE = "and_more" => "…and {} more", CANCEL = "cancel" => "Cancel", CANT_DELETE_SELF = "cant_delete_self" => "You cannot delete yourself", CLOSE = "close" => "Close", CLOSE_ESC = "close_esc" => "Close (Esc)", COPIED = "copied" => "Copied", COPY = "copy" => "Copy", COPY_DAV_LINK = "copy_dav_link" => "Copy WebDAV link", COPY_HERE = "copy_here" => "Copy here", COPY_LINK = "copy_link" => "Copy link", COPY_MANUAL = "copy_manual" => "Copy the link manually", COPY_N_TO = "copy_n_to" => "Copy {} items to…", COPY_TO = "copy_to" => "Copy “{}” to…", CREATE = "create" => "Create", CREATE_ADMIN = "create_admin" => "Create admin account", CREATE_LINK = "create_link" => "Create link", CREATE_USER = "create_user" => "Create user", CREATING = "creating" => "Creating…", CUSTOM = "custom" => "Custom…", DELETE = "delete" => "Delete", DELETE_ALL = "delete_all" => "Delete all", DELETE_ALL_MSG = "delete_all_msg" => "Delete all {} share links? Anyone holding them loses access. This cannot be undone.", DELETE_ALL_TITLE = "delete_all_title" => "Delete all shares?", DELETE_FOLDER_MSG = "delete_folder_msg" => "Delete folder “{}” and everything inside it?\nThis cannot be undone.", DELETE_MSG = "delete_msg" => "Delete “{}”?\nThis cannot be undone.", DELETE_N_MSG = "delete_n_msg" => "Delete {} items?\nThis cannot be undone.", DELETE_PARTIAL = "delete_partial" => "{} of {} shares could not be deleted", DELETE_SHARE = "delete_share" => "Delete share", DELETE_USER = "delete_user" => "Delete user", DELETE_USER_CONFIRM = "delete_user_confirm" => "Delete user “{}”? This cannot be undone.", DELETED = "deleted" => "Deleted", DELETED_N_SHARES = "deleted_n_shares" => "Deleted {} shares", DISCARD = "discard" => "Discard", DISCARD_QUESTION = "discard_question" => "Discard your changes and close the editor?", DISMISS = "dismiss" => "Dismiss", DISMISS_ERROR = "dismiss_error" => "Dismiss error", DONE = "done" => "Done", DOWNLOAD = "download" => "Download", DOWNLOAD_FORMATS_MSG = "download_formats_msg" => "Choose an archive format for the folder.", DOWNLOAD_STARTED = "download_started" => "Download started", DOWNLOAD_TITLE = "download_title" => "Download “{}”", EDIT = "edit" => "Edit", EDIT_USER_TITLE = "edit_user_title" => "Edit “{}”", EMPTY_FOLDER = "empty_folder" => "This folder is empty.", ERR_ADMIN_ONLY = "err_admin_only" => "admin only", ERR_ALREADY_SET_UP = "err_already_set_up" => "server is already set up", ERR_BAD_ACTION = "err_bad_action" => "expected action=content", ERR_BAD_BODY = "err_bad_body" => "invalid request body", ERR_BAD_EXPIRES_AT = "err_bad_expires_at" => "the expiry date is invalid", ERR_BAD_FORMAT = "err_bad_format" => "format must be one of: zip, tar, tar.gz, tar.zst", ERR_BAD_MULTIPART = "err_bad_multipart" => "expected multipart/form-data with a boundary", ERR_BAD_PART_NAME = "err_bad_part_name" => "invalid part name", ERR_BAD_POST = "err_bad_post" => "POST expects a multipart upload, a JSON mutation, or ?action=mkdir/create-file", ERR_BAD_UPLOAD = "err_bad_upload" => "invalid upload data", ERR_BAD_UPLOAD_PATH = "err_bad_upload_path" => "invalid file path in upload", ERR_DST_REQUIRED = "err_dst_required" => "dst_root_id is required", ERR_FILE_NAME_REQUIRED = "err_file_name_required" => "a file name is required", ERR_FILES_EXIST = "err_files_exist" => "some files already exist", ERR_FOLDER_EXISTS = "err_folder_exists" => "a folder with this name already exists", ERR_FOLDER_NAME_REQUIRED = "err_folder_name_required" => "a folder name is required", ERR_FS_CONFLICT = "err_fs_conflict" => "already exists", ERR_FS_FORBIDDEN = "err_fs_forbidden" => "access denied", ERR_FS_NOT_A_DIR = "err_fs_not_a_dir" => "not a folder", ERR_FS_NOT_FOUND = "err_fs_not_found" => "folder not found", ERR_FS_ROOT_MISSING = "err_fs_root_missing" => "the configured folder no longer exists", ERR_INTERNAL = "err_internal" => "internal error", ERR_INVALID_CREDENTIALS = "err_invalid_credentials" => "invalid name or password", ERR_INVALID_LANGUAGE = "err_invalid_language" => "invalid language tag", ERR_LAST_ADMIN = "err_last_admin" => "cannot remove the last active admin", ERR_LAST_ADMIN_DELETE = "err_last_admin_delete" => "cannot delete the last active admin", ERR_NAME_LENGTH = "err_name_length" => "name must be 1–64 characters", ERR_NEW_NAME_REQUIRED = "err_new_name_required" => "new_name is required", ERR_NO_FILES_UPLOADED = "err_no_files_uploaded" => "no files were uploaded", ERR_NO_SUCH_FOLDER = "err_no_such_folder" => "no such folder", ERR_NOT_A_FILE = "err_not_a_file" => "not a file", ERR_NOT_SIGNED_IN = "err_not_signed_in" => "not signed in", ERR_OWN_ACCOUNT = "err_own_account" => "you cannot disable your own account", ERR_OWN_ADMIN = "err_own_admin" => "you cannot remove your own admin rights", ERR_OWN_DELETE = "err_own_delete" => "you cannot delete your own account", ERR_PART_NO_NAME = "err_part_no_name" => "part without a name", ERR_PASSWORD_SHORT = "err_password_short" => "password must be at least 8 characters", ERR_PATH_REQUIRED = "err_path_required" => "a path inside the folder is required", ERR_READ_ONLY_FOLDER = "err_read_only_folder" => "read-only folder", ERR_ROOT_FORBIDDEN = "err_root_forbidden" => "root not accessible", ERR_RW_RO_FOLDER = "err_rw_ro_folder" => "this folder is read-only for you, so it cannot be shared writably", ERR_RW_SHARES_DISABLED = "err_rw_shares_disabled" => "writable shares are disabled", ERR_SAVE_FAILED = "err_save_failed" => "could not save the file", ERR_SEARCH_BAD_SCOPE = "err_search_bad_scope" => "scope must be name, content or both", ERR_SEARCH_FORBIDDEN = "err_search_forbidden" => "search requires a signed-in session", ERR_SESSION_EXPIRED = "err_session_expired" => "session expired, please sign in again", ERR_SHARE_EXPIRED = "err_share_expired" => "this share has expired", ERR_SHARE_LOCKED = "err_share_locked" => "This share is password protected.", ERR_SHARE_NO_PASSWORD = "err_share_no_password" => "This share has no password.", ERR_SHARE_NOT_FOUND = "err_share_not_found" => "share not found", ERR_SHARE_TOKEN_FORBIDDEN = "err_share_token_forbidden" => "a share token cannot be used here; sign in instead", ERR_SHARE_WRONG_PASSWORD = "err_share_wrong_password" => "Wrong password.", ERR_TOO_LARGE_PREVIEW = "err_too_large_preview" => "file too large to preview", ERR_TOO_LARGE_SAVE = "err_too_large_save" => "file too large to save", ERR_USER_EXISTS = "err_user_exists" => "a user with that name already exists", ERR_USER_NOT_FOUND = "err_user_not_found" => "user not found", EXCLUDE_DUP_ERR = "exclude_dup_err" => "That folder is already excluded.", EXCLUDE_FOLDER = "exclude_folder" => "Exclude folder…", EXCLUDE_HERE = "exclude_here" => "Exclude this folder", EXCLUDE_ROOT_ERR = "exclude_root_err" => "The whole root cannot be excluded; that would turn search off.", EXPIRES = "expires" => "Expires", EXPIRES_AT = "expires_at" => "Expires {}", EXPIRY_1D = "expiry_1d" => "1 day", EXPIRY_1H = "expiry_1h" => "1 hour", EXPIRY_1MO = "expiry_1mo" => "1 month", EXPIRY_1W = "expiry_1w" => "1 week", FILE = "file" => "File", FILE_CHANGED = "file_changed" => "File changed on disk", FILE_CREATED = "file_created" => "File created", FILE_INFO = "file_info" => "File info", FILE_NAME = "file_name" => "File name", FILES = "files" => "Files", FMT_TAR = "fmt_tar" => "Tar", FMT_TARGZ = "fmt_targz" => "Tar + gzip", FMT_TARZST = "fmt_tarzst" => "Tar + zstd", FMT_ZIP = "fmt_zip" => "Zip", FOLDER = "folder" => "Folder", FOLDER_CREATED = "folder_created" => "Folder created", FOLDER_GONE_MSG = "folder_gone_msg" => "This folder is no longer accessible. Choose another one from the sidebar.", FOLDER_GONE_TITLE = "folder_gone_title" => "Folder not available", FOLDER_NAME = "folder_name" => "Folder name", FOLDERS = "folders" => "Folders", FOLDERS_PATHS_HINT = "folders_paths_hint" => "Paths are relative to the server root. “.” is the whole root.", GRID_VIEW = "grid_view" => "Grid view", INFO = "info" => "Info", KEEP_EDITING = "keep_editing" => "Keep editing", LANG_AUTO = "lang_auto" => "Automatic", LANG_DE = "lang_de" => "German", LANG_EN = "lang_en" => "English", LANG_FR = "lang_fr" => "French", LANGUAGE = "language" => "Language", LANGUAGE_DESC = "language_desc" => "The interface language. Automatic uses your browser's language.", LINK_COPIED = "link_copied" => "Link copied to clipboard", LIST_TRUNCATED = "list_truncated" => "Only the first {} entries are shown.", LIST_VIEW = "list_view" => "List view", LOAD_LATEST = "load_latest" => "Load latest", LOADING = "loading" => "Loading…", LOADING_SHARE = "loading_share" => "Loading share…", LOCATION = "location" => "Location", LOG_OUT = "log_out" => "Log out", LOGIN_ERROR = "login_error" => "Please enter your name and password.", LOGIN_SUBTITLE = "login_subtitle" => "Sign in to continue.", MANAGE = "manage" => "Manage", MENU = "menu" => "Menu", META_CREATED = "meta_created" => "created {}", META_EXPIRES = "meta_expires" => "expires {}", MODE_RO = "mode_ro" => "read-only", MODE_RW = "mode_rw" => "read-write", MODIFIED = "modified" => "Modified", MODIFIED_AFTER = "modified_after" => "This file was modified after you opened it. “Load latest” discards your unsaved changes; “Overwrite” keeps your version.", MORE_ACTIONS = "more_actions" => "More actions", MOVE = "move" => "Move", MOVE_HERE = "move_here" => "Move here", MOVE_N_TO = "move_n_to" => "Move {} items to…", MOVE_TO = "move_to" => "Move “{}” to…", MOVED = "moved" => "Moved", NAME = "name" => "Name", NAME_REQUIRED = "name_required" => "A name is required.", NAME_TAKEN = "name_taken" => "Name already in use", NEED_FOLDER = "need_folder" => "Give the user at least one folder.", NEVER = "never" => "Never", NEVER_EXPIRES = "never_expires" => "Never expires", NEW_FILE = "new_file" => "New file", NEW_FOLDER = "new_folder" => "New folder", NEW_NAME = "new_name" => "New name", NEW_PASSWORD_KEEP = "new_password_keep" => "New password (leave blank to keep)", NEW_USER = "new_user" => "New user", NO_FOLDERS_MSG = "no_folders_msg" => "An administrator hasn't assigned you any folders yet.", NO_FOLDERS_TITLE = "no_folders_title" => "No folders available", NO_PREVIEW = "no_preview" => "No preview available", NO_PREVIEW_MSG = "no_preview_msg" => "The app can’t display “{}” here.", NO_SHARES = "no_shares" => "No shares yet", NO_SUBFOLDERS = "no_subfolders" => "No subfolders", NO_USERS = "no_users" => "No users.", OPEN = "open" => "Open", OPEN_IN_NEW_TAB = "open_in_new_tab" => "Open in new tab", OPEN_MENU = "open_menu" => "Open menu", OVERWRITE = "overwrite" => "Overwrite", OVERWRITE_QUESTION = "overwrite_question" => "The following files already exist. Overwrite them?", PASSWORD = "password" => "Password", PROFILE = "profile" => "Profile", PROFILE_SAVE_ERR = "profile_save_err" => "Could not save profile: {}", PROFILE_SAVED = "profile_saved" => "Profile saved", PW_SHORT = "pw_short" => "Password must be at least 8 characters.", READ_ONLY_BADGE = "read_only_badge" => "Read-only", READ_ONLY_HINT = "read_only_hint" => "Read-only folder", REFRESH = "refresh" => "Refresh", REFRESH_FOLDERS = "refresh_folders" => "Refresh your folders", REMOVE_FOLDER = "remove_folder" => "Remove folder", RENAME = "rename" => "Rename", RENAME_TITLE = "rename_title" => "Rename “{}”", RENAMED = "renamed" => "Renamed", REPEAT_PASSWORD = "repeat_password" => "Repeat password", RETRY = "retry" => "Retry", ROOT_DUP_ERR = "root_dup_err" => "That folder is already in the list.", RW_DISABLED = "rw_disabled" => "Writable shares are disabled by the server.", RW_RO_FOLDER = "rw_ro_folder" => "This folder is read-only, so the share is read-only too.", SAVE = "save" => "Save", SAVE_PROFILE = "save_profile" => "Save profile", SAVE_SETTINGS = "save_settings" => "Save settings", SAVED = "saved" => "Saved", SAVING = "saving" => "Saving…", SEARCH = "search" => "Search", SEARCH_BOTH = "search_both" => "Both", SEARCH_COLLAPSE_ALL = "search_collapse_all" => "Collapse all", SEARCH_CONTENT = "search_content" => "Content", SEARCH_EXCLUDES = "search_excludes" => "Excluded from search", SEARCH_EXCLUDES_EMPTY = "search_excludes_empty" => "No folders are excluded.", SEARCH_EXPAND_ALL = "search_expand_all" => "Expand all", SEARCH_FAILED = "search_failed" => "The search failed.", SEARCH_GOTO = "search_goto" => "Show in folder", SEARCH_HERE = "search_here" => "Search here", SEARCH_IN = "search_in" => "Search in…", SEARCH_IN_FOLDER = "search_in_folder" => "Search in this folder", SEARCH_IN_N_FILES = "search_in_n_files" => "in {} files", SEARCH_N_MATCHES = "search_n_matches" => "{} matches", SEARCH_N_RESULTS = "search_n_results" => "{} results", SEARCH_NAME = "search_name" => "Name", SEARCH_NO_RESULTS = "search_no_results" => "no results", SEARCH_PLACEHOLDER = "search_placeholder" => "Search files and their contents", SEARCH_RUN = "search_run" => "Search", SEARCH_SCANNED = "search_scanned" => "scanned {} files", SEARCH_SHOW_MORE = "search_show_more" => "Show {} more", SEARCH_SHOWN_OF = "search_shown_of" => "Showing {} of {}", SEARCH_SKIPPED = "search_skipped" => "{} skipped (over 10 MiB)", SEARCH_STOP = "search_stop" => "Stop", SEARCH_STOPPED = "search_stopped" => "stopped · {} results", SEARCH_TOOK = "search_took" => "{} ms", SEARCHING = "searching" => "searching…", SELECT_FOLDER_MSG = "select_folder_msg" => "Choose a folder from the sidebar to start browsing.", SELECT_FOLDER_TITLE = "select_folder_title" => "Select a folder", SERVER = "server" => "Server", SERVER_ROOT = "server_root" => "Server root", SERVER_UNREACHABLE = "server_unreachable" => "The server is not reachable.", SETTINGS = "settings" => "Settings", SETTINGS_SAVE_ERR = "settings_save_err" => "Could not save settings: {}", SETTINGS_SAVED = "settings_saved" => "Settings saved", SETUP_INTRO = "setup_intro" => "First boot: create the admin account. This user can manage other users and server settings.", SETUP_NAME_ERR = "setup_name_err" => "Please choose a name (1–64 characters).", SETUP_PW_MISMATCH = "setup_pw_mismatch" => "Passwords don't match.", SHARE = "share" => "Share", SHARE_CREATE_ERR = "share_create_err" => "Could not create share: {}", SHARE_DELETE_ERR = "share_delete_err" => "Could not delete share", SHARE_DELETED = "share_deleted" => "Share deleted", SHARE_DIALOG_HINT = "share_dialog_hint" => "Create a link for this item. Writable links let viewers upload and edit.", SHARE_EXPIRED = "share_expired" => "Share expired", SHARE_EXPIRED_MSG = "share_expired_msg" => "This share link is no longer active.", SHARE_LINK = "share_link" => "Share link", SHARE_LOCKED_TITLE = "share_locked_title" => "Password required", SHARE_NOT_FOUND = "share_not_found" => "Share not found", SHARE_NOT_FOUND_MSG = "share_not_found_msg" => "This link is invalid or the share was removed.", SHARE_PASSWORD_HINT = "share_password_hint" => "Optional. Visitors must enter it before the share opens, at least 8 characters.", SHARE_PERM_RO = "share_perm_ro" => "Anyone with this link can read “{}”.", SHARE_PERM_RW = "share_perm_rw" => "Anyone with this link can read and write “{}”.", SHARE_PROTECTED = "share_protected" => "Password protected", SHARE_TITLE = "share_title" => "Share {}", SHARES = "shares" => "Shares", SHARES_DELETE_ERR = "shares_delete_err" => "Could not delete shares", SHARES_EMPTY_HINT = "shares_empty_hint" => "Right-click a file or folder and choose “Share” to create a link.", SHARES_VIEW_HINT = "shares_view_hint" => "Links you created. Right-click a file or folder and choose “Share” to add one.", SIGN_IN = "sign_in" => "Sign in", SINGLE_CLICK_DESC = "single_click_desc" => "When on, a single click opens an entry and Ctrl+click selects it. When off, a single click selects and a double click opens.", SINGLE_CLICK_LABEL = "single_click_label" => "Use single click to open", SIZE = "size" => "Size", SOME_FILES_EXIST = "some_files_exist" => "Some files already exist", SORT_ASC = "sort_asc" => "Ascending", SORT_BY = "sort_by" => "Sort by", SORT_DESC = "sort_desc" => "Descending", THEME_AUTO = "theme_auto" => "Auto", THEME_DARK = "theme_dark" => "Dark", THEME_LIGHT = "theme_light" => "Light", THEME_TITLE = "theme_title" => "Theme: {} (click to switch)", THUMBNAILS_DESC = "thumbnails_desc" => "Show a small preview instead of an icon for images and videos in grid view.", THUMBNAILS_LABEL = "thumbnails_label" => "Show thumbnails", TYPE = "type" => "Type", UNLOCK = "unlock" => "Open share", UNSAVED_CHANGES = "unsaved_changes" => "Unsaved changes", UNTIL = "until" => "Until", UPLOAD_COMPLETE = "upload_complete" => "Upload complete", UPLOAD_FILES = "upload_files" => "Upload files", UPLOAD_FOLDER = "upload_folder" => "Upload folder", UPLOADING = "uploading" => "Uploading {} files…", USER_CREATED = "user_created" => "User created", USER_DELETE_ERR = "user_delete_err" => "Could not delete user", USER_SAVE_ERR = "user_save_err" => "Could not save user: {}", USER_UPDATED = "user_updated" => "User updated", USERS = "users" => "Users", YOUR_SHARES = "your_shares" => "Your shares", } /// German translations. const DE: &[(&str, &str)] = &[ ("active_can_signin", "Aktiv (kann sich anmelden)"), ("add_folder", "Ordner hinzufügen…"), ("add_here", "Diesen Ordner hinzufügen"), ("administrator", "Administrator"), ("allow_editing", "Bearbeitung erlauben (lesen/schreiben)"), ("allow_rw_shares", "Schreibbare Freigaben erlauben"), ( "allow_rw_shares_desc", "Benutzern erlauben, schreibbare Freigabelinks zu erstellen. Standardmäßig aus.", ), ("and_more", "…und {} weitere"), ("cancel", "Abbrechen"), ("cant_delete_self", "Sie können sich nicht selbst löschen"), ("close", "Schließen"), ("close_esc", "Schließen (Esc)"), ("copied", "Kopiert"), ("copy", "Kopieren"), ("copy_dav_link", "WebDAV-Link kopieren"), ("copy_here", "Hierher kopieren"), ("copy_link", "Link kopieren"), ("copy_manual", "Link manuell kopieren"), ("copy_n_to", "{} Elemente kopieren nach…"), ("copy_to", "„{}“ kopieren nach…"), ("create", "Erstellen"), ("create_admin", "Administrator-Konto erstellen"), ("create_link", "Link erstellen"), ("create_user", "Benutzer erstellen"), ("creating", "Wird erstellt…"), ("custom", "Benutzerdefiniert…"), ("delete", "Löschen"), ("delete_all", "Alle löschen"), ( "delete_all_msg", "Alle {} Freigabelinks löschen? Jeder, der sie besitzt, verliert den Zugriff. Das kann nicht rückgängig gemacht werden.", ), ("delete_all_title", "Alle Freigaben löschen?"), ( "delete_folder_msg", "Ordner „{}“ mit allem Inhalt löschen?\nDas kann nicht rückgängig gemacht werden.", ), ( "delete_msg", "„{}“ löschen?\nDas kann nicht rückgängig gemacht werden.", ), ( "delete_n_msg", "{} Elemente löschen?\nDas kann nicht rückgängig gemacht werden.", ), ( "delete_partial", "{} von {} Freigaben konnten nicht gelöscht werden", ), ("delete_share", "Freigabe löschen"), ("delete_user", "Benutzer löschen"), ( "delete_user_confirm", "Benutzer „{}“ löschen? Das kann nicht rückgängig gemacht werden.", ), ("deleted", "Gelöscht"), ("deleted_n_shares", "{} Freigabe(n) gelöscht"), ("discard", "Verwerfen"), ( "discard_question", "Änderungen verwerfen und den Editor schließen?", ), ("dismiss", "Ausblenden"), ("dismiss_error", "Fehler ausblenden"), ("done", "Fertig"), ("download", "Herunterladen"), ( "download_formats_msg", "Archivformat für den Ordner wählen.", ), ("download_started", "Download gestartet"), ("download_title", "„{}“ herunterladen"), ("edit", "Bearbeiten"), ("edit_user_title", "„{}“ bearbeiten"), ("empty_folder", "Dieser Ordner ist leer."), (k::ERR_ADMIN_ONLY, "nur für Administratoren"), (k::ERR_ALREADY_SET_UP, "Server ist bereits eingerichtet"), (k::ERR_BAD_ACTION, "action=content erwartet"), (k::ERR_BAD_BODY, "ungültiger Anfrageinhalt"), (k::ERR_BAD_EXPIRES_AT, "das Ablaufdatum ist ungültig"), ( k::ERR_BAD_FORMAT, "das Format muss eines der folgenden sein: zip, tar, tar.gz, tar.zst", ), ( k::ERR_BAD_MULTIPART, "multipart/form-data mit Boundary erwartet", ), (k::ERR_BAD_PART_NAME, "ungültiger Part-Name"), ( k::ERR_BAD_POST, "POST erwartet einen Multipart-Upload, eine JSON-Mutation oder ?action=mkdir/create-file", ), (k::ERR_BAD_UPLOAD, "ungültige Upload-Daten"), (k::ERR_BAD_UPLOAD_PATH, "ungültiger Dateipfad im Upload"), (k::ERR_DST_REQUIRED, "dst_root_id ist erforderlich"), (k::ERR_FILE_NAME_REQUIRED, "ein Dateiname ist erforderlich"), (k::ERR_FILES_EXIST, "einige Dateien existieren bereits"), ( k::ERR_FOLDER_EXISTS, "ein Ordner mit diesem Namen existiert bereits", ), ( k::ERR_FOLDER_NAME_REQUIRED, "ein Ordnername ist erforderlich", ), (k::ERR_FS_CONFLICT, "existiert bereits"), (k::ERR_FS_FORBIDDEN, "Zugriff verweigert"), (k::ERR_FS_NOT_A_DIR, "kein Ordner"), (k::ERR_FS_NOT_FOUND, "Ordner nicht gefunden"), ( k::ERR_FS_ROOT_MISSING, "der konfigurierte Ordner existiert nicht mehr", ), (k::ERR_INTERNAL, "interner Fehler"), (k::ERR_INVALID_CREDENTIALS, "Name oder Passwort ungültig"), (k::ERR_INVALID_LANGUAGE, "ungültiges Sprachkürzel"), ( k::ERR_LAST_ADMIN, "der letzte aktive Administrator kann nicht entfernt werden", ), ( k::ERR_LAST_ADMIN_DELETE, "der letzte aktive Administrator kann nicht gelöscht werden", ), (k::ERR_NAME_LENGTH, "der Name muss 1–64 Zeichen haben"), (k::ERR_NEW_NAME_REQUIRED, "new_name ist erforderlich"), (k::ERR_NO_FILES_UPLOADED, "keine Dateien hochgeladen"), (k::ERR_NO_SUCH_FOLDER, "dieser Ordner existiert nicht"), (k::ERR_NOT_A_FILE, "keine Datei"), (k::ERR_NOT_SIGNED_IN, "nicht angemeldet"), ( k::ERR_OWN_ACCOUNT, "Sie können Ihr eigenes Konto nicht deaktivieren", ), ( k::ERR_OWN_ADMIN, "Sie können Ihre eigenen Administratorrechte nicht entfernen", ), ( k::ERR_OWN_DELETE, "Sie können Ihr eigenes Konto nicht löschen", ), (k::ERR_PART_NO_NAME, "Part ohne Namen"), ( k::ERR_PASSWORD_SHORT, "das Passwort muss mindestens 8 Zeichen lang sein", ), ( k::ERR_PATH_REQUIRED, "ein Pfad innerhalb des Ordners ist erforderlich", ), (k::ERR_READ_ONLY_FOLDER, "schreibgeschützter Ordner"), (k::ERR_ROOT_FORBIDDEN, "kein Zugriff auf diesen Ordner"), ( k::ERR_RW_RO_FOLDER, "dieser Ordner ist für Sie schreibgeschützt und kann nicht schreibbar freigegeben werden", ), ( k::ERR_RW_SHARES_DISABLED, "schreibbare Freigaben sind deaktiviert", ), ( k::ERR_SAVE_FAILED, "die Datei konnte nicht gespeichert werden", ), ( k::ERR_SEARCH_BAD_SCOPE, "Bereich muss name, content oder both sein", ), ( k::ERR_SEARCH_FORBIDDEN, "Suche erfordert eine angemeldete Sitzung", ), ( k::ERR_SESSION_EXPIRED, "Sitzung abgelaufen, bitte erneut anmelden", ), (k::ERR_SHARE_EXPIRED, "diese Freigabe ist abgelaufen"), (k::ERR_SHARE_LOCKED, "Diese Freigabe ist passwortgeschützt."), ( k::ERR_SHARE_NO_PASSWORD, "Diese Freigabe hat kein Passwort.", ), (k::ERR_SHARE_NOT_FOUND, "Freigabe nicht gefunden"), ( k::ERR_SHARE_TOKEN_FORBIDDEN, "ein Freigabetoken kann hier nicht verwendet werden; bitte anmelden", ), (k::ERR_SHARE_WRONG_PASSWORD, "Falsches Passwort."), (k::ERR_TOO_LARGE_PREVIEW, "Datei zu groß für eine Vorschau"), (k::ERR_TOO_LARGE_SAVE, "Datei zu groß zum Speichern"), ( k::ERR_USER_EXISTS, "ein Benutzer mit diesem Namen existiert bereits", ), (k::ERR_USER_NOT_FOUND, "Benutzer nicht gefunden"), ( "exclude_dup_err", "Dieser Ordner ist bereits ausgeschlossen.", ), ("exclude_folder", "Ordner ausschließen…"), ("exclude_here", "Diesen Ordner ausschließen"), ( "exclude_root_err", "Das gesamte Stammverzeichnis kann nicht ausgeschlossen werden; das würde die Suche abschalten.", ), ("expires", "Läuft ab"), ("expires_at", "Läuft ab: {}"), ("expiry_1d", "1 Tag"), ("expiry_1h", "1 Stunde"), ("expiry_1mo", "1 Monat"), ("expiry_1w", "1 Woche"), ("file", "Datei"), ("file_changed", "Datei auf der Festplatte geändert"), ("file_created", "Datei erstellt"), ("file_info", "Datei-Info"), ("file_name", "Dateiname"), ("files", "Dateien"), ("fmt_tar", "Tar"), ("fmt_targz", "Tar + gzip"), ("fmt_tarzst", "Tar + zstd"), ("fmt_zip", "Zip"), ("folder", "Ordner"), ("folder_created", "Ordner erstellt"), ( "folder_gone_msg", "Dieser Ordner ist nicht mehr verfügbar. Wählen Sie links einen anderen aus.", ), ("folder_gone_title", "Ordner nicht verfügbar"), ("folder_name", "Ordnername"), ("folders", "Ordner"), ( "folders_paths_hint", "Pfade sind relativ zur Serverwurzel. „.“ ist der gesamte Wurzelordner.", ), ("grid_view", "Kachelansicht"), ("info", "Info"), ("keep_editing", "Weiter bearbeiten"), ("lang_auto", "Automatisch"), ("lang_de", "Deutsch"), ("lang_en", "Englisch"), ("lang_fr", "Französisch"), ("language", "Sprache"), ( "language_desc", "Die Sprache der Benutzeroberfläche. „Automatisch“ verwendet die Sprache Ihres Browsers.", ), ("link_copied", "Link in die Zwischenablage kopiert"), ( "list_truncated", "Nur die ersten {} Einträge werden angezeigt.", ), ("list_view", "Listenansicht"), ("load_latest", "Neueste Version laden"), ("loading", "Wird geladen…"), ("loading_share", "Freigabe wird geladen…"), ("location", "Ort"), ("log_out", "Abmelden"), ("login_error", "Bitte Name und Passwort eingeben."), ("login_subtitle", "Anmelden, um fortzufahren."), ("manage", "Verwalten"), ("menu", "Menü"), ("meta_created", "erstellt: {}"), ("meta_expires", "läuft ab: {}"), ("mode_ro", "nur lesen"), ("mode_rw", "lesen/schreiben"), ("modified", "Geändert"), ( "modified_after", "Diese Datei wurde nach dem Öffnen geändert. „Neueste Version laden“ verwirft Ihre ungespeicherten Änderungen; „Überschreiben“ behält Ihre Version bei.", ), ("more_actions", "Weitere Aktionen"), ("move", "Verschieben"), ("move_here", "Hierher verschieben"), ("move_n_to", "{} Elemente verschieben nach…"), ("move_to", "„{}“ verschieben nach…"), ("moved", "Verschoben"), ("name", "Name"), ("name_required", "Ein Name ist erforderlich."), ("name_taken", "Name wird bereits verwendet"), ( "need_folder", "Dem Benutzer muss mindestens ein Ordner zugewiesen werden.", ), ("never", "Nie"), ("never_expires", "Läuft nie ab"), ("new_file", "Neue Datei"), ("new_folder", "Neuer Ordner"), ("new_name", "Neuer Name"), ( "new_password_keep", "Neues Passwort (leer lassen, um beizubehalten)", ), ("new_user", "Neuer Benutzer"), ( "no_folders_msg", "Ein Administrator hat Ihnen noch keine Ordner zugewiesen.", ), ("no_folders_title", "Keine Ordner verfügbar"), ("no_preview", "Keine Vorschau verfügbar"), ("no_preview_msg", "Die App kann „{}“ hier nicht anzeigen."), ("no_shares", "Noch keine Freigaben"), ("no_subfolders", "Keine Unterordner"), ("no_users", "Keine Benutzer."), ("open", "Öffnen"), ("open_in_new_tab", "In neuem Tab öffnen"), ("open_menu", "Menü öffnen"), ("overwrite", "Überschreiben"), ( "overwrite_question", "Die folgenden Dateien existieren bereits. Überschreiben?", ), ("password", "Passwort"), ("profile", "Profil"), ( "profile_save_err", "Profil konnte nicht gespeichert werden: {}", ), ("profile_saved", "Profil gespeichert"), ( "pw_short", "Das Passwort muss mindestens 8 Zeichen lang sein.", ), ("read_only_badge", "Schreibgeschützt"), ("read_only_hint", "Schreibgeschützter Ordner"), ("refresh", "Aktualisieren"), ("refresh_folders", "Ordner aktualisieren"), ("remove_folder", "Ordner entfernen"), ("rename", "Umbenennen"), ("rename_title", "„{}“ umbenennen"), ("renamed", "Umbenannt"), ("repeat_password", "Passwort wiederholen"), ("retry", "Erneut versuchen"), ("root_dup_err", "Dieser Ordner ist bereits in der Liste."), ( "rw_disabled", "Schreibbare Freigaben sind vom Server deaktiviert.", ), ( "rw_ro_folder", "Dieser Ordner ist schreibgeschützt, daher ist die Freigabe auch schreibgeschützt.", ), ("save", "Speichern"), ("save_profile", "Profil speichern"), ("save_settings", "Einstellungen speichern"), ("saved", "Gespeichert"), ("saving", "Wird gespeichert…"), ("search", "Suche"), ("search_both", "Beides"), ("search_collapse_all", "Alle einklappen"), ("search_content", "Inhalt"), ("search_excludes", "Von der Suche ausgeschlossen"), ("search_excludes_empty", "Keine Ordner ausgeschlossen."), ("search_expand_all", "Alle ausklappen"), ("search_failed", "Die Suche ist fehlgeschlagen."), ("search_goto", "Im Ordner anzeigen"), ("search_here", "Hier suchen"), ("search_in", "Suchen in…"), ("search_in_folder", "In diesem Ordner suchen"), ("search_in_n_files", "in {} Dateien"), ("search_n_matches", "{} Treffer"), ("search_n_results", "{} Treffer"), ("search_name", "Name"), ("search_no_results", "keine Treffer"), ( "search_placeholder", "Dateien und deren Inhalte durchsuchen", ), ("search_run", "Suchen"), ("search_scanned", "{} Dateien durchsucht"), ("search_show_more", "{} weitere anzeigen"), ("search_shown_of", "{} von {} angezeigt"), ("search_skipped", "{} übersprungen (über 10 MiB)"), ("search_stop", "Stopp"), ("search_stopped", "gestoppt · {} Treffer"), ("search_took", "{} ms"), ("searching", "Suche läuft…"), ( "select_folder_msg", "Wählen Sie links einen Ordner aus, um mit dem Durchsuchen zu beginnen.", ), ("select_folder_title", "Ordner auswählen"), ("server", "Server"), ("server_root", "Serverwurzel"), ("server_unreachable", "Der Server ist nicht erreichbar."), ("settings", "Einstellungen"), ( "settings_save_err", "Einstellungen konnten nicht gespeichert werden: {}", ), ("settings_saved", "Einstellungen gespeichert"), ( "setup_intro", "Erster Start: Erstellen Sie das Administrator-Konto. Dieser Benutzer kann andere Benutzer und die Servereinstellungen verwalten.", ), ( "setup_name_err", "Bitte wählen Sie einen Namen (1–64 Zeichen).", ), ("setup_pw_mismatch", "Die Passwörter stimmen nicht überein."), ("share", "Teilen"), ( "share_create_err", "Freigabe konnte nicht erstellt werden: {}", ), ("share_delete_err", "Freigabe konnte nicht gelöscht werden"), ("share_deleted", "Freigabe gelöscht"), ( "share_dialog_hint", "Erstellt einen Link für diesen Eintrag. Mit schreibbaren Links können Empfänger Dateien hochladen und bearbeiten.", ), ("share_expired", "Freigabe abgelaufen"), ( "share_expired_msg", "Dieser Freigabelink ist nicht mehr aktiv.", ), ("share_link", "Freigabelink"), ("share_locked_title", "Passwort erforderlich"), ("share_not_found", "Freigabe nicht gefunden"), ( "share_not_found_msg", "Dieser Link ist ungültig oder die Freigabe wurde entfernt.", ), ( "share_password_hint", "Optional. Besucher müssen es eingeben, bevor die Freigabe geöffnet wird, mindestens 8 Zeichen.", ), ("share_perm_ro", "Jeder mit diesem Link kann „{}“ lesen."), ( "share_perm_rw", "Jeder mit diesem Link kann „{}“ lesen und schreiben.", ), ("share_protected", "Passwortgeschützt"), ("share_title", "{} teilen"), ("shares", "Freigaben"), ( "shares_delete_err", "Freigaben konnten nicht gelöscht werden", ), ( "shares_empty_hint", "Klicken Sie mit der rechten Maustaste auf eine Datei oder einen Ordner und wählen Sie „Teilen“, um einen Link zu erstellen.", ), ( "shares_view_hint", "Von Ihnen erstellte Links. Klicken Sie mit der rechten Maustaste auf eine Datei oder einen Ordner und wählen Sie „Teilen“, um einen hinzuzufügen.", ), ("sign_in", "Anmelden"), ( "single_click_desc", "Wenn aktiv, öffnet ein einfacher Klick einen Eintrag und Strg+Klick wählt ihn aus. Wenn inaktiv, wählt ein einfacher Klick aus und ein Doppelklick öffnet.", ), ("single_click_label", "Einzelklick öffnet Einträge"), ("size", "Größe"), ("some_files_exist", "Einige Dateien existieren bereits"), ("sort_asc", "Aufsteigend"), ("sort_by", "Sortieren nach"), ("sort_desc", "Absteigend"), ("theme_auto", "Auto"), ("theme_dark", "Dunkel"), ("theme_light", "Hell"), ("theme_title", "Design: {} (klicken zum Wechseln)"), ( "thumbnails_desc", "Für Bilder und Videos in der Kachelansicht eine kleine Vorschau statt eines Symbols zeigen.", ), ("thumbnails_label", "Vorschaubilder zeigen"), ("type", "Typ"), ("unlock", "Freigabe öffnen"), ("unsaved_changes", "Ungespeicherte Änderungen"), ("until", "Bis"), ("upload_complete", "Hochladen abgeschlossen"), ("upload_files", "Dateien hochladen"), ("upload_folder", "Ordner hochladen"), ("uploading", "{} Datei(en) werden hochgeladen…"), ("user_created", "Benutzer erstellt"), ("user_delete_err", "Benutzer konnte nicht gelöscht werden"), ( "user_save_err", "Benutzer konnte nicht gespeichert werden: {}", ), ("user_updated", "Benutzer aktualisiert"), ("users", "Benutzer"), ("your_shares", "Ihre Freigaben"), ]; /// French translations. const FR: &[(&str, &str)] = &[ ("active_can_signin", "Actif (peut se connecter)"), ("add_folder", "Ajouter un dossier…"), ("add_here", "Ajouter ce dossier"), ("administrator", "Administrateur"), ( "allow_editing", "Autoriser la modification (lecture-écriture)", ), ("allow_rw_shares", "Autoriser les partages modifiables"), ( "allow_rw_shares_desc", "Autoriser les utilisateurs à créer des liens de partage en lecture-écriture. Désactivé par défaut.", ), ("and_more", "…et {} autres"), ("cancel", "Annuler"), ( "cant_delete_self", "Vous ne pouvez pas supprimer votre propre compte", ), ("close", "Fermer"), ("close_esc", "Fermer (Échap)"), ("copied", "Copié"), ("copy", "Copier"), ("copy_dav_link", "Copier le lien WebDAV"), ("copy_here", "Copier ici"), ("copy_link", "Copier le lien"), ("copy_manual", "Copiez le lien manuellement"), ("copy_n_to", "Copier {} éléments vers…"), ("copy_to", "Copier « {} » vers…"), ("create", "Créer"), ("create_admin", "Créer le compte administrateur"), ("create_link", "Créer le lien"), ("create_user", "Créer l'utilisateur"), ("creating", "Création…"), ("custom", "Personnalisé…"), ("delete", "Supprimer"), ("delete_all", "Tout supprimer"), ( "delete_all_msg", "Supprimer les {} liens de partage ? Toute personne les possédant perdra l'accès. Cette action est irréversible.", ), ("delete_all_title", "Supprimer tous les partages ?"), ( "delete_folder_msg", "Supprimer le dossier « {} » et tout son contenu ?\nCette action est irréversible.", ), ( "delete_msg", "Supprimer « {} » ?\nCette action est irréversible.", ), ( "delete_n_msg", "Supprimer {} éléments ?\nCette action est irréversible.", ), ( "delete_partial", "{} des {} liens n'ont pas pu être supprimés", ), ("delete_share", "Supprimer le partage"), ("delete_user", "Supprimer l'utilisateur"), ( "delete_user_confirm", "Supprimer l'utilisateur « {} » ? Cette action est irréversible.", ), ("deleted", "Supprimé"), ("deleted_n_shares", "{} partage(s) supprimé(s)"), ("discard", "Abandonner"), ( "discard_question", "Abandonner vos modifications et fermer l'éditeur ?", ), ("dismiss", "Ignorer"), ("dismiss_error", "Ignorer l'erreur"), ("done", "Terminé"), ("download", "Télécharger"), ( "download_formats_msg", "Choisissez un format d'archivage pour le dossier.", ), ("download_started", "Téléchargement démarré"), ("download_title", "Télécharger « {} »"), ("edit", "Modifier"), ("edit_user_title", "Modifier « {} »"), ("empty_folder", "Ce dossier est vide."), (k::ERR_ADMIN_ONLY, "réservé aux administrateurs"), (k::ERR_ALREADY_SET_UP, "le serveur est déjà configuré"), (k::ERR_BAD_ACTION, "action=content attendu"), (k::ERR_BAD_BODY, "corps de requête invalide"), (k::ERR_BAD_EXPIRES_AT, "la date d'expiration est invalide"), ( k::ERR_BAD_FORMAT, "le format doit être zip, tar, tar.gz ou tar.zst", ), ( k::ERR_BAD_MULTIPART, "multipart/form-data avec boundary attendu", ), (k::ERR_BAD_PART_NAME, "nom de partie invalide"), ( k::ERR_BAD_POST, "POST attend un téléversement multipart, une mutation JSON ou ?action=mkdir/create-file", ), (k::ERR_BAD_UPLOAD, "données de téléversement invalides"), ( k::ERR_BAD_UPLOAD_PATH, "chemin de fichier invalide dans le téléversement", ), (k::ERR_DST_REQUIRED, "dst_root_id est requis"), (k::ERR_FILE_NAME_REQUIRED, "un nom de fichier est requis"), (k::ERR_FILES_EXIST, "certains fichiers existent déjà"), (k::ERR_FOLDER_EXISTS, "un dossier avec ce nom existe déjà"), (k::ERR_FOLDER_NAME_REQUIRED, "un nom de dossier est requis"), (k::ERR_FS_CONFLICT, "existe déjà"), (k::ERR_FS_FORBIDDEN, "accès refusé"), (k::ERR_FS_NOT_A_DIR, "ce n'est pas un dossier"), (k::ERR_FS_NOT_FOUND, "dossier introuvable"), (k::ERR_FS_ROOT_MISSING, "le dossier configuré n'existe plus"), (k::ERR_INTERNAL, "erreur interne"), (k::ERR_INVALID_CREDENTIALS, "nom ou mot de passe incorrect"), (k::ERR_INVALID_LANGUAGE, "balise de langue invalide"), ( k::ERR_LAST_ADMIN, "impossible de supprimer le dernier administrateur actif", ), ( k::ERR_LAST_ADMIN_DELETE, "impossible de supprimer le dernier administrateur actif", ), (k::ERR_NAME_LENGTH, "le nom doit contenir 1 à 64 caractères"), (k::ERR_NEW_NAME_REQUIRED, "new_name est requis"), (k::ERR_NO_FILES_UPLOADED, "aucun fichier n'a été téléversé"), (k::ERR_NO_SUCH_FOLDER, "ce dossier n'existe pas"), (k::ERR_NOT_A_FILE, "ce n'est pas un fichier"), (k::ERR_NOT_SIGNED_IN, "non connecté"), ( k::ERR_OWN_ACCOUNT, "vous ne pouvez pas désactiver votre propre compte", ), ( k::ERR_OWN_ADMIN, "vous ne pouvez pas retirer vos propres droits d'administrateur", ), ( k::ERR_OWN_DELETE, "vous ne pouvez pas supprimer votre propre compte", ), (k::ERR_PART_NO_NAME, "partie sans nom"), ( k::ERR_PASSWORD_SHORT, "le mot de passe doit contenir au moins 8 caractères", ), (k::ERR_PATH_REQUIRED, "un chemin dans le dossier est requis"), (k::ERR_READ_ONLY_FOLDER, "dossier en lecture seule"), (k::ERR_ROOT_FORBIDDEN, "dossier inaccessible"), ( k::ERR_RW_RO_FOLDER, "ce dossier est en lecture seule pour vous, il ne peut donc pas être partagé en écriture", ), ( k::ERR_RW_SHARES_DISABLED, "les partages modifiables sont désactivés", ), (k::ERR_SAVE_FAILED, "impossible d'enregistrer le fichier"), ( k::ERR_SEARCH_BAD_SCOPE, "la portée doit être name, content ou both", ), ( k::ERR_SEARCH_FORBIDDEN, "la recherche nécessite une session connectée", ), ( k::ERR_SESSION_EXPIRED, "session expirée, veuillez vous connecter à nouveau", ), (k::ERR_SHARE_EXPIRED, "ce partage a expiré"), ( k::ERR_SHARE_LOCKED, "Ce partage est protégé par un mot de passe.", ), ( k::ERR_SHARE_NO_PASSWORD, "Ce partage n'a pas de mot de passe.", ), (k::ERR_SHARE_NOT_FOUND, "partage introuvable"), ( k::ERR_SHARE_TOKEN_FORBIDDEN, "un jeton de partage ne peut pas être utilisé ici ; connectez-vous à la place", ), (k::ERR_SHARE_WRONG_PASSWORD, "Mot de passe incorrect."), ( k::ERR_TOO_LARGE_PREVIEW, "fichier trop volumineux pour l'aperçu", ), ( k::ERR_TOO_LARGE_SAVE, "fichier trop volumineux pour être enregistré", ), (k::ERR_USER_EXISTS, "un utilisateur avec ce nom existe déjà"), (k::ERR_USER_NOT_FOUND, "utilisateur introuvable"), ("exclude_dup_err", "Ce dossier est déjà exclu."), ("exclude_folder", "Exclure un dossier…"), ("exclude_here", "Exclure ce dossier"), ( "exclude_root_err", "La racine entière ne peut pas être exclue ; cela désactiverait la recherche.", ), ("expires", "Expire"), ("expires_at", "Expire le {}"), ("expiry_1d", "1 jour"), ("expiry_1h", "1 heure"), ("expiry_1mo", "1 mois"), ("expiry_1w", "1 semaine"), ("file", "Fichier"), ("file_changed", "Fichier modifié sur disque"), ("file_created", "Fichier créé"), ("file_info", "Informations"), ("file_name", "Nom du fichier"), ("files", "Fichiers"), ("fmt_tar", "Tar"), ("fmt_targz", "Tar + gzip"), ("fmt_tarzst", "Tar + zstd"), ("fmt_zip", "Zip"), ("folder", "Dossier"), ("folder_created", "Dossier créé"), ( "folder_gone_msg", "Ce dossier n'est plus accessible. Choisissez-en un autre dans le panneau latéral.", ), ("folder_gone_title", "Dossier indisponible"), ("folder_name", "Nom du dossier"), ("folders", "Dossiers"), ( "folders_paths_hint", "Les chemins sont relatifs à la racine du serveur. « . » désigne toute la racine.", ), ("grid_view", "Vue en grille"), ("info", "Infos"), ("keep_editing", "Continuer à modifier"), ("lang_auto", "Automatique"), ("lang_de", "Allemand"), ("lang_en", "Anglais"), ("lang_fr", "Français"), ("language", "Langue"), ( "language_desc", "La langue de l'interface. « Automatique » utilise la langue de votre navigateur.", ), ("link_copied", "Lien copié dans le presse-papiers"), ( "list_truncated", "Seules les {} premières entrées sont affichées.", ), ("list_view", "Vue en liste"), ("load_latest", "Charger la version la plus récente"), ("loading", "Chargement…"), ("loading_share", "Chargement du partage…"), ("location", "Emplacement"), ("log_out", "Se déconnecter"), ( "login_error", "Veuillez saisir votre nom et votre mot de passe.", ), ("login_subtitle", "Connectez-vous pour continuer."), ("manage", "Gérer"), ("menu", "Menu"), ("meta_created", "créé le {}"), ("meta_expires", "expire le {}"), ("mode_ro", "lecture seule"), ("mode_rw", "lecture-écriture"), ("modified", "Modifié"), ( "modified_after", "Ce fichier a été modifié après son ouverture. « Charger la version la plus récente » abandonne vos modifications non enregistrées ; « Écraser » conserve votre version.", ), ("more_actions", "Plus d'actions"), ("move", "Déplacer"), ("move_here", "Déplacer ici"), ("move_n_to", "Déplacer {} éléments vers…"), ("move_to", "Déplacer « {} » vers…"), ("moved", "Déplacé"), ("name", "Nom"), ("name_required", "Un nom est requis."), ("name_taken", "Ce nom est déjà utilisé"), ("need_folder", "Donnez au moins un dossier à l'utilisateur."), ("never", "Jamais"), ("never_expires", "N'expire jamais"), ("new_file", "Nouveau fichier"), ("new_folder", "Nouveau dossier"), ("new_name", "Nouveau nom"), ( "new_password_keep", "Nouveau mot de passe (laisser vide pour conserver)", ), ("new_user", "Nouvel utilisateur"), ( "no_folders_msg", "Aucun administrateur ne vous a encore attribué de dossier.", ), ("no_folders_title", "Aucun dossier disponible"), ("no_preview", "Aperçu non disponible"), ( "no_preview_msg", "L'application ne peut pas afficher « {} » ici.", ), ("no_shares", "Aucun partage pour l'instant"), ("no_subfolders", "Aucun sous-dossier"), ("no_users", "Aucun utilisateur."), ("open", "Ouvrir"), ("open_in_new_tab", "Ouvrir dans un nouvel onglet"), ("open_menu", "Ouvrir le menu"), ("overwrite", "Écraser"), ( "overwrite_question", "Les fichiers suivants existent déjà. Les écraser ?", ), ("password", "Mot de passe"), ("profile", "Profil"), ( "profile_save_err", "Impossible d'enregistrer le profil : {}", ), ("profile_saved", "Profil enregistré"), ( "pw_short", "Le mot de passe doit contenir au moins 8 caractères.", ), ("read_only_badge", "Lecture seule"), ("read_only_hint", "Dossier en lecture seule"), ("refresh", "Actualiser"), ("refresh_folders", "Actualiser vos dossiers"), ("remove_folder", "Retirer le dossier"), ("rename", "Renommer"), ("rename_title", "Renommer « {} »"), ("renamed", "Renommé"), ("repeat_password", "Répéter le mot de passe"), ("retry", "Réessayer"), ("root_dup_err", "Ce dossier est déjà dans la liste."), ( "rw_disabled", "Les partages modifiables sont désactivés par le serveur.", ), ( "rw_ro_folder", "Ce dossier est en lecture seule, le partage l'est donc aussi.", ), ("save", "Enregistrer"), ("save_profile", "Enregistrer le profil"), ("save_settings", "Enregistrer les paramètres"), ("saved", "Enregistré"), ("saving", "Enregistrement…"), ("search", "Rechercher"), ("search_both", "Les deux"), ("search_collapse_all", "Tout replier"), ("search_content", "Contenu"), ("search_excludes", "Exclu de la recherche"), ("search_excludes_empty", "Aucun dossier exclu."), ("search_expand_all", "Tout déplier"), ("search_failed", "La recherche a échoué."), ("search_goto", "Afficher dans le dossier"), ("search_here", "Rechercher ici"), ("search_in", "Rechercher dans…"), ("search_in_folder", "Rechercher dans ce dossier"), ("search_in_n_files", "dans {} fichiers"), ("search_n_matches", "{} correspondances"), ("search_n_results", "{} résultats"), ("search_name", "Nom"), ("search_no_results", "aucun résultat"), ( "search_placeholder", "Rechercher dans les fichiers et leur contenu", ), ("search_run", "Rechercher"), ("search_scanned", "{} fichiers parcourus"), ("search_show_more", "Afficher {} de plus"), ("search_shown_of", "{} sur {} affichés"), ("search_skipped", "{} ignorés (plus de 10 Mio)"), ("search_stop", "Arrêter"), ("search_stopped", "arrêté · {} résultats"), ("search_took", "{} ms"), ("searching", "recherche…"), ( "select_folder_msg", "Choisissez un dossier dans le panneau latéral pour commencer à naviguer.", ), ("select_folder_title", "Choisir un dossier"), ("server", "Serveur"), ("server_root", "Racine du serveur"), ("server_unreachable", "Le serveur est injoignable."), ("settings", "Paramètres"), ( "settings_save_err", "Impossible d'enregistrer les paramètres : {}", ), ("settings_saved", "Paramètres enregistrés"), ( "setup_intro", "Premier démarrage : créez le compte administrateur. Cet utilisateur peut gérer les autres utilisateurs et les paramètres du serveur.", ), ( "setup_name_err", "Veuillez choisir un nom (1 à 64 caractères).", ), ( "setup_pw_mismatch", "Les mots de passe ne correspondent pas.", ), ("share", "Partager"), ("share_create_err", "Impossible de créer le partage : {}"), ("share_delete_err", "Impossible de supprimer le partage"), ("share_deleted", "Partage supprimé"), ( "share_dialog_hint", "Crée un lien pour cet élément. Les liens modifiables permettent aux destinataires de téléverser et de modifier.", ), ("share_expired", "Partage expiré"), ("share_expired_msg", "Ce lien de partage n'est plus actif."), ("share_link", "Lien de partage"), ("share_locked_title", "Mot de passe requis"), ("share_not_found", "Partage introuvable"), ( "share_not_found_msg", "Ce lien est invalide ou le partage a été supprimé.", ), ( "share_password_hint", "Facultatif. Les visiteurs doivent le saisir avant d'ouvrir le partage, au moins 8 caractères.", ), ( "share_perm_ro", "Toute personne possédant ce lien peut lire « {} ».", ), ( "share_perm_rw", "Toute personne possédant ce lien peut lire et modifier « {} ».", ), ("share_protected", "Protégé par mot de passe"), ("share_title", "Partager {}"), ("shares", "Partages"), ("shares_delete_err", "Impossible de supprimer les partages"), ( "shares_empty_hint", "Faites un clic droit sur un fichier ou un dossier, puis choisissez « Partager » pour créer un lien.", ), ( "shares_view_hint", "Liens que vous avez créés. Faites un clic droit sur un fichier ou un dossier, puis choisissez « Partager » pour en ajouter un.", ), ("sign_in", "Se connecter"), ( "single_click_desc", "Quand activé, un simple clic ouvre une entrée et Ctrl+clic la sélectionne. Quand désactivé, un simple clic sélectionne et un double-clic ouvre.", ), ("single_click_label", "Ouvrir au clic simple"), ("size", "Taille"), ("some_files_exist", "Certains fichiers existent déjà"), ("sort_asc", "Croissant"), ("sort_by", "Trier par"), ("sort_desc", "Décroissant"), ("theme_auto", "Auto"), ("theme_dark", "Sombre"), ("theme_light", "Clair"), ("theme_title", "Thème : {} (cliquer pour changer)"), ( "thumbnails_desc", "Afficher un aperçu à la place d'une icône pour les images et les vidéos en vue grille.", ), ("thumbnails_label", "Afficher les miniatures"), ("type", "Type"), ("unlock", "Ouvrir le partage"), ("unsaved_changes", "Modifications non enregistrées"), ("until", "Jusqu'au"), ("upload_complete", "Téléversement terminé"), ("upload_files", "Téléverser des fichiers"), ("upload_folder", "Téléverser un dossier"), ("uploading", "Téléversement de {} fichier(s)…"), ("user_created", "Utilisateur créé"), ("user_delete_err", "Impossible de supprimer l'utilisateur"), ( "user_save_err", "Impossible d'enregistrer l'utilisateur : {}", ), ("user_updated", "Utilisateur mis à jour"), ("users", "Utilisateurs"), ("your_shares", "Vos partages"), ]; #[cfg(test)] mod tests { use super::*; fn keys<'a>(table: &'a [(&'a str, &'a str)]) -> Vec<&'a str> { table.iter().map(|(k, _)| *k).collect() } #[test] fn tables_are_sorted() { // `find` binary-searches, so an out-of-order entry is unreachable. for (name, table) in [("EN", EN), ("DE", DE), ("FR", FR)] { let ks = keys(table); let mut sorted = ks.clone(); sorted.sort_unstable(); assert_eq!(ks, sorted, "{name} table is not sorted by key"); } } #[test] fn no_duplicate_keys() { for (name, table) in [("EN", EN), ("DE", DE), ("FR", FR)] { let mut ks = keys(table); ks.sort_unstable(); let dups = ks .windows(2) .filter(|w| w[0] == w[1]) .map(|w| w[0]) .collect::>(); assert!(dups.is_empty(), "{name} duplicates: {dups:?}"); } } #[test] fn translations_cover_all_keys() { // Full parity: DE and FR must translate every EN key (both // directions), so no string can silently fall back to English. let en = keys(EN); for (name, table) in [("DE", DE), ("FR", FR)] { let other = keys(table); assert_eq!( en.len(), other.len(), "{name} has {} keys, EN has {}", other.len(), en.len() ); for k in &other { assert!(en.contains(k), "{name} key not in EN: {k}"); } } } }