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