i18n.rs
⎇
Raw
1//! Lightweight i18n: one central registry (in the spirit of `icons.rs`).
2//!
3//! `EN` is the canonical list of user-visible strings; it doubles as the key
4//! table. `DE` and `FR` translate the same keys; a key missing from a
5//! translation falls back to English.
6//!
7//! - `t(key)` resolves a key for the current locale. Non-reactive — for
8//! toasts, event handlers, and one-shot constructions (dialogs).
9//! - `tr(key)` is the same as a reactive closure, for `view!` children and
10//! attributes, so switching the language re-renders in place.
11//! - `error_text(code, fallback)` maps a server error *code* to a
12//! translation; unknown or absent codes fall back to the server message.
13//!
14//! Parameterized strings carry exactly one `{}` placeholder; callers do
15//! `format!(t(k::X), arg)`.
16//!
17//! Every table is sorted by key so lookups can binary-search.
18//!
19//! Locale precedence: the signed-in user's profile setting (persisted
20//! server-side), else the browser language. `init` + `set_locale` drive a
21//! global signal so every `tr` subscriber updates.
22
23use std::sync::OnceLock;
24
25use leptos::prelude::*;
26use wasm_bindgen::JsCast;
27
28/// A supported UI language.
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30pub enum Locale {
31 En,
32 De,
33 Fr,
34}
35
36impl Locale {
37 pub const fn code(self) -> &'static str {
38 match self {
39 Locale::En => "en",
40 Locale::De => "de",
41 Locale::Fr => "fr",
42 }
43 }
44
45 /// Parse a BCP-47-ish tag ("de", "de-AT", "DE") into a supported locale.
46 pub fn from_code(code: &str) -> Option<Self> {
47 let lang = code.split(['-', '_']).next()?.to_ascii_lowercase();
48 match lang.as_str() {
49 "en" => Some(Locale::En),
50 "de" => Some(Locale::De),
51 "fr" => Some(Locale::Fr),
52 _ => None,
53 }
54 }
55
56 /// The browser's language, restricted to the supported locales.
57 pub fn from_browser() -> Option<Self> {
58 let lang = web_sys::window()?.navigator().language()?;
59 Self::from_code(&lang)
60 }
61}
62
63static READ: OnceLock<ReadSignal<Locale>> = OnceLock::new();
64static WRITE: OnceLock<WriteSignal<Locale>> = OnceLock::new();
65
66/// Called once during app setup, before the first UI render.
67pub fn init(locale: Locale) {
68 let (read, write) = signal(locale);
69 let _ = READ.set(read);
70 let _ = WRITE.set(write);
71 apply_doc_lang(locale);
72}
73
74pub fn set_locale(locale: Locale) {
75 if let Some(w) = WRITE.get() {
76 w.set(locale);
77 }
78 apply_doc_lang(locale);
79}
80
81pub fn current() -> Locale {
82 READ.get().map(|r| r.get()).unwrap_or(Locale::En)
83}
84
85/// Like [`current`] but without registering a reactive dependency — for
86/// code that reads the locale only to compare before writing (e.g. the
87/// profile effect in `app.rs`), so the read cannot retrigger itself.
88pub fn current_untracked() -> Locale {
89 READ.get().map(|r| r.get_untracked()).unwrap_or(Locale::En)
90}
91
92/// Keep `<html lang>` in sync for screen readers and the browser UI.
93fn apply_doc_lang(locale: Locale) {
94 if let Some(html) = web_sys::window()
95 .and_then(|w| w.document())
96 .and_then(|d| d.document_element())
97 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok())
98 {
99 let _ = html.set_attribute("lang", locale.code());
100 }
101}
102
103/// Resolve a key for the current locale (non-reactive).
104pub fn t(key: &str) -> &'static str {
105 text(key, current())
106}
107
108/// Resolve a key for the current locale as a reactive closure, for use as a
109/// `view!` child or attribute value.
110pub fn tr(key: &str) -> impl Fn() -> String + Clone {
111 move || t(key).to_string()
112}
113
114/// Like [`t`], but substitutes a single `{}` placeholder in the translation
115/// with `arg`.
116pub fn t_fmt(key: &str, arg: &str) -> String {
117 t(key).replace("{}", arg)
118}
119
120/// Like [`t_fmt`] for translations with two placeholders, filling the first
121/// `{}` with `a` and the second with `b`. Positional, so a locale that needs
122/// the other order has to reword rather than swap.
123pub fn t_fmt2(key: &str, a: &str, b: &str) -> String {
124 let s = t(key);
125 match s.split_once("{}") {
126 Some((head, tail)) => format!("{head}{a}{}", tail.replacen("{}", b, 1)),
127 None => s.to_string(),
128 }
129}
130
131/// Localize a server error: a known code maps to its translation, anything
132/// else (no code, unknown code) falls back to the server's English message.
133pub fn error_text(code: Option<&str>, fallback: &str) -> String {
134 let Some(code) = code.filter(|c| find(EN, c).is_some()) else {
135 return fallback.to_string();
136 };
137 // One-shot lookup for a toast message: no reactive context, so read the
138 // locale untracked.
139 text(code, current_untracked()).to_string()
140}
141
142fn text(key: &str, locale: Locale) -> &'static str {
143 // Keys are compile-time constants from `k`; a miss is a bug (the unit
144 // tests pin the tables against each other).
145 match locale {
146 Locale::En => find(EN, key).expect("unknown i18n key"),
147 Locale::De => find(DE, key)
148 .or_else(|| find(EN, key))
149 .expect("unknown i18n key"),
150 Locale::Fr => find(FR, key)
151 .or_else(|| find(EN, key))
152 .expect("unknown i18n key"),
153 }
154}
155
156/// The tables are sorted by key, so a lookup is a binary search. The
157/// `tables_are_sorted` test keeps that invariant true.
158fn find(table: &'static [(&'static str, &'static str)], key: &str) -> Option<&'static str> {
159 table
160 .binary_search_by(|(k, _)| (*k).cmp(key))
161 .ok()
162 .map(|i| table[i].1)
163}
164
165/// Declares the translation keys and the canonical English text in one
166/// place: one row per key, `CONST = "key" => "English"`.
167///
168/// The `k` constants and the `EN` table are generated from the same row, so
169/// a key cannot exist in one without the other. Rows are sorted by key,
170/// because [`find`] binary-searches (`tables_are_sorted` pins that).
171macro_rules! i18n_keys {
172 ($($name:ident = $key:literal => $en:expr),* $(,)?) => {
173 /// Translation keys. Use these, never the raw strings: a typo in a
174 /// key is a compile error, a typo in a string is a runtime panic.
175 pub mod k {
176 $(pub const $name: &str = $key;)*
177 }
178
179 /// Canonical English strings (key → text). The source of truth for
180 /// which keys exist; DE and FR translate the same list.
181 const EN: &[(&str, &str)] = &[$(($key, $en)),*];
182 };
183}
184
185i18n_keys! {
186 ACTIVE_CAN_SIGNIN = "active_can_signin" => "Active (can sign in)",
187 ADD_FOLDER = "add_folder" => "Add folder…",
188 ADD_HERE = "add_here" => "Add this folder",
189 ADMINISTRATOR = "administrator" => "Administrator",
190 ALLOW_EDITING = "allow_editing" => "Allow editing (read-write)",
191 ALLOW_RW_SHARES = "allow_rw_shares" => "Allow writable shares",
192 ALLOW_RW_SHARES_DESC = "allow_rw_shares_desc" => "Let users create read-write share links. Off by default.",
193 AND_MORE = "and_more" => "…and {} more",
194 CANCEL = "cancel" => "Cancel",
195 CANT_DELETE_SELF = "cant_delete_self" => "You cannot delete yourself",
196 CLOSE = "close" => "Close",
197 CLOSE_ESC = "close_esc" => "Close (Esc)",
198 COPIED = "copied" => "Copied",
199 COPY = "copy" => "Copy",
200 COPY_DAV_LINK = "copy_dav_link" => "Copy WebDAV link",
201 COPY_HERE = "copy_here" => "Copy here",
202 COPY_LINK = "copy_link" => "Copy link",
203 COPY_MANUAL = "copy_manual" => "Copy the link manually",
204 COPY_N_TO = "copy_n_to" => "Copy {} items to…",
205 COPY_TO = "copy_to" => "Copy “{}” to…",
206 CREATE = "create" => "Create",
207 CREATE_ADMIN = "create_admin" => "Create admin account",
208 CREATE_LINK = "create_link" => "Create link",
209 CREATE_USER = "create_user" => "Create user",
210 CREATING = "creating" => "Creating…",
211 CUSTOM = "custom" => "Custom…",
212 DELETE = "delete" => "Delete",
213 DELETE_ALL = "delete_all" => "Delete all",
214 DELETE_ALL_MSG = "delete_all_msg" => "Delete all {} share links? Anyone holding them loses access. This cannot be undone.",
215 DELETE_ALL_TITLE = "delete_all_title" => "Delete all shares?",
216 DELETE_FOLDER_MSG = "delete_folder_msg" => "Delete folder “{}” and everything inside it?\nThis cannot be undone.",
217 DELETE_MSG = "delete_msg" => "Delete “{}”?\nThis cannot be undone.",
218 DELETE_N_MSG = "delete_n_msg" => "Delete {} items?\nThis cannot be undone.",
219 DELETE_PARTIAL = "delete_partial" => "{} of {} shares could not be deleted",
220 DELETE_SHARE = "delete_share" => "Delete share",
221 DELETE_USER = "delete_user" => "Delete user",
222 DELETE_USER_CONFIRM = "delete_user_confirm" => "Delete user “{}”? This cannot be undone.",
223 DELETED = "deleted" => "Deleted",
224 DELETED_N_SHARES = "deleted_n_shares" => "Deleted {} shares",
225 DISCARD = "discard" => "Discard",
226 DISCARD_QUESTION = "discard_question" => "Discard your changes and close the editor?",
227 DISMISS = "dismiss" => "Dismiss",
228 DISMISS_ERROR = "dismiss_error" => "Dismiss error",
229 DONE = "done" => "Done",
230 DOWNLOAD = "download" => "Download",
231 DOWNLOAD_FORMATS_MSG = "download_formats_msg" => "Choose an archive format for the folder.",
232 DOWNLOAD_STARTED = "download_started" => "Download started",
233 DOWNLOAD_TITLE = "download_title" => "Download “{}”",
234 EDIT = "edit" => "Edit",
235 EDIT_USER_TITLE = "edit_user_title" => "Edit “{}”",
236 EMPTY_FOLDER = "empty_folder" => "This folder is empty.",
237 ERR_ADMIN_ONLY = "err_admin_only" => "admin only",
238 ERR_ALREADY_SET_UP = "err_already_set_up" => "server is already set up",
239 ERR_BAD_ACTION = "err_bad_action" => "expected action=content",
240 ERR_BAD_BODY = "err_bad_body" => "invalid request body",
241 ERR_BAD_EXPIRES_AT = "err_bad_expires_at" => "the expiry date is invalid",
242 ERR_BAD_FORMAT = "err_bad_format" => "format must be one of: zip, tar, tar.gz, tar.zst",
243 ERR_BAD_MULTIPART = "err_bad_multipart" => "expected multipart/form-data with a boundary",
244 ERR_BAD_PART_NAME = "err_bad_part_name" => "invalid part name",
245 ERR_BAD_POST = "err_bad_post" => "POST expects a multipart upload, a JSON mutation, or ?action=mkdir/create-file",
246 ERR_BAD_UPLOAD = "err_bad_upload" => "invalid upload data",
247 ERR_BAD_UPLOAD_PATH = "err_bad_upload_path" => "invalid file path in upload",
248 ERR_DST_REQUIRED = "err_dst_required" => "dst_root_id is required",
249 ERR_FILE_NAME_REQUIRED = "err_file_name_required" => "a file name is required",
250 ERR_FILES_EXIST = "err_files_exist" => "some files already exist",
251 ERR_FOLDER_EXISTS = "err_folder_exists" => "a folder with this name already exists",
252 ERR_FOLDER_NAME_REQUIRED = "err_folder_name_required" => "a folder name is required",
253 ERR_FS_CONFLICT = "err_fs_conflict" => "already exists",
254 ERR_FS_FORBIDDEN = "err_fs_forbidden" => "access denied",
255 ERR_FS_NOT_A_DIR = "err_fs_not_a_dir" => "not a folder",
256 ERR_FS_NOT_FOUND = "err_fs_not_found" => "folder not found",
257 ERR_FS_ROOT_MISSING = "err_fs_root_missing" => "the configured folder no longer exists",
258 ERR_INTERNAL = "err_internal" => "internal error",
259 ERR_INVALID_CREDENTIALS = "err_invalid_credentials" => "invalid name or password",
260 ERR_INVALID_LANGUAGE = "err_invalid_language" => "invalid language tag",
261 ERR_LAST_ADMIN = "err_last_admin" => "cannot remove the last active admin",
262 ERR_LAST_ADMIN_DELETE = "err_last_admin_delete" => "cannot delete the last active admin",
263 ERR_NAME_LENGTH = "err_name_length" => "name must be 1–64 characters",
264 ERR_NEW_NAME_REQUIRED = "err_new_name_required" => "new_name is required",
265 ERR_NO_FILES_UPLOADED = "err_no_files_uploaded" => "no files were uploaded",
266 ERR_NO_SUCH_FOLDER = "err_no_such_folder" => "no such folder",
267 ERR_NOT_A_FILE = "err_not_a_file" => "not a file",
268 ERR_NOT_SIGNED_IN = "err_not_signed_in" => "not signed in",
269 ERR_OWN_ACCOUNT = "err_own_account" => "you cannot disable your own account",
270 ERR_OWN_ADMIN = "err_own_admin" => "you cannot remove your own admin rights",
271 ERR_OWN_DELETE = "err_own_delete" => "you cannot delete your own account",
272 ERR_PART_NO_NAME = "err_part_no_name" => "part without a name",
273 ERR_PASSWORD_SHORT = "err_password_short" => "password must be at least 8 characters",
274 ERR_PATH_REQUIRED = "err_path_required" => "a path inside the folder is required",
275 ERR_READ_ONLY_FOLDER = "err_read_only_folder" => "read-only folder",
276 ERR_ROOT_FORBIDDEN = "err_root_forbidden" => "root not accessible",
277 ERR_RW_RO_FOLDER = "err_rw_ro_folder" => "this folder is read-only for you, so it cannot be shared writably",
278 ERR_RW_SHARES_DISABLED = "err_rw_shares_disabled" => "writable shares are disabled",
279 ERR_SAVE_FAILED = "err_save_failed" => "could not save the file",
280 ERR_SEARCH_BAD_SCOPE = "err_search_bad_scope" => "scope must be name, content or both",
281 ERR_SEARCH_FORBIDDEN = "err_search_forbidden" => "search requires a signed-in session",
282 ERR_SESSION_EXPIRED = "err_session_expired" => "session expired, please sign in again",
283 ERR_SHARE_EXPIRED = "err_share_expired" => "this share has expired",
284 ERR_SHARE_LOCKED = "err_share_locked" => "This share is password protected.",
285 ERR_SHARE_NO_PASSWORD = "err_share_no_password" => "This share has no password.",
286 ERR_SHARE_NOT_FOUND = "err_share_not_found" => "share not found",
287 ERR_SHARE_TOKEN_FORBIDDEN = "err_share_token_forbidden" => "a share token cannot be used here; sign in instead",
288 ERR_SHARE_WRONG_PASSWORD = "err_share_wrong_password" => "Wrong password.",
289 ERR_TOO_LARGE_PREVIEW = "err_too_large_preview" => "file too large to preview",
290 ERR_TOO_LARGE_SAVE = "err_too_large_save" => "file too large to save",
291 ERR_USER_EXISTS = "err_user_exists" => "a user with that name already exists",
292 ERR_USER_NOT_FOUND = "err_user_not_found" => "user not found",
293 EXCLUDE_DUP_ERR = "exclude_dup_err" => "That folder is already excluded.",
294 EXCLUDE_FOLDER = "exclude_folder" => "Exclude folder…",
295 EXCLUDE_HERE = "exclude_here" => "Exclude this folder",
296 EXCLUDE_ROOT_ERR = "exclude_root_err" => "The whole root cannot be excluded; that would turn search off.",
297 EXPIRES = "expires" => "Expires",
298 EXPIRES_AT = "expires_at" => "Expires {}",
299 EXPIRY_1D = "expiry_1d" => "1 day",
300 EXPIRY_1H = "expiry_1h" => "1 hour",
301 EXPIRY_1MO = "expiry_1mo" => "1 month",
302 EXPIRY_1W = "expiry_1w" => "1 week",
303 FILE = "file" => "File",
304 FILE_CHANGED = "file_changed" => "File changed on disk",
305 FILE_CREATED = "file_created" => "File created",
306 FILE_INFO = "file_info" => "File info",
307 FILE_NAME = "file_name" => "File name",
308 FILES = "files" => "Files",
309 FMT_TAR = "fmt_tar" => "Tar",
310 FMT_TARGZ = "fmt_targz" => "Tar + gzip",
311 FMT_TARZST = "fmt_tarzst" => "Tar + zstd",
312 FMT_ZIP = "fmt_zip" => "Zip",
313 FOLDER = "folder" => "Folder",
314 FOLDER_CREATED = "folder_created" => "Folder created",
315 FOLDER_GONE_MSG = "folder_gone_msg" => "This folder is no longer accessible. Choose another one from the sidebar.",
316 FOLDER_GONE_TITLE = "folder_gone_title" => "Folder not available",
317 FOLDER_NAME = "folder_name" => "Folder name",
318 FOLDERS = "folders" => "Folders",
319 FOLDERS_PATHS_HINT = "folders_paths_hint" => "Paths are relative to the server root. “.” is the whole root.",
320 GRID_VIEW = "grid_view" => "Grid view",
321 INFO = "info" => "Info",
322 KEEP_EDITING = "keep_editing" => "Keep editing",
323 LANG_AUTO = "lang_auto" => "Automatic",
324 LANG_DE = "lang_de" => "German",
325 LANG_EN = "lang_en" => "English",
326 LANG_FR = "lang_fr" => "French",
327 LANGUAGE = "language" => "Language",
328 LANGUAGE_DESC = "language_desc" => "The interface language. Automatic uses your browser's language.",
329 LINK_COPIED = "link_copied" => "Link copied to clipboard",
330 LIST_TRUNCATED = "list_truncated" => "Only the first {} entries are shown.",
331 LIST_VIEW = "list_view" => "List view",
332 LOAD_LATEST = "load_latest" => "Load latest",
333 LOADING = "loading" => "Loading…",
334 LOADING_SHARE = "loading_share" => "Loading share…",
335 LOCATION = "location" => "Location",
336 LOG_OUT = "log_out" => "Log out",
337 LOGIN_ERROR = "login_error" => "Please enter your name and password.",
338 LOGIN_SUBTITLE = "login_subtitle" => "Sign in to continue.",
339 MANAGE = "manage" => "Manage",
340 MENU = "menu" => "Menu",
341 META_CREATED = "meta_created" => "created {}",
342 META_EXPIRES = "meta_expires" => "expires {}",
343 MODE_RO = "mode_ro" => "read-only",
344 MODE_RW = "mode_rw" => "read-write",
345 MODIFIED = "modified" => "Modified",
346 MODIFIED_AFTER = "modified_after" => "This file was modified after you opened it. “Load latest” discards your unsaved changes; “Overwrite” keeps your version.",
347 MORE_ACTIONS = "more_actions" => "More actions",
348 MOVE = "move" => "Move",
349 MOVE_HERE = "move_here" => "Move here",
350 MOVE_N_TO = "move_n_to" => "Move {} items to…",
351 MOVE_TO = "move_to" => "Move “{}” to…",
352 MOVED = "moved" => "Moved",
353 NAME = "name" => "Name",
354 NAME_REQUIRED = "name_required" => "A name is required.",
355 NAME_TAKEN = "name_taken" => "Name already in use",
356 NEED_FOLDER = "need_folder" => "Give the user at least one folder.",
357 NEVER = "never" => "Never",
358 NEVER_EXPIRES = "never_expires" => "Never expires",
359 NEW_FILE = "new_file" => "New file",
360 NEW_FOLDER = "new_folder" => "New folder",
361 NEW_NAME = "new_name" => "New name",
362 NEW_PASSWORD_KEEP = "new_password_keep" => "New password (leave blank to keep)",
363 NEW_USER = "new_user" => "New user",
364 NO_FOLDERS_MSG = "no_folders_msg" => "An administrator hasn't assigned you any folders yet.",
365 NO_FOLDERS_TITLE = "no_folders_title" => "No folders available",
366 NO_PREVIEW = "no_preview" => "No preview available",
367 NO_PREVIEW_MSG = "no_preview_msg" => "The app can’t display “{}” here.",
368 NO_SHARES = "no_shares" => "No shares yet",
369 NO_SUBFOLDERS = "no_subfolders" => "No subfolders",
370 NO_USERS = "no_users" => "No users.",
371 OPEN = "open" => "Open",
372 OPEN_IN_NEW_TAB = "open_in_new_tab" => "Open in new tab",
373 OPEN_MENU = "open_menu" => "Open menu",
374 OVERWRITE = "overwrite" => "Overwrite",
375 OVERWRITE_QUESTION = "overwrite_question" => "The following files already exist. Overwrite them?",
376 PASSWORD = "password" => "Password",
377 PROFILE = "profile" => "Profile",
378 PROFILE_SAVE_ERR = "profile_save_err" => "Could not save profile: {}",
379 PROFILE_SAVED = "profile_saved" => "Profile saved",
380 PW_SHORT = "pw_short" => "Password must be at least 8 characters.",
381 READ_ONLY_BADGE = "read_only_badge" => "Read-only",
382 READ_ONLY_HINT = "read_only_hint" => "Read-only folder",
383 REFRESH = "refresh" => "Refresh",
384 REFRESH_FOLDERS = "refresh_folders" => "Refresh your folders",
385 REMOVE_FOLDER = "remove_folder" => "Remove folder",
386 RENAME = "rename" => "Rename",
387 RENAME_TITLE = "rename_title" => "Rename “{}”",
388 RENAMED = "renamed" => "Renamed",
389 REPEAT_PASSWORD = "repeat_password" => "Repeat password",
390 RETRY = "retry" => "Retry",
391 ROOT_DUP_ERR = "root_dup_err" => "That folder is already in the list.",
392 RW_DISABLED = "rw_disabled" => "Writable shares are disabled by the server.",
393 RW_RO_FOLDER = "rw_ro_folder" => "This folder is read-only, so the share is read-only too.",
394 SAVE = "save" => "Save",
395 SAVE_PROFILE = "save_profile" => "Save profile",
396 SAVE_SETTINGS = "save_settings" => "Save settings",
397 SAVED = "saved" => "Saved",
398 SAVING = "saving" => "Saving…",
399 SEARCH = "search" => "Search",
400 SEARCH_BOTH = "search_both" => "Both",
401 SEARCH_COLLAPSE_ALL = "search_collapse_all" => "Collapse all",
402 SEARCH_CONTENT = "search_content" => "Content",
403 SEARCH_EXCLUDES = "search_excludes" => "Excluded from search",
404 SEARCH_EXCLUDES_EMPTY = "search_excludes_empty" => "No folders are excluded.",
405 SEARCH_EXPAND_ALL = "search_expand_all" => "Expand all",
406 SEARCH_FAILED = "search_failed" => "The search failed.",
407 SEARCH_GOTO = "search_goto" => "Show in folder",
408 SEARCH_HERE = "search_here" => "Search here",
409 SEARCH_IN = "search_in" => "Search in…",
410 SEARCH_IN_FOLDER = "search_in_folder" => "Search in this folder",
411 SEARCH_IN_N_FILES = "search_in_n_files" => "in {} files",
412 SEARCH_N_MATCHES = "search_n_matches" => "{} matches",
413 SEARCH_N_RESULTS = "search_n_results" => "{} results",
414 SEARCH_NAME = "search_name" => "Name",
415 SEARCH_NO_RESULTS = "search_no_results" => "no results",
416 SEARCH_PLACEHOLDER = "search_placeholder" => "Search files and their contents",
417 SEARCH_RUN = "search_run" => "Search",
418 SEARCH_SCANNED = "search_scanned" => "scanned {} files",
419 SEARCH_SHOW_MORE = "search_show_more" => "Show {} more",
420 SEARCH_SHOWN_OF = "search_shown_of" => "Showing {} of {}",
421 SEARCH_SKIPPED = "search_skipped" => "{} skipped (over 10 MiB)",
422 SEARCH_STOP = "search_stop" => "Stop",
423 SEARCH_STOPPED = "search_stopped" => "stopped · {} results",
424 SEARCH_TOOK = "search_took" => "{} ms",
425 SEARCHING = "searching" => "searching…",
426 SELECT_FOLDER_MSG = "select_folder_msg" => "Choose a folder from the sidebar to start browsing.",
427 SELECT_FOLDER_TITLE = "select_folder_title" => "Select a folder",
428 SERVER = "server" => "Server",
429 SERVER_ROOT = "server_root" => "Server root",
430 SERVER_UNREACHABLE = "server_unreachable" => "The server is not reachable.",
431 SETTINGS = "settings" => "Settings",
432 SETTINGS_SAVE_ERR = "settings_save_err" => "Could not save settings: {}",
433 SETTINGS_SAVED = "settings_saved" => "Settings saved",
434 SETUP_INTRO = "setup_intro" => "First boot: create the admin account. This user can manage other users and server settings.",
435 SETUP_NAME_ERR = "setup_name_err" => "Please choose a name (1–64 characters).",
436 SETUP_PW_MISMATCH = "setup_pw_mismatch" => "Passwords don't match.",
437 SHARE = "share" => "Share",
438 SHARE_CREATE_ERR = "share_create_err" => "Could not create share: {}",
439 SHARE_DELETE_ERR = "share_delete_err" => "Could not delete share",
440 SHARE_DELETED = "share_deleted" => "Share deleted",
441 SHARE_DIALOG_HINT = "share_dialog_hint" => "Create a link for this item. Writable links let viewers upload and edit.",
442 SHARE_EXPIRED = "share_expired" => "Share expired",
443 SHARE_EXPIRED_MSG = "share_expired_msg" => "This share link is no longer active.",
444 SHARE_LINK = "share_link" => "Share link",
445 SHARE_LOCKED_TITLE = "share_locked_title" => "Password required",
446 SHARE_NOT_FOUND = "share_not_found" => "Share not found",
447 SHARE_NOT_FOUND_MSG = "share_not_found_msg" => "This link is invalid or the share was removed.",
448 SHARE_PASSWORD_HINT = "share_password_hint" => "Optional. Visitors must enter it before the share opens, at least 8 characters.",
449 SHARE_PERM_RO = "share_perm_ro" => "Anyone with this link can read “{}”.",
450 SHARE_PERM_RW = "share_perm_rw" => "Anyone with this link can read and write “{}”.",
451 SHARE_PROTECTED = "share_protected" => "Password protected",
452 SHARE_TITLE = "share_title" => "Share {}",
453 SHARES = "shares" => "Shares",
454 SHARES_DELETE_ERR = "shares_delete_err" => "Could not delete shares",
455 SHARES_EMPTY_HINT = "shares_empty_hint" => "Right-click a file or folder and choose “Share” to create a link.",
456 SHARES_VIEW_HINT = "shares_view_hint" => "Links you created. Right-click a file or folder and choose “Share” to add one.",
457 SIGN_IN = "sign_in" => "Sign in",
458 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.",
459 SINGLE_CLICK_LABEL = "single_click_label" => "Use single click to open",
460 SIZE = "size" => "Size",
461 SOME_FILES_EXIST = "some_files_exist" => "Some files already exist",
462 SORT_ASC = "sort_asc" => "Ascending",
463 SORT_BY = "sort_by" => "Sort by",
464 SORT_DESC = "sort_desc" => "Descending",
465 THEME_AUTO = "theme_auto" => "Auto",
466 THEME_DARK = "theme_dark" => "Dark",
467 THEME_LIGHT = "theme_light" => "Light",
468 THEME_TITLE = "theme_title" => "Theme: {} (click to switch)",
469 TYPE = "type" => "Type",
470 UNLOCK = "unlock" => "Open share",
471 UNSAVED_CHANGES = "unsaved_changes" => "Unsaved changes",
472 UNTIL = "until" => "Until",
473 UPLOAD_COMPLETE = "upload_complete" => "Upload complete",
474 UPLOAD_FILES = "upload_files" => "Upload files",
475 UPLOAD_FOLDER = "upload_folder" => "Upload folder",
476 UPLOADING = "uploading" => "Uploading {} files…",
477 USER_CREATED = "user_created" => "User created",
478 USER_DELETE_ERR = "user_delete_err" => "Could not delete user",
479 USER_SAVE_ERR = "user_save_err" => "Could not save user: {}",
480 USER_UPDATED = "user_updated" => "User updated",
481 USERS = "users" => "Users",
482 YOUR_SHARES = "your_shares" => "Your shares",
483}
484
485/// German translations.
486const DE: &[(&str, &str)] = &[
487 ("active_can_signin", "Aktiv (kann sich anmelden)"),
488 ("add_folder", "Ordner hinzufügen…"),
489 ("add_here", "Diesen Ordner hinzufügen"),
490 ("administrator", "Administrator"),
491 ("allow_editing", "Bearbeitung erlauben (lesen/schreiben)"),
492 ("allow_rw_shares", "Schreibbare Freigaben erlauben"),
493 (
494 "allow_rw_shares_desc",
495 "Benutzern erlauben, schreibbare Freigabelinks zu erstellen. Standardmäßig aus.",
496 ),
497 ("and_more", "…und {} weitere"),
498 ("cancel", "Abbrechen"),
499 ("cant_delete_self", "Sie können sich nicht selbst löschen"),
500 ("close", "Schließen"),
501 ("close_esc", "Schließen (Esc)"),
502 ("copied", "Kopiert"),
503 ("copy", "Kopieren"),
504 ("copy_dav_link", "WebDAV-Link kopieren"),
505 ("copy_here", "Hierher kopieren"),
506 ("copy_link", "Link kopieren"),
507 ("copy_manual", "Link manuell kopieren"),
508 ("copy_n_to", "{} Elemente kopieren nach…"),
509 ("copy_to", "„{}“ kopieren nach…"),
510 ("create", "Erstellen"),
511 ("create_admin", "Administrator-Konto erstellen"),
512 ("create_link", "Link erstellen"),
513 ("create_user", "Benutzer erstellen"),
514 ("creating", "Wird erstellt…"),
515 ("custom", "Benutzerdefiniert…"),
516 ("delete", "Löschen"),
517 ("delete_all", "Alle löschen"),
518 (
519 "delete_all_msg",
520 "Alle {} Freigabelinks löschen? Jeder, der sie besitzt, verliert den Zugriff. Das kann nicht rückgängig gemacht werden.",
521 ),
522 ("delete_all_title", "Alle Freigaben löschen?"),
523 (
524 "delete_folder_msg",
525 "Ordner „{}“ mit allem Inhalt löschen?\nDas kann nicht rückgängig gemacht werden.",
526 ),
527 (
528 "delete_msg",
529 "„{}“ löschen?\nDas kann nicht rückgängig gemacht werden.",
530 ),
531 (
532 "delete_n_msg",
533 "{} Elemente löschen?\nDas kann nicht rückgängig gemacht werden.",
534 ),
535 (
536 "delete_partial",
537 "{} von {} Freigaben konnten nicht gelöscht werden",
538 ),
539 ("delete_share", "Freigabe löschen"),
540 ("delete_user", "Benutzer löschen"),
541 (
542 "delete_user_confirm",
543 "Benutzer „{}“ löschen? Das kann nicht rückgängig gemacht werden.",
544 ),
545 ("deleted", "Gelöscht"),
546 ("deleted_n_shares", "{} Freigabe(n) gelöscht"),
547 ("discard", "Verwerfen"),
548 (
549 "discard_question",
550 "Änderungen verwerfen und den Editor schließen?",
551 ),
552 ("dismiss", "Ausblenden"),
553 ("dismiss_error", "Fehler ausblenden"),
554 ("done", "Fertig"),
555 ("download", "Herunterladen"),
556 (
557 "download_formats_msg",
558 "Archivformat für den Ordner wählen.",
559 ),
560 ("download_started", "Download gestartet"),
561 ("download_title", "„{}“ herunterladen"),
562 ("edit", "Bearbeiten"),
563 ("edit_user_title", "„{}“ bearbeiten"),
564 ("empty_folder", "Dieser Ordner ist leer."),
565 (k::ERR_ADMIN_ONLY, "nur für Administratoren"),
566 (k::ERR_ALREADY_SET_UP, "Server ist bereits eingerichtet"),
567 (k::ERR_BAD_ACTION, "action=content erwartet"),
568 (k::ERR_BAD_BODY, "ungültiger Anfrageinhalt"),
569 (k::ERR_BAD_EXPIRES_AT, "das Ablaufdatum ist ungültig"),
570 (
571 k::ERR_BAD_FORMAT,
572 "das Format muss eines der folgenden sein: zip, tar, tar.gz, tar.zst",
573 ),
574 (
575 k::ERR_BAD_MULTIPART,
576 "multipart/form-data mit Boundary erwartet",
577 ),
578 (k::ERR_BAD_PART_NAME, "ungültiger Part-Name"),
579 (
580 k::ERR_BAD_POST,
581 "POST erwartet einen Multipart-Upload, eine JSON-Mutation oder ?action=mkdir/create-file",
582 ),
583 (k::ERR_BAD_UPLOAD, "ungültige Upload-Daten"),
584 (k::ERR_BAD_UPLOAD_PATH, "ungültiger Dateipfad im Upload"),
585 (k::ERR_DST_REQUIRED, "dst_root_id ist erforderlich"),
586 (k::ERR_FILE_NAME_REQUIRED, "ein Dateiname ist erforderlich"),
587 (k::ERR_FILES_EXIST, "einige Dateien existieren bereits"),
588 (
589 k::ERR_FOLDER_EXISTS,
590 "ein Ordner mit diesem Namen existiert bereits",
591 ),
592 (
593 k::ERR_FOLDER_NAME_REQUIRED,
594 "ein Ordnername ist erforderlich",
595 ),
596 (k::ERR_FS_CONFLICT, "existiert bereits"),
597 (k::ERR_FS_FORBIDDEN, "Zugriff verweigert"),
598 (k::ERR_FS_NOT_A_DIR, "kein Ordner"),
599 (k::ERR_FS_NOT_FOUND, "Ordner nicht gefunden"),
600 (
601 k::ERR_FS_ROOT_MISSING,
602 "der konfigurierte Ordner existiert nicht mehr",
603 ),
604 (k::ERR_INTERNAL, "interner Fehler"),
605 (k::ERR_INVALID_CREDENTIALS, "Name oder Passwort ungültig"),
606 (k::ERR_INVALID_LANGUAGE, "ungültiges Sprachkürzel"),
607 (
608 k::ERR_LAST_ADMIN,
609 "der letzte aktive Administrator kann nicht entfernt werden",
610 ),
611 (
612 k::ERR_LAST_ADMIN_DELETE,
613 "der letzte aktive Administrator kann nicht gelöscht werden",
614 ),
615 (k::ERR_NAME_LENGTH, "der Name muss 1–64 Zeichen haben"),
616 (k::ERR_NEW_NAME_REQUIRED, "new_name ist erforderlich"),
617 (k::ERR_NO_FILES_UPLOADED, "keine Dateien hochgeladen"),
618 (k::ERR_NO_SUCH_FOLDER, "dieser Ordner existiert nicht"),
619 (k::ERR_NOT_A_FILE, "keine Datei"),
620 (k::ERR_NOT_SIGNED_IN, "nicht angemeldet"),
621 (
622 k::ERR_OWN_ACCOUNT,
623 "Sie können Ihr eigenes Konto nicht deaktivieren",
624 ),
625 (
626 k::ERR_OWN_ADMIN,
627 "Sie können Ihre eigenen Administratorrechte nicht entfernen",
628 ),
629 (
630 k::ERR_OWN_DELETE,
631 "Sie können Ihr eigenes Konto nicht löschen",
632 ),
633 (k::ERR_PART_NO_NAME, "Part ohne Namen"),
634 (
635 k::ERR_PASSWORD_SHORT,
636 "das Passwort muss mindestens 8 Zeichen lang sein",
637 ),
638 (
639 k::ERR_PATH_REQUIRED,
640 "ein Pfad innerhalb des Ordners ist erforderlich",
641 ),
642 (k::ERR_READ_ONLY_FOLDER, "schreibgeschützter Ordner"),
643 (k::ERR_ROOT_FORBIDDEN, "kein Zugriff auf diesen Ordner"),
644 (
645 k::ERR_RW_RO_FOLDER,
646 "dieser Ordner ist für Sie schreibgeschützt und kann nicht schreibbar freigegeben werden",
647 ),
648 (
649 k::ERR_RW_SHARES_DISABLED,
650 "schreibbare Freigaben sind deaktiviert",
651 ),
652 (
653 k::ERR_SAVE_FAILED,
654 "die Datei konnte nicht gespeichert werden",
655 ),
656 (
657 k::ERR_SEARCH_BAD_SCOPE,
658 "Bereich muss name, content oder both sein",
659 ),
660 (
661 k::ERR_SEARCH_FORBIDDEN,
662 "Suche erfordert eine angemeldete Sitzung",
663 ),
664 (
665 k::ERR_SESSION_EXPIRED,
666 "Sitzung abgelaufen, bitte erneut anmelden",
667 ),
668 (k::ERR_SHARE_EXPIRED, "diese Freigabe ist abgelaufen"),
669 (k::ERR_SHARE_LOCKED, "Diese Freigabe ist passwortgeschützt."),
670 (
671 k::ERR_SHARE_NO_PASSWORD,
672 "Diese Freigabe hat kein Passwort.",
673 ),
674 (k::ERR_SHARE_NOT_FOUND, "Freigabe nicht gefunden"),
675 (
676 k::ERR_SHARE_TOKEN_FORBIDDEN,
677 "ein Freigabetoken kann hier nicht verwendet werden; bitte anmelden",
678 ),
679 (k::ERR_SHARE_WRONG_PASSWORD, "Falsches Passwort."),
680 (k::ERR_TOO_LARGE_PREVIEW, "Datei zu groß für eine Vorschau"),
681 (k::ERR_TOO_LARGE_SAVE, "Datei zu groß zum Speichern"),
682 (
683 k::ERR_USER_EXISTS,
684 "ein Benutzer mit diesem Namen existiert bereits",
685 ),
686 (k::ERR_USER_NOT_FOUND, "Benutzer nicht gefunden"),
687 (
688 "exclude_dup_err",
689 "Dieser Ordner ist bereits ausgeschlossen.",
690 ),
691 ("exclude_folder", "Ordner ausschließen…"),
692 ("exclude_here", "Diesen Ordner ausschließen"),
693 (
694 "exclude_root_err",
695 "Das gesamte Stammverzeichnis kann nicht ausgeschlossen werden; das würde die Suche abschalten.",
696 ),
697 ("expires", "Läuft ab"),
698 ("expires_at", "Läuft ab: {}"),
699 ("expiry_1d", "1 Tag"),
700 ("expiry_1h", "1 Stunde"),
701 ("expiry_1mo", "1 Monat"),
702 ("expiry_1w", "1 Woche"),
703 ("file", "Datei"),
704 ("file_changed", "Datei auf der Festplatte geändert"),
705 ("file_created", "Datei erstellt"),
706 ("file_info", "Datei-Info"),
707 ("file_name", "Dateiname"),
708 ("files", "Dateien"),
709 ("fmt_tar", "Tar"),
710 ("fmt_targz", "Tar + gzip"),
711 ("fmt_tarzst", "Tar + zstd"),
712 ("fmt_zip", "Zip"),
713 ("folder", "Ordner"),
714 ("folder_created", "Ordner erstellt"),
715 (
716 "folder_gone_msg",
717 "Dieser Ordner ist nicht mehr verfügbar. Wählen Sie links einen anderen aus.",
718 ),
719 ("folder_gone_title", "Ordner nicht verfügbar"),
720 ("folder_name", "Ordnername"),
721 ("folders", "Ordner"),
722 (
723 "folders_paths_hint",
724 "Pfade sind relativ zur Serverwurzel. „.“ ist der gesamte Wurzelordner.",
725 ),
726 ("grid_view", "Kachelansicht"),
727 ("info", "Info"),
728 ("keep_editing", "Weiter bearbeiten"),
729 ("lang_auto", "Automatisch"),
730 ("lang_de", "Deutsch"),
731 ("lang_en", "Englisch"),
732 ("lang_fr", "Französisch"),
733 ("language", "Sprache"),
734 (
735 "language_desc",
736 "Die Sprache der Benutzeroberfläche. „Automatisch“ verwendet die Sprache Ihres Browsers.",
737 ),
738 ("link_copied", "Link in die Zwischenablage kopiert"),
739 (
740 "list_truncated",
741 "Nur die ersten {} Einträge werden angezeigt.",
742 ),
743 ("list_view", "Listenansicht"),
744 ("load_latest", "Neueste Version laden"),
745 ("loading", "Wird geladen…"),
746 ("loading_share", "Freigabe wird geladen…"),
747 ("location", "Ort"),
748 ("log_out", "Abmelden"),
749 ("login_error", "Bitte Name und Passwort eingeben."),
750 ("login_subtitle", "Anmelden, um fortzufahren."),
751 ("manage", "Verwalten"),
752 ("menu", "Menü"),
753 ("meta_created", "erstellt: {}"),
754 ("meta_expires", "läuft ab: {}"),
755 ("mode_ro", "nur lesen"),
756 ("mode_rw", "lesen/schreiben"),
757 ("modified", "Geändert"),
758 (
759 "modified_after",
760 "Diese Datei wurde nach dem Öffnen geändert. „Neueste Version laden“ verwirft Ihre ungespeicherten Änderungen; „Überschreiben“ behält Ihre Version bei.",
761 ),
762 ("more_actions", "Weitere Aktionen"),
763 ("move", "Verschieben"),
764 ("move_here", "Hierher verschieben"),
765 ("move_n_to", "{} Elemente verschieben nach…"),
766 ("move_to", "„{}“ verschieben nach…"),
767 ("moved", "Verschoben"),
768 ("name", "Name"),
769 ("name_required", "Ein Name ist erforderlich."),
770 ("name_taken", "Name wird bereits verwendet"),
771 (
772 "need_folder",
773 "Dem Benutzer muss mindestens ein Ordner zugewiesen werden.",
774 ),
775 ("never", "Nie"),
776 ("never_expires", "Läuft nie ab"),
777 ("new_file", "Neue Datei"),
778 ("new_folder", "Neuer Ordner"),
779 ("new_name", "Neuer Name"),
780 (
781 "new_password_keep",
782 "Neues Passwort (leer lassen, um beizubehalten)",
783 ),
784 ("new_user", "Neuer Benutzer"),
785 (
786 "no_folders_msg",
787 "Ein Administrator hat Ihnen noch keine Ordner zugewiesen.",
788 ),
789 ("no_folders_title", "Keine Ordner verfügbar"),
790 ("no_preview", "Keine Vorschau verfügbar"),
791 ("no_preview_msg", "Die App kann „{}“ hier nicht anzeigen."),
792 ("no_shares", "Noch keine Freigaben"),
793 ("no_subfolders", "Keine Unterordner"),
794 ("no_users", "Keine Benutzer."),
795 ("open", "Öffnen"),
796 ("open_in_new_tab", "In neuem Tab öffnen"),
797 ("open_menu", "Menü öffnen"),
798 ("overwrite", "Überschreiben"),
799 (
800 "overwrite_question",
801 "Die folgenden Dateien existieren bereits. Überschreiben?",
802 ),
803 ("password", "Passwort"),
804 ("profile", "Profil"),
805 (
806 "profile_save_err",
807 "Profil konnte nicht gespeichert werden: {}",
808 ),
809 ("profile_saved", "Profil gespeichert"),
810 (
811 "pw_short",
812 "Das Passwort muss mindestens 8 Zeichen lang sein.",
813 ),
814 ("read_only_badge", "Schreibgeschützt"),
815 ("read_only_hint", "Schreibgeschützter Ordner"),
816 ("refresh", "Aktualisieren"),
817 ("refresh_folders", "Ordner aktualisieren"),
818 ("remove_folder", "Ordner entfernen"),
819 ("rename", "Umbenennen"),
820 ("rename_title", "„{}“ umbenennen"),
821 ("renamed", "Umbenannt"),
822 ("repeat_password", "Passwort wiederholen"),
823 ("retry", "Erneut versuchen"),
824 ("root_dup_err", "Dieser Ordner ist bereits in der Liste."),
825 (
826 "rw_disabled",
827 "Schreibbare Freigaben sind vom Server deaktiviert.",
828 ),
829 (
830 "rw_ro_folder",
831 "Dieser Ordner ist schreibgeschützt, daher ist die Freigabe auch schreibgeschützt.",
832 ),
833 ("save", "Speichern"),
834 ("save_profile", "Profil speichern"),
835 ("save_settings", "Einstellungen speichern"),
836 ("saved", "Gespeichert"),
837 ("saving", "Wird gespeichert…"),
838 ("search", "Suche"),
839 ("search_both", "Beides"),
840 ("search_collapse_all", "Alle einklappen"),
841 ("search_content", "Inhalt"),
842 ("search_excludes", "Von der Suche ausgeschlossen"),
843 ("search_excludes_empty", "Keine Ordner ausgeschlossen."),
844 ("search_expand_all", "Alle ausklappen"),
845 ("search_failed", "Die Suche ist fehlgeschlagen."),
846 ("search_goto", "Im Ordner anzeigen"),
847 ("search_here", "Hier suchen"),
848 ("search_in", "Suchen in…"),
849 ("search_in_folder", "In diesem Ordner suchen"),
850 ("search_in_n_files", "in {} Dateien"),
851 ("search_n_matches", "{} Treffer"),
852 ("search_n_results", "{} Treffer"),
853 ("search_name", "Name"),
854 ("search_no_results", "keine Treffer"),
855 (
856 "search_placeholder",
857 "Dateien und deren Inhalte durchsuchen",
858 ),
859 ("search_run", "Suchen"),
860 ("search_scanned", "{} Dateien durchsucht"),
861 ("search_show_more", "{} weitere anzeigen"),
862 ("search_shown_of", "{} von {} angezeigt"),
863 ("search_skipped", "{} übersprungen (über 10 MiB)"),
864 ("search_stop", "Stopp"),
865 ("search_stopped", "gestoppt · {} Treffer"),
866 ("search_took", "{} ms"),
867 ("searching", "Suche läuft…"),
868 (
869 "select_folder_msg",
870 "Wählen Sie links einen Ordner aus, um mit dem Durchsuchen zu beginnen.",
871 ),
872 ("select_folder_title", "Ordner auswählen"),
873 ("server", "Server"),
874 ("server_root", "Serverwurzel"),
875 ("server_unreachable", "Der Server ist nicht erreichbar."),
876 ("settings", "Einstellungen"),
877 (
878 "settings_save_err",
879 "Einstellungen konnten nicht gespeichert werden: {}",
880 ),
881 ("settings_saved", "Einstellungen gespeichert"),
882 (
883 "setup_intro",
884 "Erster Start: Erstellen Sie das Administrator-Konto. Dieser Benutzer kann andere Benutzer und die Servereinstellungen verwalten.",
885 ),
886 (
887 "setup_name_err",
888 "Bitte wählen Sie einen Namen (1–64 Zeichen).",
889 ),
890 ("setup_pw_mismatch", "Die Passwörter stimmen nicht überein."),
891 ("share", "Teilen"),
892 (
893 "share_create_err",
894 "Freigabe konnte nicht erstellt werden: {}",
895 ),
896 ("share_delete_err", "Freigabe konnte nicht gelöscht werden"),
897 ("share_deleted", "Freigabe gelöscht"),
898 (
899 "share_dialog_hint",
900 "Erstellt einen Link für diesen Eintrag. Mit schreibbaren Links können Empfänger Dateien hochladen und bearbeiten.",
901 ),
902 ("share_expired", "Freigabe abgelaufen"),
903 (
904 "share_expired_msg",
905 "Dieser Freigabelink ist nicht mehr aktiv.",
906 ),
907 ("share_link", "Freigabelink"),
908 ("share_locked_title", "Passwort erforderlich"),
909 ("share_not_found", "Freigabe nicht gefunden"),
910 (
911 "share_not_found_msg",
912 "Dieser Link ist ungültig oder die Freigabe wurde entfernt.",
913 ),
914 (
915 "share_password_hint",
916 "Optional. Besucher müssen es eingeben, bevor die Freigabe geöffnet wird, mindestens 8 Zeichen.",
917 ),
918 ("share_perm_ro", "Jeder mit diesem Link kann „{}“ lesen."),
919 (
920 "share_perm_rw",
921 "Jeder mit diesem Link kann „{}“ lesen und schreiben.",
922 ),
923 ("share_protected", "Passwortgeschützt"),
924 ("share_title", "{} teilen"),
925 ("shares", "Freigaben"),
926 (
927 "shares_delete_err",
928 "Freigaben konnten nicht gelöscht werden",
929 ),
930 (
931 "shares_empty_hint",
932 "Klicken Sie mit der rechten Maustaste auf eine Datei oder einen Ordner und wählen Sie „Teilen“, um einen Link zu erstellen.",
933 ),
934 (
935 "shares_view_hint",
936 "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.",
937 ),
938 ("sign_in", "Anmelden"),
939 (
940 "single_click_desc",
941 "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.",
942 ),
943 ("single_click_label", "Einzelklick öffnet Einträge"),
944 ("size", "Größe"),
945 ("some_files_exist", "Einige Dateien existieren bereits"),
946 ("sort_asc", "Aufsteigend"),
947 ("sort_by", "Sortieren nach"),
948 ("sort_desc", "Absteigend"),
949 ("theme_auto", "Auto"),
950 ("theme_dark", "Dunkel"),
951 ("theme_light", "Hell"),
952 ("theme_title", "Design: {} (klicken zum Wechseln)"),
953 ("type", "Typ"),
954 ("unlock", "Freigabe öffnen"),
955 ("unsaved_changes", "Ungespeicherte Änderungen"),
956 ("until", "Bis"),
957 ("upload_complete", "Hochladen abgeschlossen"),
958 ("upload_files", "Dateien hochladen"),
959 ("upload_folder", "Ordner hochladen"),
960 ("uploading", "{} Datei(en) werden hochgeladen…"),
961 ("user_created", "Benutzer erstellt"),
962 ("user_delete_err", "Benutzer konnte nicht gelöscht werden"),
963 (
964 "user_save_err",
965 "Benutzer konnte nicht gespeichert werden: {}",
966 ),
967 ("user_updated", "Benutzer aktualisiert"),
968 ("users", "Benutzer"),
969 ("your_shares", "Ihre Freigaben"),
970];
971
972/// French translations.
973const FR: &[(&str, &str)] = &[
974 ("active_can_signin", "Actif (peut se connecter)"),
975 ("add_folder", "Ajouter un dossier…"),
976 ("add_here", "Ajouter ce dossier"),
977 ("administrator", "Administrateur"),
978 (
979 "allow_editing",
980 "Autoriser la modification (lecture-écriture)",
981 ),
982 ("allow_rw_shares", "Autoriser les partages modifiables"),
983 (
984 "allow_rw_shares_desc",
985 "Autoriser les utilisateurs à créer des liens de partage en lecture-écriture. Désactivé par défaut.",
986 ),
987 ("and_more", "…et {} autres"),
988 ("cancel", "Annuler"),
989 (
990 "cant_delete_self",
991 "Vous ne pouvez pas supprimer votre propre compte",
992 ),
993 ("close", "Fermer"),
994 ("close_esc", "Fermer (Échap)"),
995 ("copied", "Copié"),
996 ("copy", "Copier"),
997 ("copy_dav_link", "Copier le lien WebDAV"),
998 ("copy_here", "Copier ici"),
999 ("copy_link", "Copier le lien"),
1000 ("copy_manual", "Copiez le lien manuellement"),
1001 ("copy_n_to", "Copier {} éléments vers…"),
1002 ("copy_to", "Copier « {} » vers…"),
1003 ("create", "Créer"),
1004 ("create_admin", "Créer le compte administrateur"),
1005 ("create_link", "Créer le lien"),
1006 ("create_user", "Créer l'utilisateur"),
1007 ("creating", "Création…"),
1008 ("custom", "Personnalisé…"),
1009 ("delete", "Supprimer"),
1010 ("delete_all", "Tout supprimer"),
1011 (
1012 "delete_all_msg",
1013 "Supprimer les {} liens de partage ? Toute personne les possédant perdra l'accès. Cette action est irréversible.",
1014 ),
1015 ("delete_all_title", "Supprimer tous les partages ?"),
1016 (
1017 "delete_folder_msg",
1018 "Supprimer le dossier « {} » et tout son contenu ?\nCette action est irréversible.",
1019 ),
1020 (
1021 "delete_msg",
1022 "Supprimer « {} » ?\nCette action est irréversible.",
1023 ),
1024 (
1025 "delete_n_msg",
1026 "Supprimer {} éléments ?\nCette action est irréversible.",
1027 ),
1028 (
1029 "delete_partial",
1030 "{} des {} liens n'ont pas pu être supprimés",
1031 ),
1032 ("delete_share", "Supprimer le partage"),
1033 ("delete_user", "Supprimer l'utilisateur"),
1034 (
1035 "delete_user_confirm",
1036 "Supprimer l'utilisateur « {} » ? Cette action est irréversible.",
1037 ),
1038 ("deleted", "Supprimé"),
1039 ("deleted_n_shares", "{} partage(s) supprimé(s)"),
1040 ("discard", "Abandonner"),
1041 (
1042 "discard_question",
1043 "Abandonner vos modifications et fermer l'éditeur ?",
1044 ),
1045 ("dismiss", "Ignorer"),
1046 ("dismiss_error", "Ignorer l'erreur"),
1047 ("done", "Terminé"),
1048 ("download", "Télécharger"),
1049 (
1050 "download_formats_msg",
1051 "Choisissez un format d'archivage pour le dossier.",
1052 ),
1053 ("download_started", "Téléchargement démarré"),
1054 ("download_title", "Télécharger « {} »"),
1055 ("edit", "Modifier"),
1056 ("edit_user_title", "Modifier « {} »"),
1057 ("empty_folder", "Ce dossier est vide."),
1058 (k::ERR_ADMIN_ONLY, "réservé aux administrateurs"),
1059 (k::ERR_ALREADY_SET_UP, "le serveur est déjà configuré"),
1060 (k::ERR_BAD_ACTION, "action=content attendu"),
1061 (k::ERR_BAD_BODY, "corps de requête invalide"),
1062 (k::ERR_BAD_EXPIRES_AT, "la date d'expiration est invalide"),
1063 (
1064 k::ERR_BAD_FORMAT,
1065 "le format doit être zip, tar, tar.gz ou tar.zst",
1066 ),
1067 (
1068 k::ERR_BAD_MULTIPART,
1069 "multipart/form-data avec boundary attendu",
1070 ),
1071 (k::ERR_BAD_PART_NAME, "nom de partie invalide"),
1072 (
1073 k::ERR_BAD_POST,
1074 "POST attend un téléversement multipart, une mutation JSON ou ?action=mkdir/create-file",
1075 ),
1076 (k::ERR_BAD_UPLOAD, "données de téléversement invalides"),
1077 (
1078 k::ERR_BAD_UPLOAD_PATH,
1079 "chemin de fichier invalide dans le téléversement",
1080 ),
1081 (k::ERR_DST_REQUIRED, "dst_root_id est requis"),
1082 (k::ERR_FILE_NAME_REQUIRED, "un nom de fichier est requis"),
1083 (k::ERR_FILES_EXIST, "certains fichiers existent déjà"),
1084 (k::ERR_FOLDER_EXISTS, "un dossier avec ce nom existe déjà"),
1085 (k::ERR_FOLDER_NAME_REQUIRED, "un nom de dossier est requis"),
1086 (k::ERR_FS_CONFLICT, "existe déjà"),
1087 (k::ERR_FS_FORBIDDEN, "accès refusé"),
1088 (k::ERR_FS_NOT_A_DIR, "ce n'est pas un dossier"),
1089 (k::ERR_FS_NOT_FOUND, "dossier introuvable"),
1090 (k::ERR_FS_ROOT_MISSING, "le dossier configuré n'existe plus"),
1091 (k::ERR_INTERNAL, "erreur interne"),
1092 (k::ERR_INVALID_CREDENTIALS, "nom ou mot de passe incorrect"),
1093 (k::ERR_INVALID_LANGUAGE, "balise de langue invalide"),
1094 (
1095 k::ERR_LAST_ADMIN,
1096 "impossible de supprimer le dernier administrateur actif",
1097 ),
1098 (
1099 k::ERR_LAST_ADMIN_DELETE,
1100 "impossible de supprimer le dernier administrateur actif",
1101 ),
1102 (k::ERR_NAME_LENGTH, "le nom doit contenir 1 à 64 caractères"),
1103 (k::ERR_NEW_NAME_REQUIRED, "new_name est requis"),
1104 (k::ERR_NO_FILES_UPLOADED, "aucun fichier n'a été téléversé"),
1105 (k::ERR_NO_SUCH_FOLDER, "ce dossier n'existe pas"),
1106 (k::ERR_NOT_A_FILE, "ce n'est pas un fichier"),
1107 (k::ERR_NOT_SIGNED_IN, "non connecté"),
1108 (
1109 k::ERR_OWN_ACCOUNT,
1110 "vous ne pouvez pas désactiver votre propre compte",
1111 ),
1112 (
1113 k::ERR_OWN_ADMIN,
1114 "vous ne pouvez pas retirer vos propres droits d'administrateur",
1115 ),
1116 (
1117 k::ERR_OWN_DELETE,
1118 "vous ne pouvez pas supprimer votre propre compte",
1119 ),
1120 (k::ERR_PART_NO_NAME, "partie sans nom"),
1121 (
1122 k::ERR_PASSWORD_SHORT,
1123 "le mot de passe doit contenir au moins 8 caractères",
1124 ),
1125 (k::ERR_PATH_REQUIRED, "un chemin dans le dossier est requis"),
1126 (k::ERR_READ_ONLY_FOLDER, "dossier en lecture seule"),
1127 (k::ERR_ROOT_FORBIDDEN, "dossier inaccessible"),
1128 (
1129 k::ERR_RW_RO_FOLDER,
1130 "ce dossier est en lecture seule pour vous, il ne peut donc pas être partagé en écriture",
1131 ),
1132 (
1133 k::ERR_RW_SHARES_DISABLED,
1134 "les partages modifiables sont désactivés",
1135 ),
1136 (k::ERR_SAVE_FAILED, "impossible d'enregistrer le fichier"),
1137 (
1138 k::ERR_SEARCH_BAD_SCOPE,
1139 "la portée doit être name, content ou both",
1140 ),
1141 (
1142 k::ERR_SEARCH_FORBIDDEN,
1143 "la recherche nécessite une session connectée",
1144 ),
1145 (
1146 k::ERR_SESSION_EXPIRED,
1147 "session expirée, veuillez vous connecter à nouveau",
1148 ),
1149 (k::ERR_SHARE_EXPIRED, "ce partage a expiré"),
1150 (
1151 k::ERR_SHARE_LOCKED,
1152 "Ce partage est protégé par un mot de passe.",
1153 ),
1154 (
1155 k::ERR_SHARE_NO_PASSWORD,
1156 "Ce partage n'a pas de mot de passe.",
1157 ),
1158 (k::ERR_SHARE_NOT_FOUND, "partage introuvable"),
1159 (
1160 k::ERR_SHARE_TOKEN_FORBIDDEN,
1161 "un jeton de partage ne peut pas être utilisé ici ; connectez-vous à la place",
1162 ),
1163 (k::ERR_SHARE_WRONG_PASSWORD, "Mot de passe incorrect."),
1164 (
1165 k::ERR_TOO_LARGE_PREVIEW,
1166 "fichier trop volumineux pour l'aperçu",
1167 ),
1168 (
1169 k::ERR_TOO_LARGE_SAVE,
1170 "fichier trop volumineux pour être enregistré",
1171 ),
1172 (k::ERR_USER_EXISTS, "un utilisateur avec ce nom existe déjà"),
1173 (k::ERR_USER_NOT_FOUND, "utilisateur introuvable"),
1174 ("exclude_dup_err", "Ce dossier est déjà exclu."),
1175 ("exclude_folder", "Exclure un dossier…"),
1176 ("exclude_here", "Exclure ce dossier"),
1177 (
1178 "exclude_root_err",
1179 "La racine entière ne peut pas être exclue ; cela désactiverait la recherche.",
1180 ),
1181 ("expires", "Expire"),
1182 ("expires_at", "Expire le {}"),
1183 ("expiry_1d", "1 jour"),
1184 ("expiry_1h", "1 heure"),
1185 ("expiry_1mo", "1 mois"),
1186 ("expiry_1w", "1 semaine"),
1187 ("file", "Fichier"),
1188 ("file_changed", "Fichier modifié sur disque"),
1189 ("file_created", "Fichier créé"),
1190 ("file_info", "Informations"),
1191 ("file_name", "Nom du fichier"),
1192 ("files", "Fichiers"),
1193 ("fmt_tar", "Tar"),
1194 ("fmt_targz", "Tar + gzip"),
1195 ("fmt_tarzst", "Tar + zstd"),
1196 ("fmt_zip", "Zip"),
1197 ("folder", "Dossier"),
1198 ("folder_created", "Dossier créé"),
1199 (
1200 "folder_gone_msg",
1201 "Ce dossier n'est plus accessible. Choisissez-en un autre dans le panneau latéral.",
1202 ),
1203 ("folder_gone_title", "Dossier indisponible"),
1204 ("folder_name", "Nom du dossier"),
1205 ("folders", "Dossiers"),
1206 (
1207 "folders_paths_hint",
1208 "Les chemins sont relatifs à la racine du serveur. « . » désigne toute la racine.",
1209 ),
1210 ("grid_view", "Vue en grille"),
1211 ("info", "Infos"),
1212 ("keep_editing", "Continuer à modifier"),
1213 ("lang_auto", "Automatique"),
1214 ("lang_de", "Allemand"),
1215 ("lang_en", "Anglais"),
1216 ("lang_fr", "Français"),
1217 ("language", "Langue"),
1218 (
1219 "language_desc",
1220 "La langue de l'interface. « Automatique » utilise la langue de votre navigateur.",
1221 ),
1222 ("link_copied", "Lien copié dans le presse-papiers"),
1223 (
1224 "list_truncated",
1225 "Seules les {} premières entrées sont affichées.",
1226 ),
1227 ("list_view", "Vue en liste"),
1228 ("load_latest", "Charger la version la plus récente"),
1229 ("loading", "Chargement…"),
1230 ("loading_share", "Chargement du partage…"),
1231 ("location", "Emplacement"),
1232 ("log_out", "Se déconnecter"),
1233 (
1234 "login_error",
1235 "Veuillez saisir votre nom et votre mot de passe.",
1236 ),
1237 ("login_subtitle", "Connectez-vous pour continuer."),
1238 ("manage", "Gérer"),
1239 ("menu", "Menu"),
1240 ("meta_created", "créé le {}"),
1241 ("meta_expires", "expire le {}"),
1242 ("mode_ro", "lecture seule"),
1243 ("mode_rw", "lecture-écriture"),
1244 ("modified", "Modifié"),
1245 (
1246 "modified_after",
1247 "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.",
1248 ),
1249 ("more_actions", "Plus d'actions"),
1250 ("move", "Déplacer"),
1251 ("move_here", "Déplacer ici"),
1252 ("move_n_to", "Déplacer {} éléments vers…"),
1253 ("move_to", "Déplacer « {} » vers…"),
1254 ("moved", "Déplacé"),
1255 ("name", "Nom"),
1256 ("name_required", "Un nom est requis."),
1257 ("name_taken", "Ce nom est déjà utilisé"),
1258 ("need_folder", "Donnez au moins un dossier à l'utilisateur."),
1259 ("never", "Jamais"),
1260 ("never_expires", "N'expire jamais"),
1261 ("new_file", "Nouveau fichier"),
1262 ("new_folder", "Nouveau dossier"),
1263 ("new_name", "Nouveau nom"),
1264 (
1265 "new_password_keep",
1266 "Nouveau mot de passe (laisser vide pour conserver)",
1267 ),
1268 ("new_user", "Nouvel utilisateur"),
1269 (
1270 "no_folders_msg",
1271 "Aucun administrateur ne vous a encore attribué de dossier.",
1272 ),
1273 ("no_folders_title", "Aucun dossier disponible"),
1274 ("no_preview", "Aperçu non disponible"),
1275 (
1276 "no_preview_msg",
1277 "L'application ne peut pas afficher « {} » ici.",
1278 ),
1279 ("no_shares", "Aucun partage pour l'instant"),
1280 ("no_subfolders", "Aucun sous-dossier"),
1281 ("no_users", "Aucun utilisateur."),
1282 ("open", "Ouvrir"),
1283 ("open_in_new_tab", "Ouvrir dans un nouvel onglet"),
1284 ("open_menu", "Ouvrir le menu"),
1285 ("overwrite", "Écraser"),
1286 (
1287 "overwrite_question",
1288 "Les fichiers suivants existent déjà. Les écraser ?",
1289 ),
1290 ("password", "Mot de passe"),
1291 ("profile", "Profil"),
1292 (
1293 "profile_save_err",
1294 "Impossible d'enregistrer le profil : {}",
1295 ),
1296 ("profile_saved", "Profil enregistré"),
1297 (
1298 "pw_short",
1299 "Le mot de passe doit contenir au moins 8 caractères.",
1300 ),
1301 ("read_only_badge", "Lecture seule"),
1302 ("read_only_hint", "Dossier en lecture seule"),
1303 ("refresh", "Actualiser"),
1304 ("refresh_folders", "Actualiser vos dossiers"),
1305 ("remove_folder", "Retirer le dossier"),
1306 ("rename", "Renommer"),
1307 ("rename_title", "Renommer « {} »"),
1308 ("renamed", "Renommé"),
1309 ("repeat_password", "Répéter le mot de passe"),
1310 ("retry", "Réessayer"),
1311 ("root_dup_err", "Ce dossier est déjà dans la liste."),
1312 (
1313 "rw_disabled",
1314 "Les partages modifiables sont désactivés par le serveur.",
1315 ),
1316 (
1317 "rw_ro_folder",
1318 "Ce dossier est en lecture seule, le partage l'est donc aussi.",
1319 ),
1320 ("save", "Enregistrer"),
1321 ("save_profile", "Enregistrer le profil"),
1322 ("save_settings", "Enregistrer les paramètres"),
1323 ("saved", "Enregistré"),
1324 ("saving", "Enregistrement…"),
1325 ("search", "Rechercher"),
1326 ("search_both", "Les deux"),
1327 ("search_collapse_all", "Tout replier"),
1328 ("search_content", "Contenu"),
1329 ("search_excludes", "Exclu de la recherche"),
1330 ("search_excludes_empty", "Aucun dossier exclu."),
1331 ("search_expand_all", "Tout déplier"),
1332 ("search_failed", "La recherche a échoué."),
1333 ("search_goto", "Afficher dans le dossier"),
1334 ("search_here", "Rechercher ici"),
1335 ("search_in", "Rechercher dans…"),
1336 ("search_in_folder", "Rechercher dans ce dossier"),
1337 ("search_in_n_files", "dans {} fichiers"),
1338 ("search_n_matches", "{} correspondances"),
1339 ("search_n_results", "{} résultats"),
1340 ("search_name", "Nom"),
1341 ("search_no_results", "aucun résultat"),
1342 (
1343 "search_placeholder",
1344 "Rechercher dans les fichiers et leur contenu",
1345 ),
1346 ("search_run", "Rechercher"),
1347 ("search_scanned", "{} fichiers parcourus"),
1348 ("search_show_more", "Afficher {} de plus"),
1349 ("search_shown_of", "{} sur {} affichés"),
1350 ("search_skipped", "{} ignorés (plus de 10 Mio)"),
1351 ("search_stop", "Arrêter"),
1352 ("search_stopped", "arrêté · {} résultats"),
1353 ("search_took", "{} ms"),
1354 ("searching", "recherche…"),
1355 (
1356 "select_folder_msg",
1357 "Choisissez un dossier dans le panneau latéral pour commencer à naviguer.",
1358 ),
1359 ("select_folder_title", "Choisir un dossier"),
1360 ("server", "Serveur"),
1361 ("server_root", "Racine du serveur"),
1362 ("server_unreachable", "Le serveur est injoignable."),
1363 ("settings", "Paramètres"),
1364 (
1365 "settings_save_err",
1366 "Impossible d'enregistrer les paramètres : {}",
1367 ),
1368 ("settings_saved", "Paramètres enregistrés"),
1369 (
1370 "setup_intro",
1371 "Premier démarrage : créez le compte administrateur. Cet utilisateur peut gérer les autres utilisateurs et les paramètres du serveur.",
1372 ),
1373 (
1374 "setup_name_err",
1375 "Veuillez choisir un nom (1 à 64 caractères).",
1376 ),
1377 (
1378 "setup_pw_mismatch",
1379 "Les mots de passe ne correspondent pas.",
1380 ),
1381 ("share", "Partager"),
1382 ("share_create_err", "Impossible de créer le partage : {}"),
1383 ("share_delete_err", "Impossible de supprimer le partage"),
1384 ("share_deleted", "Partage supprimé"),
1385 (
1386 "share_dialog_hint",
1387 "Crée un lien pour cet élément. Les liens modifiables permettent aux destinataires de téléverser et de modifier.",
1388 ),
1389 ("share_expired", "Partage expiré"),
1390 ("share_expired_msg", "Ce lien de partage n'est plus actif."),
1391 ("share_link", "Lien de partage"),
1392 ("share_locked_title", "Mot de passe requis"),
1393 ("share_not_found", "Partage introuvable"),
1394 (
1395 "share_not_found_msg",
1396 "Ce lien est invalide ou le partage a été supprimé.",
1397 ),
1398 (
1399 "share_password_hint",
1400 "Facultatif. Les visiteurs doivent le saisir avant d'ouvrir le partage, au moins 8 caractères.",
1401 ),
1402 (
1403 "share_perm_ro",
1404 "Toute personne possédant ce lien peut lire « {} ».",
1405 ),
1406 (
1407 "share_perm_rw",
1408 "Toute personne possédant ce lien peut lire et modifier « {} ».",
1409 ),
1410 ("share_protected", "Protégé par mot de passe"),
1411 ("share_title", "Partager {}"),
1412 ("shares", "Partages"),
1413 ("shares_delete_err", "Impossible de supprimer les partages"),
1414 (
1415 "shares_empty_hint",
1416 "Faites un clic droit sur un fichier ou un dossier, puis choisissez « Partager » pour créer un lien.",
1417 ),
1418 (
1419 "shares_view_hint",
1420 "Liens que vous avez créés. Faites un clic droit sur un fichier ou un dossier, puis choisissez « Partager » pour en ajouter un.",
1421 ),
1422 ("sign_in", "Se connecter"),
1423 (
1424 "single_click_desc",
1425 "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.",
1426 ),
1427 ("single_click_label", "Ouvrir au clic simple"),
1428 ("size", "Taille"),
1429 ("some_files_exist", "Certains fichiers existent déjà"),
1430 ("sort_asc", "Croissant"),
1431 ("sort_by", "Trier par"),
1432 ("sort_desc", "Décroissant"),
1433 ("theme_auto", "Auto"),
1434 ("theme_dark", "Sombre"),
1435 ("theme_light", "Clair"),
1436 ("theme_title", "Thème : {} (cliquer pour changer)"),
1437 ("type", "Type"),
1438 ("unlock", "Ouvrir le partage"),
1439 ("unsaved_changes", "Modifications non enregistrées"),
1440 ("until", "Jusqu'au"),
1441 ("upload_complete", "Téléversement terminé"),
1442 ("upload_files", "Téléverser des fichiers"),
1443 ("upload_folder", "Téléverser un dossier"),
1444 ("uploading", "Téléversement de {} fichier(s)…"),
1445 ("user_created", "Utilisateur créé"),
1446 ("user_delete_err", "Impossible de supprimer l'utilisateur"),
1447 (
1448 "user_save_err",
1449 "Impossible d'enregistrer l'utilisateur : {}",
1450 ),
1451 ("user_updated", "Utilisateur mis à jour"),
1452 ("users", "Utilisateurs"),
1453 ("your_shares", "Vos partages"),
1454];
1455
1456#[cfg(test)]
1457mod tests {
1458 use super::*;
1459
1460 fn keys<'a>(table: &'a [(&'a str, &'a str)]) -> Vec<&'a str> {
1461 table.iter().map(|(k, _)| *k).collect()
1462 }
1463
1464 #[test]
1465 fn tables_are_sorted() {
1466 // `find` binary-searches, so an out-of-order entry is unreachable.
1467 for (name, table) in [("EN", EN), ("DE", DE), ("FR", FR)] {
1468 let ks = keys(table);
1469 let mut sorted = ks.clone();
1470 sorted.sort_unstable();
1471 assert_eq!(ks, sorted, "{name} table is not sorted by key");
1472 }
1473 }
1474
1475 #[test]
1476 fn no_duplicate_keys() {
1477 for (name, table) in [("EN", EN), ("DE", DE), ("FR", FR)] {
1478 let mut ks = keys(table);
1479 ks.sort_unstable();
1480 let dups = ks
1481 .windows(2)
1482 .filter(|w| w[0] == w[1])
1483 .map(|w| w[0])
1484 .collect::<Vec<_>>();
1485 assert!(dups.is_empty(), "{name} duplicates: {dups:?}");
1486 }
1487 }
1488
1489 #[test]
1490 fn translations_cover_all_keys() {
1491 // Full parity: DE and FR must translate every EN key (both
1492 // directions), so no string can silently fall back to English.
1493 let en = keys(EN);
1494 for (name, table) in [("DE", DE), ("FR", FR)] {
1495 let other = keys(table);
1496 assert_eq!(
1497 en.len(),
1498 other.len(),
1499 "{name} has {} keys, EN has {}",
1500 other.len(),
1501 en.len()
1502 );
1503 for k in &other {
1504 assert!(en.contains(k), "{name} key not in EN: {k}");
1505 }
1506 }
1507 }
1508}
1509