//! 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)`. //! //! 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"), } } fn find(table: &'static [(&'static str, &'static str)], key: &str) -> Option<&'static str> { table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v) } /// Translation keys. English text is the value in `EN` below. pub mod k { // Empty states pub const NO_FOLDERS_TITLE: &str = "no_folders_title"; pub const NO_FOLDERS_MSG: &str = "no_folders_msg"; pub const SELECT_FOLDER_TITLE: &str = "select_folder_title"; pub const SELECT_FOLDER_MSG: &str = "select_folder_msg"; pub const FOLDER_GONE_TITLE: &str = "folder_gone_title"; pub const FOLDER_GONE_MSG: &str = "folder_gone_msg"; pub const LOADING: &str = "loading"; pub const LOADING_SHARE: &str = "loading_share"; pub const RETRY: &str = "retry"; pub const EMPTY_FOLDER: &str = "empty_folder"; // Browser chrome pub const REFRESH: &str = "refresh"; pub const GRID_VIEW: &str = "grid_view"; pub const LIST_VIEW: &str = "list_view"; // Entry actions pub const OPEN: &str = "open"; pub const OPEN_IN_NEW_TAB: &str = "open_in_new_tab"; pub const DOWNLOAD: &str = "download"; pub const RENAME: &str = "rename"; pub const MOVE: &str = "move"; pub const COPY: &str = "copy"; pub const DONE: &str = "done"; pub const SHARE: &str = "share"; pub const INFO: &str = "info"; pub const DELETE: &str = "delete"; pub const NEW_FOLDER: &str = "new_folder"; pub const NEW_FILE: &str = "new_file"; pub const UPLOAD_FILES: &str = "upload_files"; pub const UPLOAD_FOLDER: &str = "upload_folder"; pub const READ_ONLY_HINT: &str = "read_only_hint"; pub const READ_ONLY_BADGE: &str = "read_only_badge"; pub const MORE_ACTIONS: &str = "more_actions"; // Dialogs: prompt / confirm pub const FOLDER_NAME: &str = "folder_name"; pub const FILE_NAME: &str = "file_name"; pub const CREATE: &str = "create"; pub const RENAME_TITLE: &str = "rename_title"; pub const NEW_NAME: &str = "new_name"; pub const DELETE_FOLDER_MSG: &str = "delete_folder_msg"; pub const DELETE_MSG: &str = "delete_msg"; pub const DELETE_N_MSG: &str = "delete_n_msg"; pub const CANCEL: &str = "cancel"; // Dialogs: conflict / picker / download format / info pub const OVERWRITE_QUESTION: &str = "overwrite_question"; pub const OVERWRITE: &str = "overwrite"; pub const AND_MORE: &str = "and_more"; pub const NO_SUBFOLDERS: &str = "no_subfolders"; pub const MOVE_TO: &str = "move_to"; pub const COPY_TO: &str = "copy_to"; pub const MOVE_N_TO: &str = "move_n_to"; pub const COPY_N_TO: &str = "copy_n_to"; pub const MOVE_HERE: &str = "move_here"; pub const COPY_HERE: &str = "copy_here"; pub const DOWNLOAD_TITLE: &str = "download_title"; pub const DOWNLOAD_FORMATS_MSG: &str = "download_formats_msg"; pub const FMT_ZIP: &str = "fmt_zip"; pub const FMT_TAR: &str = "fmt_tar"; pub const FMT_TARGZ: &str = "fmt_targz"; pub const FMT_TARZST: &str = "fmt_tarzst"; pub const FILE_INFO: &str = "file_info"; pub const NAME: &str = "name"; pub const TYPE: &str = "type"; pub const FOLDER: &str = "folder"; pub const FILE: &str = "file"; pub const SIZE: &str = "size"; pub const MODIFIED: &str = "modified"; pub const LOCATION: &str = "location"; pub const CLOSE: &str = "close"; // Toasts pub const FOLDER_CREATED: &str = "folder_created"; pub const FILE_CREATED: &str = "file_created"; pub const NAME_TAKEN: &str = "name_taken"; pub const RENAMED: &str = "renamed"; pub const DELETED: &str = "deleted"; pub const UPLOADING: &str = "uploading"; pub const SOME_FILES_EXIST: &str = "some_files_exist"; pub const UPLOAD_COMPLETE: &str = "upload_complete"; pub const MOVED: &str = "moved"; pub const COPIED: &str = "copied"; pub const DOWNLOAD_STARTED: &str = "download_started"; pub const SAVED: &str = "saved"; // Shell: sidebar, topbar, footer pub const MENU: &str = "menu"; pub const OPEN_MENU: &str = "open_menu"; pub const REFRESH_FOLDERS: &str = "refresh_folders"; pub const THEME_TITLE: &str = "theme_title"; pub const LOG_OUT: &str = "log_out"; pub const FILES: &str = "files"; pub const MANAGE: &str = "manage"; pub const SHARES: &str = "shares"; pub const USERS: &str = "users"; pub const NO_USERS: &str = "no_users"; pub const SETTINGS: &str = "settings"; pub const THEME_AUTO: &str = "theme_auto"; pub const THEME_LIGHT: &str = "theme_light"; pub const THEME_DARK: &str = "theme_dark"; // Search view pub const SEARCH: &str = "search"; pub const SEARCH_PLACEHOLDER: &str = "search_placeholder"; pub const SEARCH_NAME: &str = "search_name"; pub const SEARCH_CONTENT: &str = "search_content"; pub const SEARCH_BOTH: &str = "search_both"; pub const SEARCH_GOTO: &str = "search_goto"; pub const SEARCH_RUN: &str = "search_run"; pub const SEARCH_IN: &str = "search_in"; pub const SEARCH_HERE: &str = "search_here"; pub const SEARCH_IN_FOLDER: &str = "search_in_folder"; pub const SEARCH_STOP: &str = "search_stop"; pub const SEARCHING: &str = "searching"; pub const SEARCH_N_RESULTS: &str = "search_n_results"; pub const SEARCH_TOOK: &str = "search_took"; pub const SEARCH_STOPPED: &str = "search_stopped"; pub const SEARCH_SCANNED: &str = "search_scanned"; pub const SEARCH_SKIPPED: &str = "search_skipped"; pub const SEARCH_N_MATCHES: &str = "search_n_matches"; pub const SEARCH_IN_N_FILES: &str = "search_in_n_files"; pub const SEARCH_COLLAPSE_ALL: &str = "search_collapse_all"; pub const SEARCH_EXPAND_ALL: &str = "search_expand_all"; pub const SEARCH_SHOWN_OF: &str = "search_shown_of"; pub const SEARCH_SHOW_MORE: &str = "search_show_more"; pub const SEARCH_NO_RESULTS: &str = "search_no_results"; // Settings pub const SAVING: &str = "saving"; pub const PROFILE: &str = "profile"; pub const SERVER: &str = "server"; pub const PROFILE_SAVED: &str = "profile_saved"; pub const PROFILE_SAVE_ERR: &str = "profile_save_err"; pub const SETTINGS_SAVED: &str = "settings_saved"; pub const SETTINGS_SAVE_ERR: &str = "settings_save_err"; pub const SINGLE_CLICK_LABEL: &str = "single_click_label"; pub const SINGLE_CLICK_DESC: &str = "single_click_desc"; pub const SAVE_PROFILE: &str = "save_profile"; pub const ALLOW_RW_SHARES: &str = "allow_rw_shares"; pub const ALLOW_RW_SHARES_DESC: &str = "allow_rw_shares_desc"; pub const SAVE_SETTINGS: &str = "save_settings"; pub const LANGUAGE: &str = "language"; pub const LANGUAGE_DESC: &str = "language_desc"; pub const LANG_AUTO: &str = "lang_auto"; pub const LANG_EN: &str = "lang_en"; pub const LANG_DE: &str = "lang_de"; pub const LANG_FR: &str = "lang_fr"; // Admin: users pub const DELETE_USER_CONFIRM: &str = "delete_user_confirm"; pub const USER_DELETE_ERR: &str = "user_delete_err"; pub const NEW_USER: &str = "new_user"; pub const EDIT: &str = "edit"; pub const DELETE_USER: &str = "delete_user"; pub const CANT_DELETE_SELF: &str = "cant_delete_self"; pub const ROOT_DUP_ERR: &str = "root_dup_err"; pub const NAME_REQUIRED: &str = "name_required"; pub const PW_SHORT: &str = "pw_short"; pub const NEED_FOLDER: &str = "need_folder"; pub const USER_CREATED: &str = "user_created"; pub const USER_UPDATED: &str = "user_updated"; pub const USER_SAVE_ERR: &str = "user_save_err"; pub const PASSWORD: &str = "password"; pub const NEW_PASSWORD_KEEP: &str = "new_password_keep"; pub const ADMINISTRATOR: &str = "administrator"; pub const ACTIVE_CAN_SIGNIN: &str = "active_can_signin"; pub const REMOVE_FOLDER: &str = "remove_folder"; pub const CREATE_USER: &str = "create_user"; pub const EDIT_USER_TITLE: &str = "edit_user_title"; pub const FOLDERS: &str = "folders"; pub const FOLDERS_PATHS_HINT: &str = "folders_paths_hint"; pub const ADD_FOLDER: &str = "add_folder"; pub const ADD_HERE: &str = "add_here"; pub const SERVER_ROOT: &str = "server_root"; pub const SERVER_UNREACHABLE: &str = "server_unreachable"; pub const SEARCH_FAILED: &str = "search_failed"; pub const MODE_RW: &str = "mode_rw"; pub const EXPIRY_1H: &str = "expiry_1h"; pub const EXPIRY_1D: &str = "expiry_1d"; pub const EXPIRY_1W: &str = "expiry_1w"; pub const EXPIRY_1MO: &str = "expiry_1mo"; pub const META_EXPIRES: &str = "meta_expires"; pub const META_CREATED: &str = "meta_created"; pub const MODE_RO: &str = "mode_ro"; // Share page (public) pub const SHARE_NOT_FOUND: &str = "share_not_found"; pub const SHARE_NOT_FOUND_MSG: &str = "share_not_found_msg"; pub const SHARE_EXPIRED: &str = "share_expired"; pub const SHARE_EXPIRED_MSG: &str = "share_expired_msg"; // Shares: dialog + management pub const COPY_MANUAL: &str = "copy_manual"; pub const LINK_COPIED: &str = "link_copied"; pub const SHARE_CREATE_ERR: &str = "share_create_err"; pub const SHARE_LINK: &str = "share_link"; pub const SHARE_PERM_RW: &str = "share_perm_rw"; pub const SHARE_PERM_RO: &str = "share_perm_ro"; pub const SHARE_TITLE: &str = "share_title"; pub const SHARE_DIALOG_HINT: &str = "share_dialog_hint"; pub const EXPIRES: &str = "expires"; pub const NEVER: &str = "never"; pub const CUSTOM: &str = "custom"; pub const UNTIL: &str = "until"; pub const EXPIRES_AT: &str = "expires_at"; pub const DELETE_PARTIAL: &str = "delete_partial"; pub const NEVER_EXPIRES: &str = "never_expires"; pub const ALLOW_EDITING: &str = "allow_editing"; pub const RW_DISABLED: &str = "rw_disabled"; pub const RW_RO_FOLDER: &str = "rw_ro_folder"; pub const CREATING: &str = "creating"; pub const CREATE_LINK: &str = "create_link"; pub const YOUR_SHARES: &str = "your_shares"; pub const DELETED_N_SHARES: &str = "deleted_n_shares"; pub const SHARES_DELETE_ERR: &str = "shares_delete_err"; pub const DELETE_ALL: &str = "delete_all"; pub const NO_SHARES: &str = "no_shares"; pub const SHARES_VIEW_HINT: &str = "shares_view_hint"; pub const SHARES_EMPTY_HINT: &str = "shares_empty_hint"; pub const COPY_LINK: &str = "copy_link"; pub const DELETE_SHARE: &str = "delete_share"; pub const SHARE_DELETED: &str = "share_deleted"; pub const SHARE_DELETE_ERR: &str = "share_delete_err"; pub const DELETE_ALL_TITLE: &str = "delete_all_title"; pub const DELETE_ALL_MSG: &str = "delete_all_msg"; // Login / setup pub const LOGIN_ERROR: &str = "login_error"; pub const LOGIN_SUBTITLE: &str = "login_subtitle"; pub const SIGN_IN: &str = "sign_in"; pub const SETUP_NAME_ERR: &str = "setup_name_err"; pub const SETUP_PW_MISMATCH: &str = "setup_pw_mismatch"; pub const REPEAT_PASSWORD: &str = "repeat_password"; pub const SETUP_INTRO: &str = "setup_intro"; pub const CREATE_ADMIN: &str = "create_admin"; // File view / editor pub const SAVE: &str = "save"; pub const CLOSE_ESC: &str = "close_esc"; pub const NO_PREVIEW: &str = "no_preview"; pub const NO_PREVIEW_MSG: &str = "no_preview_msg"; pub const DISMISS: &str = "dismiss"; pub const DISMISS_ERROR: &str = "dismiss_error"; pub const FILE_CHANGED: &str = "file_changed"; pub const MODIFIED_AFTER: &str = "modified_after"; pub const LOAD_LATEST: &str = "load_latest"; pub const KEEP_EDITING: &str = "keep_editing"; pub const DISCARD: &str = "discard"; pub const UNSAVED_CHANGES: &str = "unsaved_changes"; pub const DISCARD_QUESTION: &str = "discard_question"; #[allow(dead_code)] // Server error codes (see server/src for the constructors) pub const ERR_INTERNAL: &str = "err_internal"; pub const ERR_NOT_SIGNED_IN: &str = "err_not_signed_in"; pub const ERR_SESSION_EXPIRED: &str = "err_session_expired"; pub const ERR_INVALID_CREDENTIALS: &str = "err_invalid_credentials"; pub const ERR_ALREADY_SET_UP: &str = "err_already_set_up"; pub const ERR_SHARE_NOT_FOUND: &str = "err_share_not_found"; pub const ERR_SHARE_EXPIRED: &str = "err_share_expired"; pub const ERR_SHARE_TOKEN_FORBIDDEN: &str = "err_share_token_forbidden"; pub const ERR_NAME_LENGTH: &str = "err_name_length"; pub const ERR_PASSWORD_SHORT: &str = "err_password_short"; pub const ERR_ADMIN_ONLY: &str = "err_admin_only"; pub const ERR_USER_EXISTS: &str = "err_user_exists"; pub const ERR_USER_NOT_FOUND: &str = "err_user_not_found"; pub const ERR_OWN_ADMIN: &str = "err_own_admin"; pub const ERR_OWN_ACCOUNT: &str = "err_own_account"; pub const ERR_LAST_ADMIN: &str = "err_last_admin"; pub const ERR_OWN_DELETE: &str = "err_own_delete"; pub const ERR_LAST_ADMIN_DELETE: &str = "err_last_admin_delete"; pub const ERR_BAD_FORMAT: &str = "err_bad_format"; pub const ERR_NOT_A_FILE: &str = "err_not_a_file"; pub const ERR_TOO_LARGE_PREVIEW: &str = "err_too_large_preview"; pub const ERR_BAD_ACTION: &str = "err_bad_action"; pub const ERR_TOO_LARGE_SAVE: &str = "err_too_large_save"; pub const ERR_BAD_BODY: &str = "err_bad_body"; pub const ERR_INVALID_LANGUAGE: &str = "err_invalid_language"; pub const ERR_BAD_POST: &str = "err_bad_post"; pub const ERR_FOLDER_NAME_REQUIRED: &str = "err_folder_name_required"; pub const ERR_FILE_NAME_REQUIRED: &str = "err_file_name_required"; pub const ERR_NEW_NAME_REQUIRED: &str = "err_new_name_required"; pub const ERR_DST_REQUIRED: &str = "err_dst_required"; pub const ERR_PATH_REQUIRED: &str = "err_path_required"; pub const ERR_BAD_MULTIPART: &str = "err_bad_multipart"; pub const ERR_BAD_UPLOAD: &str = "err_bad_upload"; pub const ERR_PART_NO_NAME: &str = "err_part_no_name"; pub const ERR_BAD_PART_NAME: &str = "err_bad_part_name"; pub const ERR_FOLDER_EXISTS: &str = "err_folder_exists"; pub const ERR_SAVE_FAILED: &str = "err_save_failed"; pub const ERR_NO_FILES_UPLOADED: &str = "err_no_files_uploaded"; pub const ERR_FILES_EXIST: &str = "err_files_exist"; pub const ERR_BAD_UPLOAD_PATH: &str = "err_bad_upload_path"; pub const ERR_NO_SUCH_FOLDER: &str = "err_no_such_folder"; pub const ERR_READ_ONLY_FOLDER: &str = "err_read_only_folder"; pub const ERR_RW_SHARES_DISABLED: &str = "err_rw_shares_disabled"; pub const ERR_RW_RO_FOLDER: &str = "err_rw_ro_folder"; pub const ERR_BAD_EXPIRES_AT: &str = "err_bad_expires_at"; pub const ERR_FS_NOT_FOUND: &str = "err_fs_not_found"; pub const ERR_FS_NOT_A_DIR: &str = "err_fs_not_a_dir"; pub const ERR_FS_FORBIDDEN: &str = "err_fs_forbidden"; pub const ERR_FS_ROOT_MISSING: &str = "err_fs_root_missing"; pub const ERR_FS_CONFLICT: &str = "err_fs_conflict"; pub const ERR_SEARCH_FORBIDDEN: &str = "err_search_forbidden"; pub const ERR_SEARCH_BAD_SCOPE: &str = "err_search_bad_scope"; pub const ERR_ROOT_FORBIDDEN: &str = "err_root_forbidden"; } /// Canonical English strings (key → text). The source of truth for which /// keys exist; DE and FR may be partial and fall back to English. const EN: &[(&str, &str)] = &[ ("no_folders_title", "No folders available"), ( "no_folders_msg", "An administrator hasn't assigned you any folders yet.", ), ("select_folder_title", "Select a folder"), ( "select_folder_msg", "Choose a folder from the sidebar to start browsing.", ), ("folder_gone_title", "Folder not available"), ( "folder_gone_msg", "This folder is no longer accessible. Choose another one from the sidebar.", ), ("loading", "Loading…"), ("loading_share", "Loading share…"), ("retry", "Retry"), ("empty_folder", "This folder is empty."), ("refresh", "Refresh"), ("grid_view", "Grid view"), ("list_view", "List view"), ("open", "Open"), ("open_in_new_tab", "Open in new tab"), ("download", "Download"), ("rename", "Rename"), ("move", "Move"), ("copy", "Copy"), ("done", "Done"), ("share", "Share"), ("info", "Info"), ("delete", "Delete"), ("new_folder", "New folder"), ("new_file", "New file"), ("upload_files", "Upload files"), ("upload_folder", "Upload folder"), ("read_only_hint", "Read-only folder"), ("read_only_badge", "Read-only"), ("more_actions", "More actions"), ("folder_name", "Folder name"), ("file_name", "File name"), ("create", "Create"), ("rename_title", "Rename “{}”"), ("new_name", "New name"), ( "delete_folder_msg", "Delete folder “{}” and everything inside it?\nThis cannot be undone.", ), ("delete_msg", "Delete “{}”?\nThis cannot be undone."), ("delete_n_msg", "Delete {} items?\nThis cannot be undone."), ("cancel", "Cancel"), ( "overwrite_question", "The following files already exist. Overwrite them?", ), ("overwrite", "Overwrite"), ("and_more", "…and {} more"), ("no_subfolders", "No subfolders"), ("move_to", "Move “{}” to…"), ("copy_to", "Copy “{}” to…"), ("move_n_to", "Move {} items to…"), ("copy_n_to", "Copy {} items to…"), ("move_here", "Move here"), ("copy_here", "Copy here"), ("download_title", "Download “{}”"), ( "download_formats_msg", "Choose an archive format for the folder.", ), ("fmt_zip", "Zip"), ("fmt_tar", "Tar"), ("fmt_targz", "Tar + gzip"), ("fmt_tarzst", "Tar + zstd"), ("file_info", "File info"), ("name", "Name"), ("type", "Type"), ("folder", "Folder"), ("file", "File"), ("size", "Size"), ("modified", "Modified"), ("location", "Location"), ("close", "Close"), ("folder_created", "Folder created"), ("file_created", "File created"), ("name_taken", "Name already in use"), ("renamed", "Renamed"), ("deleted", "Deleted"), ("uploading", "Uploading {} files…"), ("some_files_exist", "Some files already exist"), ("upload_complete", "Upload complete"), ("moved", "Moved"), ("copied", "Copied"), ("download_started", "Download started"), ("saved", "Saved"), ("menu", "Menu"), ("open_menu", "Open menu"), ("refresh_folders", "Refresh your folders"), ("theme_title", "Theme: {} (click to switch)"), ("log_out", "Log out"), ("files", "Files"), ("manage", "Manage"), ("shares", "Shares"), ("users", "Users"), ("no_users", "No users."), ("settings", "Settings"), ("theme_auto", "Auto"), ("theme_light", "Light"), ("theme_dark", "Dark"), ("search", "Search"), ("search_placeholder", "Search files and their contents"), ("search_name", "Name"), ("search_content", "Content"), ("search_both", "Both"), ("search_goto", "Show in folder"), ("search_run", "Search"), ("search_in", "Search in…"), ("search_here", "Search here"), ("search_in_folder", "Search in this folder"), ("search_stop", "Stop"), ("searching", "searching…"), ("search_n_results", "{} results"), ("search_took", "{} ms"), ("search_stopped", "stopped · {} results"), ("search_scanned", "scanned {} files"), ("search_skipped", "{} skipped (over 10 MiB)"), ("search_n_matches", "{} matches"), ("search_in_n_files", "in {} files"), ("search_collapse_all", "Collapse all"), ("search_expand_all", "Expand all"), ("search_shown_of", "Showing {} of {}"), ("search_show_more", "Show {} more"), ("search_no_results", "no results"), ("saving", "Saving…"), ("profile", "Profile"), ("server", "Server"), ("profile_saved", "Profile saved"), ("profile_save_err", "Could not save profile: {}"), ("settings_saved", "Settings saved"), ("settings_save_err", "Could not save settings: {}"), ("single_click_label", "Use single click to open"), ( "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.", ), ("save_profile", "Save profile"), ("allow_rw_shares", "Allow writable shares"), ( "allow_rw_shares_desc", "Let users create read-write share links. Off by default.", ), ("save_settings", "Save settings"), ("language", "Language"), ( "language_desc", "The interface language. Automatic uses your browser's language.", ), ("lang_auto", "Automatic"), ("lang_en", "English"), ("lang_de", "German"), ("lang_fr", "French"), ( "delete_user_confirm", "Delete user “{}”? This cannot be undone.", ), ("user_delete_err", "Could not delete user"), ("new_user", "New user"), ("edit", "Edit"), ("delete_user", "Delete user"), ("cant_delete_self", "You cannot delete yourself"), ("root_dup_err", "That folder is already in the list."), ("name_required", "A name is required."), ("pw_short", "Password must be at least 8 characters."), ("need_folder", "Give the user at least one folder."), ("user_created", "User created"), ("user_updated", "User updated"), ("user_save_err", "Could not save user: {}"), ("password", "Password"), ("new_password_keep", "New password (leave blank to keep)"), ("administrator", "Administrator"), ("active_can_signin", "Active (can sign in)"), ("remove_folder", "Remove folder"), ("create_user", "Create user"), ("save", "Save"), ("edit_user_title", "Edit “{}”"), ("folders", "Folders"), ( "folders_paths_hint", "Paths are relative to the server root. “.” is the whole root.", ), ("add_folder", "Add folder…"), ("add_here", "Add this folder"), ("server_root", "Server root"), ("server_unreachable", "The server is not reachable."), ("search_failed", "The search failed."), ("mode_rw", "read-write"), ("expiry_1h", "1 hour"), ("expiry_1d", "1 day"), ("expiry_1w", "1 week"), ("expiry_1mo", "1 month"), ("meta_expires", "expires {}"), ("meta_created", "created {}"), ("mode_ro", "read-only"), ("share_not_found", "Share not found"), ( "share_not_found_msg", "This link is invalid or the share was removed.", ), ("share_expired", "Share expired"), ("share_expired_msg", "This share link is no longer active."), ("copy_manual", "Copy the link manually"), ("link_copied", "Link copied to clipboard"), ("share_create_err", "Could not create share: {}"), ("share_link", "Share link"), ( "share_perm_rw", "Anyone with this link can read and write “{}”.", ), ("share_perm_ro", "Anyone with this link can read “{}”."), ("share_title", "Share {}"), ( "share_dialog_hint", "Create a link for this item. Writable links let viewers upload and edit.", ), ("expires", "Expires"), ("never", "Never"), ("custom", "Custom…"), ("until", "Until"), ("expires_at", "Expires {}"), ("delete_partial", "{} of {} shares could not be deleted"), ("never_expires", "Never expires"), ("allow_editing", "Allow editing (read-write)"), ("rw_disabled", "Writable shares are disabled by the server."), ( "rw_ro_folder", "This folder is read-only, so the share is read-only too.", ), ("creating", "Creating…"), ("create_link", "Create link"), ("your_shares", "Your shares"), ("deleted_n_shares", "Deleted {} shares"), ("shares_delete_err", "Could not delete shares"), ("delete_all", "Delete all"), ("no_shares", "No shares yet"), ( "shares_view_hint", "Links you created. Right-click a file or folder and choose “Share” to add one.", ), ( "shares_empty_hint", "Right-click a file or folder and choose “Share” to create a link.", ), ("copy_link", "Copy link"), ("delete_share", "Delete share"), ("share_deleted", "Share deleted"), ("share_delete_err", "Could not delete share"), ("delete_all_title", "Delete all shares?"), ( "delete_all_msg", "Delete all {} share links? Anyone holding them loses access. This cannot be undone.", ), ("login_subtitle", "Sign in to continue."), ("login_error", "Please enter your name and password."), ("sign_in", "Sign in"), ("setup_name_err", "Please choose a name (1–64 characters)."), ("setup_pw_mismatch", "Passwords don't match."), ("repeat_password", "Repeat password"), ("create_admin", "Create admin account"), ( "setup_intro", "First boot: create the admin account. This user can manage other users and server settings.", ), ("close_esc", "Close (Esc)"), ("no_preview", "No preview available"), ("no_preview_msg", "The app can’t display “{}” here."), ("dismiss", "Dismiss"), ("dismiss_error", "Dismiss error"), ("file_changed", "File changed on disk"), ( "modified_after", "This file was modified after you opened it. “Load latest” discards your unsaved changes; “Overwrite” keeps your version.", ), ("load_latest", "Load latest"), ("keep_editing", "Keep editing"), ("discard", "Discard"), ("unsaved_changes", "Unsaved changes"), ( "discard_question", "Discard your changes and close the editor?", ), (k::ERR_INTERNAL, "internal error"), (k::ERR_NOT_SIGNED_IN, "not signed in"), ( k::ERR_SESSION_EXPIRED, "session expired, please sign in again", ), (k::ERR_INVALID_CREDENTIALS, "invalid name or password"), (k::ERR_ALREADY_SET_UP, "server is already set up"), (k::ERR_SHARE_NOT_FOUND, "share not found"), (k::ERR_SHARE_EXPIRED, "this share has expired"), (k::ERR_BAD_EXPIRES_AT, "the expiry date is invalid"), ( k::ERR_SHARE_TOKEN_FORBIDDEN, "a share token cannot be used here; sign in instead", ), (k::ERR_NAME_LENGTH, "name must be 1–64 characters"), ( k::ERR_PASSWORD_SHORT, "password must be at least 8 characters", ), (k::ERR_ADMIN_ONLY, "admin only"), (k::ERR_USER_EXISTS, "a user with that name already exists"), (k::ERR_USER_NOT_FOUND, "user not found"), (k::ERR_OWN_ADMIN, "you cannot remove your own admin rights"), (k::ERR_OWN_ACCOUNT, "you cannot disable your own account"), (k::ERR_LAST_ADMIN, "cannot remove the last active admin"), (k::ERR_OWN_DELETE, "you cannot delete your own account"), ( k::ERR_LAST_ADMIN_DELETE, "cannot delete the last active admin", ), ( k::ERR_BAD_FORMAT, "format must be one of: zip, tar, tar.gz, tar.zst", ), (k::ERR_NOT_A_FILE, "not a file"), (k::ERR_TOO_LARGE_PREVIEW, "file too large to preview"), (k::ERR_BAD_ACTION, "expected action=content"), (k::ERR_TOO_LARGE_SAVE, "file too large to save"), (k::ERR_BAD_BODY, "invalid request body"), (k::ERR_INVALID_LANGUAGE, "invalid language tag"), ( k::ERR_BAD_POST, "POST expects a multipart upload, a JSON mutation, or ?action=mkdir/create-file", ), (k::ERR_FOLDER_NAME_REQUIRED, "a folder name is required"), (k::ERR_FILE_NAME_REQUIRED, "a file name is required"), (k::ERR_NEW_NAME_REQUIRED, "new_name is required"), (k::ERR_DST_REQUIRED, "dst_root_id is required"), (k::ERR_PATH_REQUIRED, "a path inside the folder is required"), ( k::ERR_BAD_MULTIPART, "expected multipart/form-data with a boundary", ), (k::ERR_BAD_UPLOAD, "invalid upload data"), (k::ERR_PART_NO_NAME, "part without a name"), (k::ERR_BAD_PART_NAME, "invalid part name"), ( k::ERR_FOLDER_EXISTS, "a folder with this name already exists", ), (k::ERR_SAVE_FAILED, "could not save the file"), (k::ERR_NO_FILES_UPLOADED, "no files were uploaded"), (k::ERR_FILES_EXIST, "some files already exist"), (k::ERR_BAD_UPLOAD_PATH, "invalid file path in upload"), (k::ERR_NO_SUCH_FOLDER, "no such folder"), (k::ERR_READ_ONLY_FOLDER, "read-only folder"), (k::ERR_RW_SHARES_DISABLED, "writable shares are disabled"), ( k::ERR_RW_RO_FOLDER, "this folder is read-only for you, so it cannot be shared writably", ), (k::ERR_FS_NOT_FOUND, "folder not found"), (k::ERR_FS_NOT_A_DIR, "not a folder"), (k::ERR_FS_FORBIDDEN, "access denied"), ( k::ERR_FS_ROOT_MISSING, "the configured folder no longer exists", ), (k::ERR_FS_CONFLICT, "already exists"), ( k::ERR_SEARCH_FORBIDDEN, "search requires a signed-in session", ), ( k::ERR_SEARCH_BAD_SCOPE, "scope must be name, content or both", ), (k::ERR_ROOT_FORBIDDEN, "root not accessible"), ]; /// German translations. const DE: &[(&str, &str)] = &[ ("no_folders_title", "Keine Ordner verfügbar"), ( "no_folders_msg", "Ein Administrator hat Ihnen noch keine Ordner zugewiesen.", ), ("select_folder_title", "Ordner auswählen"), ( "select_folder_msg", "Wählen Sie links einen Ordner aus, um mit dem Durchsuchen zu beginnen.", ), ("folder_gone_title", "Ordner nicht verfügbar"), ( "folder_gone_msg", "Dieser Ordner ist nicht mehr verfügbar. Wählen Sie links einen anderen aus.", ), ("loading", "Wird geladen…"), ("loading_share", "Freigabe wird geladen…"), ("retry", "Erneut versuchen"), ("empty_folder", "Dieser Ordner ist leer."), ("refresh", "Aktualisieren"), ("grid_view", "Kachelansicht"), ("list_view", "Listenansicht"), ("open", "Öffnen"), ("open_in_new_tab", "In neuem Tab öffnen"), ("download", "Herunterladen"), ("rename", "Umbenennen"), ("move", "Verschieben"), ("copy", "Kopieren"), ("done", "Fertig"), ("share", "Teilen"), ("info", "Info"), ("delete", "Löschen"), ("new_folder", "Neuer Ordner"), ("new_file", "Neue Datei"), ("upload_files", "Dateien hochladen"), ("upload_folder", "Ordner hochladen"), ("read_only_hint", "Schreibgeschützter Ordner"), ("read_only_badge", "Schreibgeschützt"), ("more_actions", "Weitere Aktionen"), ("folder_name", "Ordnername"), ("file_name", "Dateiname"), ("create", "Erstellen"), ("rename_title", "„{}“ umbenennen"), ("new_name", "Neuer Name"), ( "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.", ), ("cancel", "Abbrechen"), ( "overwrite_question", "Die folgenden Dateien existieren bereits. Überschreiben?", ), ("overwrite", "Überschreiben"), ("and_more", "…und {} weitere"), ("no_subfolders", "Keine Unterordner"), ("move_to", "„{}“ verschieben nach…"), ("copy_to", "„{}“ kopieren nach…"), ("move_n_to", "{} Elemente verschieben nach…"), ("copy_n_to", "{} Elemente kopieren nach…"), ("move_here", "Hierher verschieben"), ("copy_here", "Hierher kopieren"), ("download_title", "„{}“ herunterladen"), ( "download_formats_msg", "Archivformat für den Ordner wählen.", ), ("fmt_zip", "Zip"), ("fmt_tar", "Tar"), ("fmt_targz", "Tar + gzip"), ("fmt_tarzst", "Tar + zstd"), ("file_info", "Datei-Info"), ("name", "Name"), ("type", "Typ"), ("folder", "Ordner"), ("file", "Datei"), ("size", "Größe"), ("modified", "Geändert"), ("location", "Ort"), ("close", "Schließen"), ("folder_created", "Ordner erstellt"), ("file_created", "Datei erstellt"), ("name_taken", "Name wird bereits verwendet"), ("renamed", "Umbenannt"), ("deleted", "Gelöscht"), ("uploading", "{} Datei(en) werden hochgeladen…"), ("some_files_exist", "Einige Dateien existieren bereits"), ("upload_complete", "Hochladen abgeschlossen"), ("moved", "Verschoben"), ("copied", "Kopiert"), ("download_started", "Download gestartet"), ("saved", "Gespeichert"), ("menu", "Menü"), ("open_menu", "Menü öffnen"), ("refresh_folders", "Ordner aktualisieren"), ("theme_title", "Design: {} (klicken zum Wechseln)"), ("log_out", "Abmelden"), ("files", "Dateien"), ("manage", "Verwalten"), ("shares", "Freigaben"), ("users", "Benutzer"), ("no_users", "Keine Benutzer."), ("settings", "Einstellungen"), ("theme_auto", "Auto"), ("theme_light", "Hell"), ("theme_dark", "Dunkel"), ("search", "Suche"), ( "search_placeholder", "Dateien und deren Inhalte durchsuchen", ), ("search_name", "Name"), ("search_content", "Inhalt"), ("search_both", "Beides"), ("search_goto", "Im Ordner anzeigen"), ("search_run", "Suchen"), ("search_in", "Suchen in…"), ("search_here", "Hier suchen"), ("search_in_folder", "In diesem Ordner suchen"), ("search_stop", "Stopp"), ("searching", "Suche läuft…"), ("search_n_results", "{} Treffer"), ("search_took", "{} ms"), ("search_stopped", "gestoppt · {} Treffer"), ("search_scanned", "{} Dateien durchsucht"), ("search_skipped", "{} übersprungen (über 10 MiB)"), ("search_n_matches", "{} Treffer"), ("search_in_n_files", "in {} Dateien"), ("search_collapse_all", "Alle einklappen"), ("search_expand_all", "Alle ausklappen"), ("search_shown_of", "{} von {} angezeigt"), ("search_show_more", "{} weitere anzeigen"), ("search_no_results", "keine Treffer"), ("saving", "Wird gespeichert…"), ("profile", "Profil"), ("server", "Server"), ("profile_saved", "Profil gespeichert"), ( "profile_save_err", "Profil konnte nicht gespeichert werden: {}", ), ("settings_saved", "Einstellungen gespeichert"), ( "settings_save_err", "Einstellungen konnten nicht gespeichert werden: {}", ), ("single_click_label", "Einzelklick öffnet Einträge"), ( "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.", ), ("save_profile", "Profil speichern"), ("allow_rw_shares", "Schreibbare Freigaben erlauben"), ( "allow_rw_shares_desc", "Benutzern erlauben, schreibbare Freigabelinks zu erstellen. Standardmäßig aus.", ), ("save_settings", "Einstellungen speichern"), ("language", "Sprache"), ( "language_desc", "Die Sprache der Benutzeroberfläche. „Automatisch“ verwendet die Sprache Ihres Browsers.", ), ("lang_auto", "Automatisch"), ("lang_en", "Englisch"), ("lang_de", "Deutsch"), ("lang_fr", "Französisch"), ( "delete_user_confirm", "Benutzer „{}“ löschen? Das kann nicht rückgängig gemacht werden.", ), ("user_delete_err", "Benutzer konnte nicht gelöscht werden"), ("new_user", "Neuer Benutzer"), ("edit", "Bearbeiten"), ("delete_user", "Benutzer löschen"), ("cant_delete_self", "Sie können sich nicht selbst löschen"), ("root_dup_err", "Dieser Ordner ist bereits in der Liste."), ("name_required", "Ein Name ist erforderlich."), ( "pw_short", "Das Passwort muss mindestens 8 Zeichen lang sein.", ), ( "need_folder", "Dem Benutzer muss mindestens ein Ordner zugewiesen werden.", ), ("user_created", "Benutzer erstellt"), ("user_updated", "Benutzer aktualisiert"), ( "user_save_err", "Benutzer konnte nicht gespeichert werden: {}", ), ("password", "Passwort"), ( "new_password_keep", "Neues Passwort (leer lassen, um beizubehalten)", ), ("administrator", "Administrator"), ("active_can_signin", "Aktiv (kann sich anmelden)"), ("remove_folder", "Ordner entfernen"), ("create_user", "Benutzer erstellen"), ("save", "Speichern"), ("edit_user_title", "„{}“ bearbeiten"), ("folders", "Ordner"), ( "folders_paths_hint", "Pfade sind relativ zur Serverwurzel. „.“ ist der gesamte Wurzelordner.", ), ("add_folder", "Ordner hinzufügen…"), ("add_here", "Diesen Ordner hinzufügen"), ("server_root", "Serverwurzel"), ("server_unreachable", "Der Server ist nicht erreichbar."), ("search_failed", "Die Suche ist fehlgeschlagen."), ("mode_rw", "lesen/schreiben"), ("expiry_1h", "1 Stunde"), ("expiry_1d", "1 Tag"), ("expiry_1w", "1 Woche"), ("expiry_1mo", "1 Monat"), ("meta_expires", "läuft ab: {}"), ("meta_created", "erstellt: {}"), ("mode_ro", "nur lesen"), ("share_not_found", "Freigabe nicht gefunden"), ( "share_not_found_msg", "Dieser Link ist ungültig oder die Freigabe wurde entfernt.", ), ("share_expired", "Freigabe abgelaufen"), ( "share_expired_msg", "Dieser Freigabelink ist nicht mehr aktiv.", ), ("copy_manual", "Link manuell kopieren"), ("link_copied", "Link in die Zwischenablage kopiert"), ( "share_create_err", "Freigabe konnte nicht erstellt werden: {}", ), ("share_link", "Freigabelink"), ( "share_perm_rw", "Jeder mit diesem Link kann „{}“ lesen und schreiben.", ), ("share_perm_ro", "Jeder mit diesem Link kann „{}“ lesen."), ("share_title", "{} teilen"), ( "share_dialog_hint", "Erstellt einen Link für diesen Eintrag. Mit schreibbaren Links können Empfänger Dateien hochladen und bearbeiten.", ), ("expires", "Läuft ab"), ("never", "Nie"), ("custom", "Benutzerdefiniert…"), ("until", "Bis"), ("expires_at", "Läuft ab: {}"), ( "delete_partial", "{} von {} Freigaben konnten nicht gelöscht werden", ), ("never_expires", "Läuft nie ab"), ("allow_editing", "Bearbeitung erlauben (lesen/schreiben)"), ( "rw_disabled", "Schreibbare Freigaben sind vom Server deaktiviert.", ), ( "rw_ro_folder", "Dieser Ordner ist schreibgeschützt, daher ist die Freigabe auch schreibgeschützt.", ), ("creating", "Wird erstellt…"), ("create_link", "Link erstellen"), ("your_shares", "Ihre Freigaben"), ("deleted_n_shares", "{} Freigabe(n) gelöscht"), ( "shares_delete_err", "Freigaben konnten nicht gelöscht werden", ), ("delete_all", "Alle löschen"), ("no_shares", "Noch keine Freigaben"), ( "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.", ), ( "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.", ), ("copy_link", "Link kopieren"), ("delete_share", "Freigabe löschen"), ("share_deleted", "Freigabe gelöscht"), ("share_delete_err", "Freigabe konnte nicht gelöscht werden"), ("delete_all_title", "Alle Freigaben löschen?"), ( "delete_all_msg", "Alle {} Freigabelinks löschen? Jeder, der sie besitzt, verliert den Zugriff. Das kann nicht rückgängig gemacht werden.", ), ("login_subtitle", "Anmelden, um fortzufahren."), ("login_error", "Bitte Name und Passwort eingeben."), ("sign_in", "Anmelden"), ( "setup_name_err", "Bitte wählen Sie einen Namen (1–64 Zeichen).", ), ("setup_pw_mismatch", "Die Passwörter stimmen nicht überein."), ("repeat_password", "Passwort wiederholen"), ("create_admin", "Administrator-Konto erstellen"), ( "setup_intro", "Erster Start: Erstellen Sie das Administrator-Konto. Dieser Benutzer kann andere Benutzer und die Servereinstellungen verwalten.", ), ("close_esc", "Schließen (Esc)"), ("no_preview", "Keine Vorschau verfügbar"), ("no_preview_msg", "Die App kann „{}“ hier nicht anzeigen."), ("dismiss", "Ausblenden"), ("dismiss_error", "Fehler ausblenden"), ("file_changed", "Datei auf der Festplatte 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.", ), ("load_latest", "Neueste Version laden"), ("keep_editing", "Weiter bearbeiten"), ("discard", "Verwerfen"), ("unsaved_changes", "Ungespeicherte Änderungen"), ( "discard_question", "Änderungen verwerfen und den Editor schließen?", ), (k::ERR_INTERNAL, "interner Fehler"), (k::ERR_NOT_SIGNED_IN, "nicht angemeldet"), ( k::ERR_SESSION_EXPIRED, "Sitzung abgelaufen, bitte erneut anmelden", ), (k::ERR_INVALID_CREDENTIALS, "Name oder Passwort ungültig"), (k::ERR_ALREADY_SET_UP, "Server ist bereits eingerichtet"), (k::ERR_SHARE_NOT_FOUND, "Freigabe nicht gefunden"), (k::ERR_SHARE_EXPIRED, "diese Freigabe ist abgelaufen"), (k::ERR_BAD_EXPIRES_AT, "das Ablaufdatum ist ungültig"), ( k::ERR_SHARE_TOKEN_FORBIDDEN, "ein Freigabetoken kann hier nicht verwendet werden; bitte anmelden", ), (k::ERR_NAME_LENGTH, "der Name muss 1–64 Zeichen haben"), ( k::ERR_PASSWORD_SHORT, "das Passwort muss mindestens 8 Zeichen lang sein", ), (k::ERR_ADMIN_ONLY, "nur für Administratoren"), ( k::ERR_USER_EXISTS, "ein Benutzer mit diesem Namen existiert bereits", ), (k::ERR_USER_NOT_FOUND, "Benutzer nicht gefunden"), ( k::ERR_OWN_ADMIN, "Sie können Ihre eigenen Administratorrechte nicht entfernen", ), ( k::ERR_OWN_ACCOUNT, "Sie können Ihr eigenes Konto nicht deaktivieren", ), ( k::ERR_LAST_ADMIN, "der letzte aktive Administrator kann nicht entfernt werden", ), ( k::ERR_OWN_DELETE, "Sie können Ihr eigenes Konto nicht löschen", ), ( k::ERR_LAST_ADMIN_DELETE, "der letzte aktive Administrator kann nicht gelöscht werden", ), ( k::ERR_BAD_FORMAT, "das Format muss eines der folgenden sein: zip, tar, tar.gz, tar.zst", ), (k::ERR_NOT_A_FILE, "keine Datei"), (k::ERR_TOO_LARGE_PREVIEW, "Datei zu groß für eine Vorschau"), (k::ERR_BAD_ACTION, "action=content erwartet"), (k::ERR_TOO_LARGE_SAVE, "Datei zu groß zum Speichern"), (k::ERR_INVALID_LANGUAGE, "ungültiges Sprachkürzel"), (k::ERR_BAD_BODY, "ungültiger Anfrageinhalt"), ( k::ERR_BAD_POST, "POST erwartet einen Multipart-Upload, eine JSON-Mutation oder ?action=mkdir/create-file", ), ( k::ERR_FOLDER_NAME_REQUIRED, "ein Ordnername ist erforderlich", ), (k::ERR_FILE_NAME_REQUIRED, "ein Dateiname ist erforderlich"), (k::ERR_NEW_NAME_REQUIRED, "new_name ist erforderlich"), (k::ERR_DST_REQUIRED, "dst_root_id ist erforderlich"), ( k::ERR_PATH_REQUIRED, "ein Pfad innerhalb des Ordners ist erforderlich", ), ( k::ERR_BAD_MULTIPART, "multipart/form-data mit Boundary erwartet", ), (k::ERR_BAD_UPLOAD, "ungültige Upload-Daten"), (k::ERR_PART_NO_NAME, "Part ohne Namen"), (k::ERR_BAD_PART_NAME, "ungültiger Part-Name"), ( k::ERR_FOLDER_EXISTS, "ein Ordner mit diesem Namen existiert bereits", ), ( k::ERR_SAVE_FAILED, "die Datei konnte nicht gespeichert werden", ), (k::ERR_NO_FILES_UPLOADED, "keine Dateien hochgeladen"), (k::ERR_FILES_EXIST, "einige Dateien existieren bereits"), (k::ERR_BAD_UPLOAD_PATH, "ungültiger Dateipfad im Upload"), (k::ERR_NO_SUCH_FOLDER, "dieser Ordner existiert nicht"), (k::ERR_READ_ONLY_FOLDER, "schreibgeschützter Ordner"), ( k::ERR_RW_SHARES_DISABLED, "schreibbare Freigaben sind deaktiviert", ), ( k::ERR_RW_RO_FOLDER, "dieser Ordner ist für Sie schreibgeschützt und kann nicht schreibbar freigegeben werden", ), (k::ERR_FS_NOT_FOUND, "Ordner nicht gefunden"), (k::ERR_FS_NOT_A_DIR, "kein Ordner"), (k::ERR_FS_FORBIDDEN, "Zugriff verweigert"), ( k::ERR_FS_ROOT_MISSING, "der konfigurierte Ordner existiert nicht mehr", ), (k::ERR_FS_CONFLICT, "existiert bereits"), ( k::ERR_SEARCH_FORBIDDEN, "Suche erfordert eine angemeldete Sitzung", ), ( k::ERR_SEARCH_BAD_SCOPE, "Bereich muss name, content oder both sein", ), (k::ERR_ROOT_FORBIDDEN, "kein Zugriff auf diesen Ordner"), ]; /// French translations. const FR: &[(&str, &str)] = &[ ("no_folders_title", "Aucun dossier disponible"), ( "no_folders_msg", "Aucun administrateur ne vous a encore attribué de dossier.", ), ("select_folder_title", "Choisir un dossier"), ( "select_folder_msg", "Choisissez un dossier dans le panneau latéral pour commencer à naviguer.", ), ("folder_gone_title", "Dossier indisponible"), ( "folder_gone_msg", "Ce dossier n'est plus accessible. Choisissez-en un autre dans le panneau latéral.", ), ("loading", "Chargement…"), ("loading_share", "Chargement du partage…"), ("retry", "Réessayer"), ("empty_folder", "Ce dossier est vide."), ("refresh", "Actualiser"), ("grid_view", "Vue en grille"), ("list_view", "Vue en liste"), ("open", "Ouvrir"), ("open_in_new_tab", "Ouvrir dans un nouvel onglet"), ("download", "Télécharger"), ("rename", "Renommer"), ("move", "Déplacer"), ("copy", "Copier"), ("done", "Terminé"), ("share", "Partager"), ("info", "Infos"), ("delete", "Supprimer"), ("new_folder", "Nouveau dossier"), ("new_file", "Nouveau fichier"), ("upload_files", "Téléverser des fichiers"), ("upload_folder", "Téléverser un dossier"), ("read_only_hint", "Dossier en lecture seule"), ("read_only_badge", "Lecture seule"), ("more_actions", "Plus d'actions"), ("folder_name", "Nom du dossier"), ("file_name", "Nom du fichier"), ("create", "Créer"), ("rename_title", "Renommer « {} »"), ("new_name", "Nouveau nom"), ( "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.", ), ("cancel", "Annuler"), ( "overwrite_question", "Les fichiers suivants existent déjà. Les écraser ?", ), ("overwrite", "Écraser"), ("and_more", "…et {} autres"), ("no_subfolders", "Aucun sous-dossier"), ("move_to", "Déplacer « {} » vers…"), ("copy_to", "Copier « {} » vers…"), ("move_n_to", "Déplacer {} éléments vers…"), ("copy_n_to", "Copier {} éléments vers…"), ("move_here", "Déplacer ici"), ("copy_here", "Copier ici"), ("download_title", "Télécharger « {} »"), ( "download_formats_msg", "Choisissez un format d'archivage pour le dossier.", ), ("fmt_zip", "Zip"), ("fmt_tar", "Tar"), ("fmt_targz", "Tar + gzip"), ("fmt_tarzst", "Tar + zstd"), ("file_info", "Informations"), ("name", "Nom"), ("type", "Type"), ("folder", "Dossier"), ("file", "Fichier"), ("size", "Taille"), ("modified", "Modifié"), ("location", "Emplacement"), ("close", "Fermer"), ("folder_created", "Dossier créé"), ("file_created", "Fichier créé"), ("name_taken", "Ce nom est déjà utilisé"), ("renamed", "Renommé"), ("deleted", "Supprimé"), ("uploading", "Téléversement de {} fichier(s)…"), ("some_files_exist", "Certains fichiers existent déjà"), ("upload_complete", "Téléversement terminé"), ("moved", "Déplacé"), ("copied", "Copié"), ("download_started", "Téléchargement démarré"), ("saved", "Enregistré"), ("menu", "Menu"), ("open_menu", "Ouvrir le menu"), ("refresh_folders", "Actualiser vos dossiers"), ("theme_title", "Thème : {} (cliquer pour changer)"), ("log_out", "Se déconnecter"), ("files", "Fichiers"), ("manage", "Gérer"), ("shares", "Partages"), ("users", "Utilisateurs"), ("no_users", "Aucun utilisateur."), ("settings", "Paramètres"), ("theme_auto", "Auto"), ("theme_light", "Clair"), ("theme_dark", "Sombre"), ("search", "Rechercher"), ( "search_placeholder", "Rechercher dans les fichiers et leur contenu", ), ("search_name", "Nom"), ("search_content", "Contenu"), ("search_both", "Les deux"), ("search_goto", "Afficher dans le dossier"), ("search_run", "Rechercher"), ("search_in", "Rechercher dans…"), ("search_here", "Rechercher ici"), ("search_in_folder", "Rechercher dans ce dossier"), ("search_stop", "Arrêter"), ("searching", "recherche…"), ("search_n_results", "{} résultats"), ("search_took", "{} ms"), ("search_stopped", "arrêté · {} résultats"), ("search_scanned", "{} fichiers parcourus"), ("search_skipped", "{} ignorés (plus de 10 Mio)"), ("search_n_matches", "{} correspondances"), ("search_in_n_files", "dans {} fichiers"), ("search_collapse_all", "Tout replier"), ("search_expand_all", "Tout déplier"), ("search_shown_of", "{} sur {} affichés"), ("search_show_more", "Afficher {} de plus"), ("search_no_results", "aucun résultat"), ("saving", "Enregistrement…"), ("profile", "Profil"), ("server", "Serveur"), ("profile_saved", "Profil enregistré"), ( "profile_save_err", "Impossible d'enregistrer le profil : {}", ), ("settings_saved", "Paramètres enregistrés"), ( "settings_save_err", "Impossible d'enregistrer les paramètres : {}", ), ("single_click_label", "Ouvrir au clic simple"), ( "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.", ), ("save_profile", "Enregistrer le profil"), ("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.", ), ("save_settings", "Enregistrer les paramètres"), ("language", "Langue"), ( "language_desc", "La langue de l'interface. « Automatique » utilise la langue de votre navigateur.", ), ("lang_auto", "Automatique"), ("lang_en", "Anglais"), ("lang_de", "Allemand"), ("lang_fr", "Français"), ( "delete_user_confirm", "Supprimer l'utilisateur « {} » ? Cette action est irréversible.", ), ("user_delete_err", "Impossible de supprimer l'utilisateur"), ("new_user", "Nouvel utilisateur"), ("edit", "Modifier"), ("delete_user", "Supprimer l'utilisateur"), ( "cant_delete_self", "Vous ne pouvez pas supprimer votre propre compte", ), ("root_dup_err", "Ce dossier est déjà dans la liste."), ("name_required", "Un nom est requis."), ( "pw_short", "Le mot de passe doit contenir au moins 8 caractères.", ), ("need_folder", "Donnez au moins un dossier à l'utilisateur."), ("user_created", "Utilisateur créé"), ("user_updated", "Utilisateur mis à jour"), ( "user_save_err", "Impossible d'enregistrer l'utilisateur : {}", ), ("password", "Mot de passe"), ( "new_password_keep", "Nouveau mot de passe (laisser vide pour conserver)", ), ("administrator", "Administrateur"), ("active_can_signin", "Actif (peut se connecter)"), ("remove_folder", "Retirer le dossier"), ("create_user", "Créer l'utilisateur"), ("save", "Enregistrer"), ("edit_user_title", "Modifier « {} »"), ("folders", "Dossiers"), ( "folders_paths_hint", "Les chemins sont relatifs à la racine du serveur. « . » désigne toute la racine.", ), ("add_folder", "Ajouter un dossier…"), ("add_here", "Ajouter ce dossier"), ("server_root", "Racine du serveur"), ("server_unreachable", "Le serveur est injoignable."), ("search_failed", "La recherche a échoué."), ("mode_rw", "lecture-écriture"), ("expiry_1h", "1 heure"), ("expiry_1d", "1 jour"), ("expiry_1w", "1 semaine"), ("expiry_1mo", "1 mois"), ("meta_expires", "expire le {}"), ("meta_created", "créé le {}"), ("mode_ro", "lecture seule"), ("share_not_found", "Partage introuvable"), ( "share_not_found_msg", "Ce lien est invalide ou le partage a été supprimé.", ), ("share_expired", "Partage expiré"), ("share_expired_msg", "Ce lien de partage n'est plus actif."), ("copy_manual", "Copiez le lien manuellement"), ("link_copied", "Lien copié dans le presse-papiers"), ("share_create_err", "Impossible de créer le partage : {}"), ("share_link", "Lien de partage"), ( "share_perm_rw", "Toute personne possédant ce lien peut lire et modifier « {} ».", ), ( "share_perm_ro", "Toute personne possédant ce lien peut lire « {} ».", ), ("share_title", "Partager {}"), ( "share_dialog_hint", "Crée un lien pour cet élément. Les liens modifiables permettent aux destinataires de téléverser et de modifier.", ), ("expires", "Expire"), ("never", "Jamais"), ("custom", "Personnalisé…"), ("until", "Jusqu'au"), ("expires_at", "Expire le {}"), ( "delete_partial", "{} des {} liens n'ont pas pu être supprimés", ), ("never_expires", "N'expire jamais"), ( "allow_editing", "Autoriser la modification (lecture-écriture)", ), ( "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.", ), ("creating", "Création…"), ("create_link", "Créer le lien"), ("your_shares", "Vos partages"), ("deleted_n_shares", "{} partage(s) supprimé(s)"), ("shares_delete_err", "Impossible de supprimer les partages"), ("delete_all", "Tout supprimer"), ("no_shares", "Aucun partage pour l'instant"), ( "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.", ), ( "shares_empty_hint", "Faites un clic droit sur un fichier ou un dossier, puis choisissez « Partager » pour créer un lien.", ), ("copy_link", "Copier le lien"), ("delete_share", "Supprimer le partage"), ("share_deleted", "Partage supprimé"), ("share_delete_err", "Impossible de supprimer le partage"), ("delete_all_title", "Supprimer tous les partages ?"), ( "delete_all_msg", "Supprimer les {} liens de partage ? Toute personne les possédant perdra l'accès. Cette action est irréversible.", ), ("login_subtitle", "Connectez-vous pour continuer."), ( "login_error", "Veuillez saisir votre nom et votre mot de passe.", ), ("sign_in", "Se connecter"), ( "setup_name_err", "Veuillez choisir un nom (1 à 64 caractères).", ), ( "setup_pw_mismatch", "Les mots de passe ne correspondent pas.", ), ("repeat_password", "Répéter le mot de passe"), ("create_admin", "Créer le compte administrateur"), ( "setup_intro", "Premier démarrage : créez le compte administrateur. Cet utilisateur peut gérer les autres utilisateurs et les paramètres du serveur.", ), ("close_esc", "Fermer (Échap)"), ("no_preview", "Aperçu non disponible"), ( "no_preview_msg", "L'application ne peut pas afficher « {} » ici.", ), ("dismiss", "Ignorer"), ("dismiss_error", "Ignorer l'erreur"), ("file_changed", "Fichier modifié sur disque"), ( "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.", ), ("load_latest", "Charger la version la plus récente"), ("keep_editing", "Continuer à modifier"), ("discard", "Abandonner"), ("unsaved_changes", "Modifications non enregistrées"), ( "discard_question", "Abandonner vos modifications et fermer l'éditeur ?", ), (k::ERR_INTERNAL, "erreur interne"), (k::ERR_NOT_SIGNED_IN, "non connecté"), ( k::ERR_SESSION_EXPIRED, "session expirée, veuillez vous connecter à nouveau", ), (k::ERR_INVALID_CREDENTIALS, "nom ou mot de passe incorrect"), (k::ERR_ALREADY_SET_UP, "le serveur est déjà configuré"), (k::ERR_SHARE_NOT_FOUND, "partage introuvable"), (k::ERR_SHARE_EXPIRED, "ce partage a expiré"), (k::ERR_BAD_EXPIRES_AT, "la date d'expiration est invalide"), ( k::ERR_SHARE_TOKEN_FORBIDDEN, "un jeton de partage ne peut pas être utilisé ici ; connectez-vous à la place", ), (k::ERR_NAME_LENGTH, "le nom doit contenir 1 à 64 caractères"), ( k::ERR_PASSWORD_SHORT, "le mot de passe doit contenir au moins 8 caractères", ), (k::ERR_ADMIN_ONLY, "réservé aux administrateurs"), (k::ERR_USER_EXISTS, "un utilisateur avec ce nom existe déjà"), (k::ERR_USER_NOT_FOUND, "utilisateur introuvable"), ( k::ERR_OWN_ADMIN, "vous ne pouvez pas retirer vos propres droits d'administrateur", ), ( k::ERR_OWN_ACCOUNT, "vous ne pouvez pas désactiver votre propre compte", ), ( k::ERR_LAST_ADMIN, "impossible de supprimer le dernier administrateur actif", ), ( k::ERR_OWN_DELETE, "vous ne pouvez pas supprimer votre propre compte", ), ( k::ERR_LAST_ADMIN_DELETE, "impossible de supprimer le dernier administrateur actif", ), ( k::ERR_BAD_FORMAT, "le format doit être zip, tar, tar.gz ou tar.zst", ), (k::ERR_NOT_A_FILE, "ce n'est pas un fichier"), ( k::ERR_TOO_LARGE_PREVIEW, "fichier trop volumineux pour l'aperçu", ), (k::ERR_BAD_ACTION, "action=content attendu"), ( k::ERR_TOO_LARGE_SAVE, "fichier trop volumineux pour être enregistré", ), (k::ERR_BAD_BODY, "corps de requête invalide"), (k::ERR_INVALID_LANGUAGE, "balise de langue invalide"), ( k::ERR_BAD_POST, "POST attend un téléversement multipart, une mutation JSON ou ?action=mkdir/create-file", ), (k::ERR_FOLDER_NAME_REQUIRED, "un nom de dossier est requis"), (k::ERR_FILE_NAME_REQUIRED, "un nom de fichier est requis"), (k::ERR_NEW_NAME_REQUIRED, "new_name est requis"), (k::ERR_DST_REQUIRED, "dst_root_id est requis"), (k::ERR_PATH_REQUIRED, "un chemin dans le dossier est requis"), ( k::ERR_BAD_MULTIPART, "multipart/form-data avec boundary attendu", ), (k::ERR_BAD_UPLOAD, "données de téléversement invalides"), (k::ERR_PART_NO_NAME, "partie sans nom"), (k::ERR_BAD_PART_NAME, "nom de partie invalide"), (k::ERR_FOLDER_EXISTS, "un dossier avec ce nom existe déjà"), (k::ERR_SAVE_FAILED, "impossible d'enregistrer le fichier"), (k::ERR_NO_FILES_UPLOADED, "aucun fichier n'a été téléversé"), (k::ERR_FILES_EXIST, "certains fichiers existent déjà"), ( k::ERR_BAD_UPLOAD_PATH, "chemin de fichier invalide dans le téléversement", ), (k::ERR_NO_SUCH_FOLDER, "ce dossier n'existe pas"), (k::ERR_READ_ONLY_FOLDER, "dossier en lecture seule"), ( k::ERR_RW_SHARES_DISABLED, "les partages modifiables sont désactivés", ), ( k::ERR_RW_RO_FOLDER, "ce dossier est en lecture seule pour vous, il ne peut donc pas être partagé en écriture", ), (k::ERR_FS_NOT_FOUND, "dossier introuvable"), (k::ERR_FS_NOT_A_DIR, "ce n'est pas un dossier"), (k::ERR_FS_FORBIDDEN, "accès refusé"), (k::ERR_FS_ROOT_MISSING, "le dossier configuré n'existe plus"), (k::ERR_FS_CONFLICT, "existe déjà"), ( k::ERR_SEARCH_FORBIDDEN, "la recherche nécessite une session connectée", ), ( k::ERR_SEARCH_BAD_SCOPE, "la portée doit être name, content ou both", ), (k::ERR_ROOT_FORBIDDEN, "dossier inaccessible"), ]; #[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 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}"); } } } /// The parity test above only compares the tables against each other, so /// a key missing from *all* of them (e.g. a `k` constant added without /// its translations) slips through. Scan the `k` module's source for /// `pub const NAME: &str = "key";` and require every key in `EN`. #[test] fn all_k_constants_exist_in_en() { let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/i18n.rs")) .expect("read i18n.rs"); let k_start = src.find("pub mod k {").expect("k module"); // The k module's closing brace is the first column-0 `}` after it. let k_end = k_start + src[k_start..].find("\n}").expect("k module end"); // Lines look like: `pub const NAME: &str = "key";` let parse_line = |l: &str| { let l = l.trim(); let mut parts = l.splitn(3, char::is_whitespace); let _kw = parts.next()?; let _const_kw = parts.next()?; let rest = parts.next()?; // `NAME: &str = "key";` let (name_ty, value) = rest.split_once('=')?; let ty = name_ty.rsplit_once(':')?.1.trim(); let val = value.trim().trim_end_matches(';').trim().trim_matches('"'); (ty == "&str").then_some(val.to_string()) }; let keys: Vec = src[k_start..k_end] .lines() .filter_map(parse_line) .filter(|k| !k.is_empty()) .collect(); assert!(keys.len() > 100, "scan found only {} keys", keys.len()); let missing: Vec<&String> = keys .iter() .filter(|k| EN.iter().all(|(t, _)| *t != k.as_str())) .collect(); assert!( missing.is_empty(), "k constants without an EN translation: {missing:?}" ); } }