forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
46.0 KiB
1//! Per-viewer UI strings, following im's `i18n.rs` strategy: one variant per UI phrase, an
2//! exhaustive `t` match so a missing translation fails to compile rather than falling through
3//! at render time, and small `fmt`-style helpers for sentences that carry arguments.
4//!
5//! A page gets its language with `lang(cx, viewer)` and passes it to the strings it draws:
6//! `t(lang, Key::Explore)`. The shell stamps `<html lang>` from the same value.
7//!
8//! Wave 1 covers the shell, the auth pages, first-run setup and the settings pages. Other
9//! pages still hold English literals until their own wave. Pages are being moved to per-group
10//! key modules in `i18n/<group>.rs`, each with its own `K` enum and `t` match, so a group can
11//! be translated without touching the others.
12
13use http::HeaderMap;
14use http::header::ACCEPT_LANGUAGE;
15use topcoat::context::Cx;
16use topcoat::router::request::headers;
17
18use forge_core::User;
19
20pub mod admin;
21pub mod code;
22pub mod community;
23pub mod integrations;
24pub mod issues;
25pub mod misc;
26pub mod orgs;
27pub mod pulls;
28pub mod pulls_review;
29pub mod releases_ci;
30pub mod repo_admin;
31
32/// A UI language. English is the default; Turkish is the only other one for now.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Lang {
35 En,
36 Tr,
37}
38
39impl Lang {
40 /// Parses a stored language code. `auto` and anything unknown fall back to English.
41 pub fn from_code(code: &str) -> Lang {
42 match code {
43 "tr" => Lang::Tr,
44 _ => Lang::En,
45 }
46 }
47
48 /// The primary language subtag of a language tag (`tr-TR`, `en-US`), or `None` when this
49 /// build has no translation for it.
50 fn from_tag(tag: &str) -> Option<Lang> {
51 let primary = tag.split('-').next()?.trim();
52 match primary.to_ascii_lowercase().as_str() {
53 "tr" => Some(Lang::Tr),
54 "en" => Some(Lang::En),
55 _ => None,
56 }
57 }
58
59 /// The `<html lang>` value.
60 pub fn code(self) -> &'static str {
61 match self {
62 Lang::En => "en",
63 Lang::Tr => "tr",
64 }
65 }
66}
67
68/// The language for a request. The viewer's stored preference wins when it names a language,
69/// then the browser's `Accept-Language`, then English. `user_language` is the stored column, and
70/// `auto` means "no preference".
71pub fn resolve(user_language: Option<&str>, request_headers: &HeaderMap) -> Lang {
72 match user_language {
73 Some(code) if code != "auto" => Lang::from_code(code),
74 _ => accept_language(request_headers).unwrap_or(Lang::En),
75 }
76}
77
78/// The language for the current request, from the viewer (if any) and the request headers.
79pub fn lang(cx: &Cx, viewer: Option<&User>) -> Lang {
80 resolve(viewer.map(|u| u.language.as_str()), headers(cx))
81}
82
83/// The best-quality `Accept-Language` entry this build can serve. Entries with `q=0` or a bad
84/// quality are skipped, and so are tags it has no translation for.
85fn accept_language(request_headers: &HeaderMap) -> Option<Lang> {
86 let mut best: Option<(f32, Lang)> = None;
87 for value in request_headers.get_all(ACCEPT_LANGUAGE) {
88 let Ok(text) = value.to_str() else {
89 continue;
90 };
91 for item in text.split(',') {
92 let mut parts = item.split(';');
93 let tag = parts.next().unwrap_or_default().trim();
94 let quality = parts
95 .find_map(|p| p.trim().strip_prefix("q="))
96 .map_or(Some(1.0), |q| q.parse::<f32>().ok());
97 let Some(quality) = quality.filter(|q| *q > 0.0) else {
98 continue;
99 };
100 let Some(lang) = Lang::from_tag(tag) else {
101 continue;
102 };
103 if best.is_none_or(|(q, _)| quality > q) {
104 best = Some((quality, lang));
105 }
106 }
107 }
108 best.map(|(_, lang)| lang)
109}
110
111/// One variant per UI phrase. A typo'd key fails to compile rather than falling through to
112/// nothing at render time.
113#[allow(dead_code)]
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum Key {
116 // Navigation and the user menu.
117 Explore,
118 Notifications,
119 NewRepository,
120 Profile,
121 SshKeys,
122 GpgKeys,
123 AccessTokens,
124 Sessions,
125 Security,
126 AdminUsers,
127 AdminAuditLog,
128 AdminRunners,
129 LogOut,
130 LogIn,
131 SiteMenu,
132
133 // Shared form words.
134 Username,
135 Password,
136 Email,
137 EmailOptional,
138 Code,
139 Verify,
140 Save,
141 Delete,
142 Revoke,
143 Never,
144 Name,
145 Expires,
146 Upload,
147 Remove,
148 Create,
149
150 // Log in and two-factor.
151 TwoFactorCheck,
152 TwoFactorCheckSub,
153 ErrInvalidLogin,
154 ErrThrottled,
155 ErrInvalidCode,
156
157 // First-run setup.
158 Setup,
159 WelcomeToForge,
160 SetupSub,
161 AtLeast12Chars,
162 ConfirmPassword,
163 CreateAdmin,
164
165 // Shared password and two-factor errors.
166 ErrPasswordsDiffer,
167 ErrWrongPassword,
168 ErrWrongConfirmCode,
169 ErrWrongCode,
170
171 // Profile.
172 OkUsernameUpdated,
173 OkAvatarUpdated,
174 OkAvatarRemoved,
175 OkProfileSaved,
176 CurrentUsername,
177 UsernameHint,
178 Avatar,
179 AvatarAlt,
180 Image,
181 AvatarHint,
182 RemoveAvatar,
183 AboutAndTimezone,
184 About,
185 AboutHint,
186 Timezone,
187 ErrChooseImage,
188
189 // Account security.
190 OkPasswordChanged,
191 OkEmailSaved,
192 OkTwoFactorOn,
193 OkTwoFactorOff,
194 OkRecoveryCodesNew,
195 SecurityLog,
196 SecurityLogRest,
197 RecoveryCodes,
198 RecoveryCodesSave,
199 CurrentPassword,
200 NewPassword,
201 NewPasswordHint,
202 ConfirmNewPassword,
203 ChangePassword,
204 SaveEmail,
205 TwoFactorAuth,
206 RegenerateCodes,
207 RegenerateCodesHint,
208 TurnOffTwoFactor,
209 TurnOffTwoFactorButton,
210 TwoFactorPendingIntro,
211 SecretPrefix,
212 LinkPrefix,
213 CodeFromApp,
214 ConfirmAndTurnOn,
215 TwoFactorOffIntro,
216 Enable2fa,
217 DeleteAccount,
218 DeleteAccountIntro,
219 TypeUsernameToConfirm,
220 TransferOrDeleteRepos,
221 ErrConfirmUsername,
222
223 // SSH and GPG keys.
224 NoSshKeys,
225 NoGpgKeys,
226 AddSshKeyTitle,
227 AddGpgKeyTitle,
228 TitleOptional,
229 PublicKeyLabel,
230 PublicKeyHint,
231 AddKeyButton,
232 ArmoredPublicKeyLabel,
233 ArmoredHint,
234 GpgUnavailable,
235
236 // Access tokens.
237 YourNewToken,
238 CopyNow,
239 UseAsPassword,
240 NoAccessTokens,
241 NewToken,
242 ExpiresIn30,
243 ExpiresIn90,
244 ExpiresIn365,
245 ExpiresNever,
246 ScopeNote,
247 CreateToken,
248
249 // Web sessions.
250 UnknownBrowser,
251 ThisSession,
252 RevokeAllOthers,
253
254 // Metadata lines (see the helpers below for the words in between).
255 Added,
256 LastUsed,
257 Created,
258 SignedIn,
259 LastSeen,
260 ExpiresLabel,
261
262 // Parse problems for SSH keys (`settings_keys::KeyProblem`).
263 ErrKeyEmpty,
264 ErrKeyTooLong,
265 ErrKeyMissingBody,
266 ErrKeyRsa,
267 ErrKeyUnsupportedType,
268 ErrKeyBadBase64,
269 ErrKeyTruncated,
270 ErrKeyMismatch,
271 // Account mail pages: reset, register, verify, invites.
272 ForgotTitle,
273 ForgotSub,
274 ForgotSubmit,
275 ForgotSent,
276 ResetTitle,
277 ResetSub,
278 ResetSubmit,
279 ResetInvalid,
280 ResetDone,
281 RegisterTitle,
282 RegisterSub,
283 RegisterInviteSub,
284 RegisterNeedsInvite,
285 RegisterInvalidInvite,
286 RegisterSubmit,
287 VerifyTitle,
288 VerifyConfirmSub,
289 VerifyConfirm,
290 VerifyDone,
291 VerifyInvalid,
292 VerifyVerified,
293 VerifyUnverified,
294 VerifyResend,
295 OkVerifySent,
296 InvitesTitle,
297 InvitesSub,
298 InvitesSubmit,
299 InvitesSent,
300 InvitesOff,
301 MailVerbOpened,
302 MailVerbCommented,
303 MailVerbClosed,
304 MailVerbReopened,
305 MailVerbMerged,
306 MailVerbReviewed,
307 MailVerbAssigned,
308 MailVerbPushed,
309 MailVerbMentioned,
310 MailSomeone,
311 MailReasonMention,
312 MailReasonAssign,
313 MailReasonReviewRequested,
314 MailReasonAuthor,
315 MailReasonComment,
316 MailReasonSubscribed,
317 MailReasonWatching,
318 MenuNotificationEmails,
319 EmailNotifyLabel,
320 EmailNotifyHelp,
321 EmailNotifyUnverified,
322 EmailNotifyServerOff,
323 EmailNotifySaved,
324}
325
326/// Every key, for the exhaustiveness test. Keep it in step with the enum.
327#[cfg(test)]
328pub const ALL: &[Key] = &[
329 Key::Explore,
330 Key::Notifications,
331 Key::NewRepository,
332 Key::Profile,
333 Key::SshKeys,
334 Key::GpgKeys,
335 Key::AccessTokens,
336 Key::Sessions,
337 Key::Security,
338 Key::AdminUsers,
339 Key::AdminAuditLog,
340 Key::AdminRunners,
341 Key::LogOut,
342 Key::LogIn,
343 Key::SiteMenu,
344 Key::Username,
345 Key::Password,
346 Key::Email,
347 Key::EmailOptional,
348 Key::Code,
349 Key::Verify,
350 Key::Save,
351 Key::Delete,
352 Key::Revoke,
353 Key::Never,
354 Key::Name,
355 Key::Expires,
356 Key::Upload,
357 Key::Remove,
358 Key::Create,
359 Key::TwoFactorCheck,
360 Key::TwoFactorCheckSub,
361 Key::ErrInvalidLogin,
362 Key::ErrThrottled,
363 Key::ErrInvalidCode,
364 Key::Setup,
365 Key::WelcomeToForge,
366 Key::SetupSub,
367 Key::AtLeast12Chars,
368 Key::ConfirmPassword,
369 Key::CreateAdmin,
370 Key::ErrPasswordsDiffer,
371 Key::ErrWrongPassword,
372 Key::ErrWrongConfirmCode,
373 Key::ErrWrongCode,
374 Key::OkUsernameUpdated,
375 Key::OkAvatarUpdated,
376 Key::OkAvatarRemoved,
377 Key::OkProfileSaved,
378 Key::CurrentUsername,
379 Key::UsernameHint,
380 Key::Avatar,
381 Key::AvatarAlt,
382 Key::Image,
383 Key::AvatarHint,
384 Key::RemoveAvatar,
385 Key::AboutAndTimezone,
386 Key::About,
387 Key::AboutHint,
388 Key::Timezone,
389 Key::ErrChooseImage,
390 Key::OkPasswordChanged,
391 Key::OkEmailSaved,
392 Key::OkTwoFactorOn,
393 Key::OkTwoFactorOff,
394 Key::OkRecoveryCodesNew,
395 Key::SecurityLog,
396 Key::SecurityLogRest,
397 Key::RecoveryCodes,
398 Key::RecoveryCodesSave,
399 Key::CurrentPassword,
400 Key::NewPassword,
401 Key::NewPasswordHint,
402 Key::ConfirmNewPassword,
403 Key::ChangePassword,
404 Key::SaveEmail,
405 Key::TwoFactorAuth,
406 Key::RegenerateCodes,
407 Key::RegenerateCodesHint,
408 Key::TurnOffTwoFactor,
409 Key::TurnOffTwoFactorButton,
410 Key::TwoFactorPendingIntro,
411 Key::SecretPrefix,
412 Key::LinkPrefix,
413 Key::CodeFromApp,
414 Key::ConfirmAndTurnOn,
415 Key::TwoFactorOffIntro,
416 Key::Enable2fa,
417 Key::DeleteAccount,
418 Key::DeleteAccountIntro,
419 Key::TypeUsernameToConfirm,
420 Key::TransferOrDeleteRepos,
421 Key::ErrConfirmUsername,
422 Key::NoSshKeys,
423 Key::NoGpgKeys,
424 Key::AddSshKeyTitle,
425 Key::AddGpgKeyTitle,
426 Key::TitleOptional,
427 Key::PublicKeyLabel,
428 Key::PublicKeyHint,
429 Key::AddKeyButton,
430 Key::ArmoredPublicKeyLabel,
431 Key::ArmoredHint,
432 Key::GpgUnavailable,
433 Key::YourNewToken,
434 Key::CopyNow,
435 Key::UseAsPassword,
436 Key::NoAccessTokens,
437 Key::NewToken,
438 Key::ExpiresIn30,
439 Key::ExpiresIn90,
440 Key::ExpiresIn365,
441 Key::ExpiresNever,
442 Key::ScopeNote,
443 Key::CreateToken,
444 Key::UnknownBrowser,
445 Key::ThisSession,
446 Key::RevokeAllOthers,
447 Key::Added,
448 Key::LastUsed,
449 Key::Created,
450 Key::SignedIn,
451 Key::LastSeen,
452 Key::ExpiresLabel,
453 Key::ErrKeyEmpty,
454 Key::ErrKeyTooLong,
455 Key::ErrKeyMissingBody,
456 Key::ErrKeyRsa,
457 Key::ErrKeyUnsupportedType,
458 Key::ErrKeyBadBase64,
459 Key::ErrKeyTruncated,
460 Key::ErrKeyMismatch,
461 Key::ForgotTitle,
462 Key::ForgotSub,
463 Key::ForgotSubmit,
464 Key::ForgotSent,
465 Key::ResetTitle,
466 Key::ResetSub,
467 Key::ResetSubmit,
468 Key::ResetInvalid,
469 Key::ResetDone,
470 Key::RegisterTitle,
471 Key::RegisterSub,
472 Key::RegisterInviteSub,
473 Key::RegisterNeedsInvite,
474 Key::RegisterInvalidInvite,
475 Key::RegisterSubmit,
476 Key::VerifyTitle,
477 Key::VerifyConfirmSub,
478 Key::VerifyConfirm,
479 Key::VerifyDone,
480 Key::VerifyInvalid,
481 Key::VerifyVerified,
482 Key::VerifyUnverified,
483 Key::VerifyResend,
484 Key::OkVerifySent,
485 Key::InvitesTitle,
486 Key::InvitesSub,
487 Key::InvitesSubmit,
488 Key::InvitesSent,
489 Key::InvitesOff,
490 Key::MailVerbOpened,
491 Key::MailVerbCommented,
492 Key::MailVerbClosed,
493 Key::MailVerbReopened,
494 Key::MailVerbMerged,
495 Key::MailVerbReviewed,
496 Key::MailVerbAssigned,
497 Key::MailVerbPushed,
498 Key::MailVerbMentioned,
499 Key::MailSomeone,
500 Key::MailReasonMention,
501 Key::MailReasonAssign,
502 Key::MailReasonReviewRequested,
503 Key::MailReasonAuthor,
504 Key::MailReasonComment,
505 Key::MailReasonSubscribed,
506 Key::MailReasonWatching,
507 Key::MenuNotificationEmails,
508 Key::EmailNotifyLabel,
509 Key::EmailNotifyHelp,
510 Key::EmailNotifyUnverified,
511 Key::EmailNotifyServerOff,
512 Key::EmailNotifySaved,
513];
514
515/// The phrase a key names, in a user's language.
516pub fn t(lang: Lang, key: Key) -> &'static str {
517 use Key::*;
518 use Lang::*;
519 match (key, lang) {
520 (Explore, En) => "Explore",
521 (Explore, Tr) => "Keşfet",
522 (Notifications, En) => "Notifications",
523 (Notifications, Tr) => "Bildirimler",
524 (NewRepository, En) => "New repository",
525 (NewRepository, Tr) => "Yeni depo",
526 (Profile, En) => "Profile",
527 (Profile, Tr) => "Profil",
528 (SshKeys, En) => "SSH keys",
529 (SshKeys, Tr) => "SSH anahtarları",
530 (GpgKeys, En) => "GPG keys",
531 (GpgKeys, Tr) => "GPG anahtarları",
532 (AccessTokens, En) => "Access tokens",
533 (AccessTokens, Tr) => "Erişim belirteçleri",
534 (Sessions, En) => "Sessions",
535 (Sessions, Tr) => "Oturumlar",
536 (Security, En) => "Security",
537 (Security, Tr) => "Güvenlik",
538 (AdminUsers, En) => "Admin: users",
539 (AdminUsers, Tr) => "Yönetici: kullanıcılar",
540 (AdminAuditLog, En) => "Admin: audit log",
541 (AdminAuditLog, Tr) => "Yönetici: denetim günlüğü",
542 (AdminRunners, En) => "Admin: runners",
543 (AdminRunners, Tr) => "Yönetici: çalıştırıcılar",
544 (LogOut, En) => "Log out",
545 (LogOut, Tr) => "Çıkış yap",
546 (SiteMenu, En) => "Menu",
547 (SiteMenu, Tr) => "Menü",
548 (LogIn, En) => "Log in",
549 (LogIn, Tr) => "Giriş yap",
550
551 (Username, En) => "Username",
552 (Username, Tr) => "Kullanıcı adı",
553 (Password, En) => "Password",
554 (Password, Tr) => "Parola",
555 (Email, En) => "Email",
556 (Email, Tr) => "E-posta",
557 (EmailOptional, En) => "Email (optional)",
558 (EmailOptional, Tr) => "E-posta (isteğe bağlı)",
559 (Code, En) => "Code",
560 (Code, Tr) => "Kod",
561 (Verify, En) => "Verify",
562 (Verify, Tr) => "Doğrula",
563 (Save, En) => "Save",
564 (Save, Tr) => "Kaydet",
565 (Delete, En) => "Delete",
566 (Delete, Tr) => "Sil",
567 (Revoke, En) => "Revoke",
568 (Revoke, Tr) => "İptal et",
569 (Never, En) => "never",
570 (Never, Tr) => "hiç",
571 (Name, En) => "Name",
572 (Name, Tr) => "Ad",
573 (Expires, En) => "Expires",
574 (Expires, Tr) => "Bitiş",
575 (Upload, En) => "Upload",
576 (Upload, Tr) => "Yükle",
577 (Remove, En) => "Remove",
578 (Remove, Tr) => "Kaldır",
579 (Create, En) => "Create",
580 (Create, Tr) => "Oluştur",
581
582 (TwoFactorCheck, En) => "Two-factor check",
583 (TwoFactorCheck, Tr) => "İki faktörlü doğrulama",
584 (TwoFactorCheckSub, En) => {
585 "Enter the 6-digit code from your authenticator app, or one of your recovery codes."
586 }
587 (TwoFactorCheckSub, Tr) => {
588 "Doğrulama uygulamanızdaki 6 haneli kodu ya da kurtarma kodlarından birini girin."
589 }
590 (ErrInvalidLogin, En) => "invalid username or password",
591 (ErrInvalidLogin, Tr) => "Kullanıcı adı ya da parola yanlış.",
592 (ErrThrottled, En) => "Too many attempts. Try again later.",
593 (ErrThrottled, Tr) => "Çok fazla deneme yapıldı. Biraz sonra tekrar deneyin.",
594 (ErrInvalidCode, En) => {
595 "That code is not right. Check your authenticator app, or use a recovery code."
596 }
597 (ErrInvalidCode, Tr) => {
598 "Bu kod doğru değil. Doğrulama uygulamanızı kontrol edin ya da bir kurtarma kodu kullanın."
599 }
600
601 (Setup, En) => "Setup",
602 (Setup, Tr) => "Kurulum",
603 (WelcomeToForge, En) => "Welcome to forge",
604 (WelcomeToForge, Tr) => "forge'a hoş geldiniz",
605 (SetupSub, En) => {
606 "Create the first administrator account. This page closes after it is created."
607 }
608 (SetupSub, Tr) => "İlk yönetici hesabını oluşturun. Hesap oluşturulunca bu sayfa kapanır.",
609 (AtLeast12Chars, En) => "At least 12 characters.",
610 (AtLeast12Chars, Tr) => "En az 12 karakter.",
611 (ConfirmPassword, En) => "Confirm password",
612 (ConfirmPassword, Tr) => "Parolayı doğrula",
613 (CreateAdmin, En) => "Create admin",
614 (CreateAdmin, Tr) => "Yönetici oluştur",
615
616 (ErrPasswordsDiffer, En) => "The passwords do not match.",
617 (ErrPasswordsDiffer, Tr) => "Parolalar eşleşmiyor.",
618 (ErrWrongPassword, En) => "Current password is incorrect.",
619 (ErrWrongPassword, Tr) => "Mevcut parola yanlış.",
620 (ErrWrongConfirmCode, En) => {
621 "That code is not right. Check the time on your device and try again."
622 }
623 (ErrWrongConfirmCode, Tr) => {
624 "Bu kod doğru değil. Cihazınızdaki saati kontrol edip tekrar deneyin."
625 }
626 (ErrWrongCode, En) => {
627 "That code is not right. Use a code from your authenticator app or a recovery code."
628 }
629 (ErrWrongCode, Tr) => {
630 "Bu kod doğru değil. Doğrulama uygulamanızdaki bir kodu ya da bir kurtarma kodunu kullanın."
631 }
632
633 (OkUsernameUpdated, En) => "Username updated.",
634 (OkUsernameUpdated, Tr) => "Kullanıcı adı güncellendi.",
635 (OkAvatarUpdated, En) => "Avatar updated.",
636 (OkAvatarUpdated, Tr) => "Avatar güncellendi.",
637 (OkAvatarRemoved, En) => "Avatar removed.",
638 (OkAvatarRemoved, Tr) => "Avatar kaldırıldı.",
639 (OkProfileSaved, En) => "Profile saved.",
640 (OkProfileSaved, Tr) => "Profil kaydedildi.",
641 (CurrentUsername, En) => "Current username: ",
642 (CurrentUsername, Tr) => "Mevcut kullanıcı adı: ",
643 (UsernameHint, En) => {
644 "Only the capitalization can be changed. Your lookup name stays the same."
645 }
646 (UsernameHint, Tr) => {
647 "Yalnızca büyük/küçük harf değiştirilebilir. Giriş adınız aynı kalır."
648 }
649 (Avatar, En) => "Avatar",
650 (Avatar, Tr) => "Avatar",
651 (AvatarAlt, En) => "Your avatar",
652 (AvatarAlt, Tr) => "Avatarınız",
653 (Image, En) => "Image",
654 (Image, Tr) => "Görsel",
655 (AvatarHint, En) => {
656 "PNG, JPEG, WebP or GIF, up to 5 MiB. Images are cropped to a square and resized to 256 by 256 pixels."
657 }
658 (AvatarHint, Tr) => {
659 "PNG, JPEG, WebP ya da GIF, en fazla 5 MiB. Görseller kareye kırpılır ve 256 x 256 piksele küçültülür."
660 }
661 (RemoveAvatar, En) => "Remove avatar",
662 (RemoveAvatar, Tr) => "Avatarı kaldır",
663 (AboutAndTimezone, En) => "About and timezone",
664 (AboutAndTimezone, Tr) => "Hakkında ve saat dilimi",
665 (About, En) => "About",
666 (About, Tr) => "Hakkında",
667 (AboutHint, En) => "At most 2000 characters. Markdown is supported.",
668 (AboutHint, Tr) => "En fazla 2000 karakter. Markdown desteklenir.",
669 (Timezone, En) => "Timezone",
670 (Timezone, Tr) => "Saat dilimi",
671 (ErrChooseImage, En) => "Choose an image to upload.",
672 (ErrChooseImage, Tr) => "Yüklemek için bir görsel seçin.",
673
674 (OkPasswordChanged, En) => "Password changed. Your other sessions were signed out.",
675 (OkPasswordChanged, Tr) => "Parola değiştirildi. Diğer oturumlarınız kapatıldı.",
676 (OkEmailSaved, En) => "Email saved.",
677 (OkEmailSaved, Tr) => "E-posta kaydedildi.",
678 (OkTwoFactorOn, En) => "Two-factor authentication is on.",
679 (OkTwoFactorOn, Tr) => "İki faktörlü kimlik doğrulama açık.",
680 (OkTwoFactorOff, En) => "Two-factor authentication is off.",
681 (OkTwoFactorOff, Tr) => "İki faktörlü kimlik doğrulama kapalı.",
682 (OkRecoveryCodesNew, En) => "New recovery codes were created. The old ones no longer work.",
683 (OkRecoveryCodesNew, Tr) => "Yeni kurtarma kodları oluşturuldu. Eskileri artık çalışmaz.",
684 (SecurityLog, En) => "Security log",
685 (SecurityLog, Tr) => "Güvenlik günlüğü",
686 (SecurityLogRest, En) => " lists your sign-ins and changes to your account.",
687 (SecurityLogRest, Tr) => " girişlerini ve hesabınızdaki değişiklikleri listeler.",
688 (RecoveryCodes, En) => "Recovery codes",
689 (RecoveryCodes, Tr) => "Kurtarma kodları",
690 (RecoveryCodesSave, En) => {
691 "Save these codes now. They are shown only once. Each code works one time, and a code can replace your authenticator app if you lose it."
692 }
693 (RecoveryCodesSave, Tr) => {
694 "Bu kodları şimdi kaydedin. Yalnızca bir kez gösterilir. Her kod bir kez çalışır; doğrulama uygulamanızı kaybederseniz kodlardan biri onun yerini tutabilir."
695 }
696 (CurrentPassword, En) => "Current password",
697 (CurrentPassword, Tr) => "Mevcut parola",
698 (NewPassword, En) => "New password",
699 (NewPassword, Tr) => "Yeni parola",
700 (NewPasswordHint, En) => "At least 10 characters. Your other sessions will be signed out.",
701 (NewPasswordHint, Tr) => "En az 10 karakter. Diğer oturumlarınız kapatılır.",
702 (ConfirmNewPassword, En) => "Confirm new password",
703 (ConfirmNewPassword, Tr) => "Yeni parolayı doğrula",
704 (ChangePassword, En) => "Change password",
705 (ChangePassword, Tr) => "Parolayı değiştir",
706 (SaveEmail, En) => "Save email",
707 (SaveEmail, Tr) => "E-postayı kaydet",
708 (TwoFactorAuth, En) => "Two-factor authentication",
709 (TwoFactorAuth, Tr) => "İki faktörlü kimlik doğrulama",
710 (RegenerateCodes, En) => "Regenerate recovery codes",
711 (RegenerateCodes, Tr) => "Kurtarma kodlarını yenile",
712 (RegenerateCodesHint, En) => {
713 "Creating new codes makes the old ones stop working. Enter a code from your authenticator app to continue."
714 }
715 (RegenerateCodesHint, Tr) => {
716 "Yeni kodlar oluşturmak eskilerini geçersiz kılar. Devam etmek için doğrulama uygulamanızdaki bir kodu girin."
717 }
718 (TurnOffTwoFactor, En) => "Turn off two-factor authentication",
719 (TurnOffTwoFactor, Tr) => "İki faktörlü kimlik doğrulamayı kapat",
720 (TurnOffTwoFactorButton, En) => "Turn off two-factor",
721 (TurnOffTwoFactorButton, Tr) => "İki faktörlüyü kapat",
722 (TwoFactorPendingIntro, En) => {
723 "Add this account to your authenticator app. Enter the secret, or open the link on a device that has the app."
724 }
725 (TwoFactorPendingIntro, Tr) => {
726 "Bu hesabı doğrulama uygulamanıza ekleyin. Anahtarı girin ya da uygulamanın yüklü olduğu bir cihazda bağlantıyı açın."
727 }
728 (SecretPrefix, En) => "Secret: ",
729 (SecretPrefix, Tr) => "Anahtar: ",
730 (LinkPrefix, En) => "Link: ",
731 (LinkPrefix, Tr) => "Bağlantı: ",
732 (CodeFromApp, En) => "Code from the app",
733 (CodeFromApp, Tr) => "Uygulamadaki kod",
734 (ConfirmAndTurnOn, En) => "Confirm and turn on",
735 (ConfirmAndTurnOn, Tr) => "Doğrula ve aç",
736 (TwoFactorOffIntro, En) => {
737 "Two-factor authentication is off. When it is on, signing in also asks for a code from an authenticator app. Git over HTTPS uses access tokens and is not affected."
738 }
739 (TwoFactorOffIntro, Tr) => {
740 "İki faktörlü kimlik doğrulama kapalı. Açıkken girişte doğrulama uygulamanızdan bir kod da istenir. HTTPS üzerinden Git erişim belirteçlerini kullanır; bu etkilenmez."
741 }
742 (Enable2fa, En) => "Enable 2FA",
743 (Enable2fa, Tr) => "2FA'yı aç",
744 (DeleteAccount, En) => "Delete account",
745 (DeleteAccount, Tr) => "Hesabı sil",
746 (DeleteAccountIntro, En) => {
747 "Deleting your account removes your sessions, SSH keys, access tokens and stars. Your issues, comments and reviews stay and are shown as written by a deleted user. This cannot be undone."
748 }
749 (DeleteAccountIntro, Tr) => {
750 "Hesabınızı silmek; oturumlarınızı, SSH anahtarlarınızı, erişim belirteçlerinizi ve yıldızlarınızı kaldırır. Sorunlarınız, yorumlarınız ve incelemeleriniz kalır; silinmiş bir kullanıcı tarafından yazılmış gibi gösterilir. Bu işlem geri alınamaz."
751 }
752 (TypeUsernameToConfirm, En) => "Type your username to confirm",
753 (TypeUsernameToConfirm, Tr) => "Onaylamak için kullanıcı adınızı yazın",
754 (TransferOrDeleteRepos, En) => {
755 "Transfer or delete these repositories before deleting your account:"
756 }
757 (TransferOrDeleteRepos, Tr) => "Hesabınızı silmeden önce bu depoları devredin ya da silin:",
758 (ErrConfirmUsername, En) => "Type your username exactly to confirm.",
759 (ErrConfirmUsername, Tr) => "Onaylamak için kullanıcı adınızı birebir yazın.",
760
761 (NoSshKeys, En) => "No SSH keys yet.",
762 (NoSshKeys, Tr) => "Henüz SSH anahtarı yok.",
763 (NoGpgKeys, En) => "No GPG keys yet.",
764 (NoGpgKeys, Tr) => "Henüz GPG anahtarı yok.",
765 (AddSshKeyTitle, En) => "Add an SSH key",
766 (AddSshKeyTitle, Tr) => "SSH anahtarı ekle",
767 (AddGpgKeyTitle, En) => "Add a GPG key",
768 (AddGpgKeyTitle, Tr) => "GPG anahtarı ekle",
769 (TitleOptional, En) => "Title (optional)",
770 (TitleOptional, Tr) => "Başlık (isteğe bağlı)",
771 (PublicKeyLabel, En) => "Public key",
772 (PublicKeyLabel, Tr) => "Ortak anahtar",
773 (PublicKeyHint, En) => "Paste the contents of id_ed25519.pub. RSA keys are not accepted.",
774 (PublicKeyHint, Tr) => {
775 "id_ed25519.pub dosyasının içeriğini yapıştırın. RSA anahtarları kabul edilmez."
776 }
777 (AddKeyButton, En) => "Add key",
778 (AddKeyButton, Tr) => "Anahtar ekle",
779 (ArmoredPublicKeyLabel, En) => "Public key (ASCII armored)",
780 (ArmoredPublicKeyLabel, Tr) => "Ortak anahtar (ASCII zırhlı)",
781 (ArmoredHint, En) => "Paste the output of gpg --armor --export with your key ID.",
782 (ArmoredHint, Tr) => {
783 "Anahtar kimliğinizle birlikte gpg --armor --export çıktısını yapıştırın."
784 }
785 (GpgUnavailable, En) => "GPG verification is unavailable on this server",
786 (GpgUnavailable, Tr) => "GPG doğrulaması bu sunucuda kullanılamıyor",
787
788 (YourNewToken, En) => "Your new token",
789 (YourNewToken, Tr) => "Yeni belirteciniz",
790 (CopyNow, En) => "Copy it now. It is shown only once and cannot be recovered later.",
791 (CopyNow, Tr) => "Şimdi kopyalayın. Yalnızca bir kez gösterilir ve sonradan geri alınamaz.",
792 (UseAsPassword, En) => {
793 "Use it as the password when a git client asks for one, with any username."
794 }
795 (UseAsPassword, Tr) => {
796 "Bir Git istemcisi parola istediğinde, herhangi bir kullanıcı adıyla birlikte bunu parola olarak kullanın."
797 }
798 (NoAccessTokens, En) => "No access tokens yet.",
799 (NoAccessTokens, Tr) => "Henüz erişim belirteci yok.",
800 (NewToken, En) => "New token",
801 (NewToken, Tr) => "Yeni belirteç",
802 (ExpiresIn30, En) => "In 30 days",
803 (ExpiresIn30, Tr) => "30 gün sonra",
804 (ExpiresIn90, En) => "In 90 days",
805 (ExpiresIn90, Tr) => "90 gün sonra",
806 (ExpiresIn365, En) => "In 365 days",
807 (ExpiresIn365, Tr) => "365 gün sonra",
808 (ExpiresNever, En) => "Never",
809 (ExpiresNever, Tr) => "Hiçbir zaman",
810 (ScopeNote, En) => "Scope: repo (read and write repositories you can access).",
811 (ScopeNote, Tr) => "Kapsam: repo (erişebildiğiniz depoları okuyabilir ve yazabilirsiniz).",
812 (CreateToken, En) => "Create token",
813 (CreateToken, Tr) => "Belirteç oluştur",
814
815 (UnknownBrowser, En) => "Unknown browser",
816 (UnknownBrowser, Tr) => "Bilinmeyen tarayıcı",
817 (ThisSession, En) => "This session",
818 (ThisSession, Tr) => "Bu oturum",
819 (RevokeAllOthers, En) => "Revoke all other sessions",
820 (RevokeAllOthers, Tr) => "Diğer tüm oturumları iptal et",
821
822 (Added, En) => "Added",
823 (Added, Tr) => "eklendi",
824 (LastUsed, En) => "last used",
825 (LastUsed, Tr) => "son kullanım",
826 (Created, En) => "Created",
827 (Created, Tr) => "Oluşturuldu",
828 (SignedIn, En) => "Signed in",
829 (SignedIn, Tr) => "Giriş",
830 (LastSeen, En) => "last seen",
831 (LastSeen, Tr) => "son görülme",
832 (ExpiresLabel, En) => "expires",
833 (ExpiresLabel, Tr) => "bitiş",
834
835 (ErrKeyEmpty, En) => "paste an OpenSSH public key line",
836 (ErrKeyEmpty, Tr) => "Bir OpenSSH ortak anahtar satırı yapıştırın.",
837 (ErrKeyTooLong, En) => "key is too long",
838 (ErrKeyTooLong, Tr) => "Anahtar çok uzun.",
839 (ErrKeyMissingBody, En) => "key is missing its base64 body",
840 (ErrKeyMissingBody, Tr) => "Anahtarın base64 gövdesi eksik.",
841 (ErrKeyRsa, En) => {
842 "RSA keys are not accepted; please use an ed25519 key (ssh-keygen -t ed25519)"
843 }
844 (ErrKeyRsa, Tr) => {
845 "RSA anahtarları kabul edilmez; lütfen bir ed25519 anahtarı kullanın (ssh-keygen -t ed25519)."
846 }
847 (ErrKeyUnsupportedType, En) => "unsupported key type",
848 (ErrKeyUnsupportedType, Tr) => "Desteklenmeyen anahtar türü",
849 (ErrKeyBadBase64, En) => "key body is not valid base64",
850 (ErrKeyBadBase64, Tr) => "Anahtar gövdesi geçerli bir base64 değil.",
851 (ErrKeyTruncated, En) => "key body is truncated",
852 (ErrKeyTruncated, Tr) => "Anahtar gövdesi eksik.",
853 (ErrKeyMismatch, En) => "key body does not match the key type",
854 (ErrKeyMismatch, Tr) => "Anahtar gövdesi anahtar türüyle eşleşmiyor.",
855 (ForgotTitle, En) => "Forgot password",
856 (ForgotTitle, Tr) => "Parolamı unuttum",
857 (ForgotSub, En) => {
858 "Enter the address on your account. If it is a verified address, we send a reset link."
859 }
860 (ForgotSub, Tr) => {
861 "Hesabınızdaki e-posta adresini girin. Doğrulanmış bir adres ise sıfırlama bağlantısı gönderilir."
862 }
863 (ForgotSubmit, En) => "Send reset link",
864 (ForgotSubmit, Tr) => "Sıfırlama bağlantısı gönder",
865 (ForgotSent, En) => {
866 "If that address belongs to a verified account, a reset link is on its way. Check your inbox."
867 }
868 (ForgotSent, Tr) => {
869 "Bu adres doğrulanmış bir hesaba aitse sıfırlama bağlantısı yolda. Gelen kutunuzu kontrol edin."
870 }
871 (ResetTitle, En) => "Set a new password",
872 (ResetTitle, Tr) => "Yeni parola belirle",
873 (ResetSub, En) => "The link works once. Choose a new password for your account.",
874 (ResetSub, Tr) => {
875 "Bu bağlantı yalnızca bir kez kullanılabilir. Hesabınız için yeni bir parola seçin."
876 }
877 (ResetSubmit, En) => "Save new password",
878 (ResetSubmit, Tr) => "Yeni parolayı kaydet",
879 (ResetInvalid, En) => "This reset link is invalid, used or expired. Request a new one.",
880 (ResetInvalid, Tr) => {
881 "Bu sıfırlama bağlantısı geçersiz, kullanılmış ya da süresi dolmuş. Yeni bir tane isteyin."
882 }
883 (ResetDone, En) => {
884 "Your password is changed. All other sessions were signed out. Log in with the new password."
885 }
886 (ResetDone, Tr) => {
887 "Parolanız değiştirildi. Diğer tüm oturumlar kapatıldı. Yeni parolanızla giriş yapın."
888 }
889 (RegisterTitle, En) => "Create an account",
890 (RegisterTitle, Tr) => "Hesap oluştur",
891 (RegisterSub, En) => {
892 "Pick a username and a password. We send a link to confirm your address."
893 }
894 (RegisterSub, Tr) => {
895 "Bir kullanıcı adı ve parola seçin. Adresinizi doğrulamak için bir bağlantı göndereceğiz."
896 }
897 (RegisterInviteSub, En) => {
898 "You were invited. This account uses the address the invite was sent to."
899 }
900 (RegisterInviteSub, Tr) => {
901 "Davet edildiniz. Bu hesap, davetin gönderildiği adresi kullanır."
902 }
903 (RegisterNeedsInvite, En) => {
904 "Registration is by invitation only. Ask an admin for an invite link."
905 }
906 (RegisterNeedsInvite, Tr) => {
907 "Kayıt yalnızca davetle yapılır. Bir yöneticiden davet bağlantısı isteyin."
908 }
909 (RegisterInvalidInvite, En) => "This invite is invalid, already used or expired.",
910 (RegisterInvalidInvite, Tr) => {
911 "Bu davet geçersiz, daha önce kullanılmış ya da süresi dolmuş."
912 }
913 (RegisterSubmit, En) => "Create account",
914 (RegisterSubmit, Tr) => "Hesap oluştur",
915 (VerifyTitle, En) => "Confirm your email",
916 (VerifyTitle, Tr) => "E-posta adresini doğrula",
917 (VerifyConfirmSub, En) => "Confirm that this address is yours.",
918 (VerifyConfirmSub, Tr) => "Bu adresin size ait olduğunu doğrulayın.",
919 (VerifyConfirm, En) => "Confirm address",
920 (VerifyConfirm, Tr) => "Adresi doğrula",
921 (VerifyDone, En) => "Your email address is verified.",
922 (VerifyDone, Tr) => "E-posta adresin doğrulandı.",
923 (VerifyInvalid, En) => "This verification link is invalid, used or expired.",
924 (VerifyInvalid, Tr) => "Bu doğrulama bağlantısı geçersiz, kullanılmış ya da süresi dolmuş.",
925 (VerifyVerified, En) => "Verified",
926 (VerifyVerified, Tr) => "Doğrulandı",
927 (VerifyUnverified, En) => {
928 "Not verified. Password reset mail is only sent to verified addresses."
929 }
930 (VerifyUnverified, Tr) => {
931 "Doğrulanmadı. Parola sıfırlama e-postaları yalnızca doğrulanmış adreslere gönderilir."
932 }
933 (VerifyResend, En) => "Send the verification link again",
934 (VerifyResend, Tr) => "Doğrulama bağlantısını yeniden gönder",
935 (OkVerifySent, En) => "We sent a verification link to your address.",
936 (OkVerifySent, Tr) => "Adresinize bir doğrulama bağlantısı gönderildi.",
937 (InvitesTitle, En) => "Invite someone",
938 (InvitesTitle, Tr) => "Birini davet et",
939 (InvitesSub, En) => {
940 "We email an invite link. It works once, for that address only, and expires in 7 days."
941 }
942 (InvitesSub, Tr) => {
943 "Bir davet bağlantısı e-posta ile gönderilir. Bağlantı yalnızca o adres için, bir kez kullanılabilir ve 7 gün geçerlidir."
944 }
945 (InvitesSubmit, En) => "Send invite",
946 (InvitesSubmit, Tr) => "Davet gönder",
947 (InvitesSent, En) => "The invite was sent.",
948 (InvitesSent, Tr) => "Davet gönderildi.",
949 (InvitesOff, En) => {
950 "Invites need registration set to invite or open. Change registration in the config first."
951 }
952 (InvitesOff, Tr) => {
953 "Davet göndermek için kayıt ayarı invite ya da open olmalı. Önce yapılandırmada kayıt ayarını değiştirin."
954 }
955 (MailVerbOpened, En) => "opened this",
956 (MailVerbCommented, En) => "commented on this",
957 (MailVerbClosed, En) => "closed this",
958 (MailVerbReopened, En) => "reopened this",
959 (MailVerbMerged, En) => "merged this",
960 (MailVerbReviewed, En) => "reviewed this",
961 (MailVerbAssigned, En) => "assigned you to this",
962 (MailVerbPushed, En) => "pushed new commits",
963 (MailVerbMentioned, En) => "mentioned you",
964 (MailSomeone, En) => "Someone",
965 (MailReasonMention, En) => "you were mentioned",
966 (MailReasonAssign, En) => "you were assigned",
967 (MailReasonReviewRequested, En) => "your review was requested",
968 (MailReasonAuthor, En) => "you opened it",
969 (MailReasonComment, En) => "you commented on it",
970 (MailReasonSubscribed, En) => "you are subscribed to it",
971 (MailReasonWatching, En) => "you watch this repository",
972 (MenuNotificationEmails, En) => "Notification emails",
973 (EmailNotifyLabel, En) => "Email me about notifications",
974 (EmailNotifyHelp, En) => {
975 "Each notification is sent to your verified address. Notifications on the site are not affected."
976 }
977 (EmailNotifyUnverified, En) => {
978 "Verify your email address first. Only verified addresses get notification emails."
979 }
980 (EmailNotifyServerOff, En) => {
981 "Email is turned off on this server, so no notification emails are sent."
982 }
983 (EmailNotifySaved, En) => "Notification settings saved.",
984 (MailVerbOpened, Tr) => "bunu açtı",
985 (MailVerbCommented, Tr) => "yorum yaptı",
986 (MailVerbClosed, Tr) => "bunu kapattı",
987 (MailVerbReopened, Tr) => "bunu yeniden açtı",
988 (MailVerbMerged, Tr) => "bunu birleştirdi",
989 (MailVerbReviewed, Tr) => "bunu inceledi",
990 (MailVerbAssigned, Tr) => "sizi bunun sorumlusu olarak atadı",
991 (MailVerbPushed, Tr) => "yeni commit'ler gönderdi",
992 (MailVerbMentioned, Tr) => "sizden bahsetti",
993 (MailSomeone, Tr) => "Birisi",
994 (MailReasonMention, Tr) => "sizden bahsedildi",
995 (MailReasonAssign, Tr) => "size atandı",
996 (MailReasonReviewRequested, Tr) => "incelemeniz istendi",
997 (MailReasonAuthor, Tr) => "bunu siz açtınız",
998 (MailReasonComment, Tr) => "bunda yorum yaptınız",
999 (MailReasonSubscribed, Tr) => "bunu takip ediyorsunuz",
1000 (MailReasonWatching, Tr) => "bu depoyu izliyorsunuz",
1001 (MenuNotificationEmails, Tr) => "Bildirim e-postaları",
1002 (EmailNotifyLabel, Tr) => "Bildirimler hakkında bana e-posta gönder",
1003 (EmailNotifyHelp, Tr) => {
1004 "Her bildirim doğrulanmış adresinize gönderilir. Sitedeki bildirimler bundan etkilenmez."
1005 }
1006 (EmailNotifyUnverified, Tr) => {
1007 "Önce e-posta adresinizi doğrulayın. Bildirim e-postaları yalnızca doğrulanmış adreslere gönderilir."
1008 }
1009 (EmailNotifyServerOff, Tr) => {
1010 "Bu sunucuda e-posta kapalı, bu yüzden bildirim e-postası gönderilmiyor."
1011 }
1012 (EmailNotifySaved, Tr) => "Bildirim ayarları kaydedildi.",
1013 }
1014}
1015
1016/// A unit of elapsed time for "3 days ago".
1017#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1018pub enum TimeUnit {
1019 Minute,
1020 Hour,
1021 Day,
1022 Month,
1023 Year,
1024}
1025
1026/// "just now", or "az önce".
1027pub fn just_now(lang: Lang) -> &'static str {
1028 match lang {
1029 Lang::En => "just now",
1030 Lang::Tr => "az önce",
1031 }
1032}
1033
1034/// "3 days ago", or "3 gün önce". Turkish nouns stay singular after a number.
1035pub fn ago(lang: Lang, n: i64, unit: TimeUnit) -> String {
1036 match lang {
1037 Lang::En => {
1038 let word = match unit {
1039 TimeUnit::Minute => "minute",
1040 TimeUnit::Hour => "hour",
1041 TimeUnit::Day => "day",
1042 TimeUnit::Month => "month",
1043 TimeUnit::Year => "year",
1044 };
1045 let plural = if n == 1 { "" } else { "s" };
1046 format!("{n} {word}{plural} ago")
1047 }
1048 Lang::Tr => {
1049 let word = match unit {
1050 TimeUnit::Minute => "dakika",
1051 TimeUnit::Hour => "saat",
1052 TimeUnit::Day => "gün",
1053 TimeUnit::Month => "ay",
1054 TimeUnit::Year => "yıl",
1055 };
1056 format!("{n} {word} önce")
1057 }
1058 }
1059}
1060
1061/// The name shown for the ghost account that owns the content of deleted users.
1062pub fn deleted_user(lang: Lang) -> &'static str {
1063 match lang {
1064 Lang::En => "Deleted user",
1065 Lang::Tr => "Silinmiş kullanıcı",
1066 }
1067}
1068
1069/// The visibility badge text: "public" or "herkese açık", "private" or "özel".
1070pub fn visibility_label(lang: Lang, visibility: forge_core::Visibility) -> &'static str {
1071 match (lang, visibility) {
1072 (Lang::En, forge_core::Visibility::Public) => "public",
1073 (Lang::En, forge_core::Visibility::Private) => "private",
1074 (Lang::Tr, forge_core::Visibility::Public) => "herkese açık",
1075 (Lang::Tr, forge_core::Visibility::Private) => "özel",
1076 }
1077}
1078
1079/// "Added 2026-01-02", with the last use appended when there is one.
1080pub fn added_line(lang: Lang, created: &str, last_used: Option<&str>) -> String {
1081 let added = match lang {
1082 Lang::En => format!("{} {created}", t(lang, Key::Added)),
1083 Lang::Tr => format!("{created} tarihinde {}", t(lang, Key::Added)),
1084 };
1085 match last_used {
1086 None => added,
1087 Some(used) => match lang {
1088 Lang::En => format!("{added} · {} {used}", t(lang, Key::LastUsed)),
1089 Lang::Tr => format!("{added} · {}: {used}", t(lang, Key::LastUsed)),
1090 },
1091 }
1092}
1093
1094/// A token's dates: "Created X · last used Y · expires Z". A missing use or expiry reads "never".
1095pub fn token_meta(
1096 lang: Lang,
1097 created: &str,
1098 last_used: Option<&str>,
1099 expires: Option<&str>,
1100) -> String {
1101 let used = last_used.unwrap_or(t(lang, Key::Never));
1102 let expiry = expires.unwrap_or(t(lang, Key::Never));
1103 dated_line(
1104 lang,
1105 (Key::Created, created),
1106 (Key::LastUsed, used),
1107 (Key::ExpiresLabel, expiry),
1108 )
1109}
1110
1111/// A web session's dates: "Signed in X · last seen Y · expires Z".
1112pub fn session_meta(lang: Lang, signed_in: &str, last_seen: &str, expires: &str) -> String {
1113 dated_line(
1114 lang,
1115 (Key::SignedIn, signed_in),
1116 (Key::LastSeen, last_seen),
1117 (Key::ExpiresLabel, expires),
1118 )
1119}
1120
1121/// Three labelled dates joined with " · ". English reads "Label value"; Turkish reads "Etiket: value".
1122fn dated_line(lang: Lang, a: (Key, &str), b: (Key, &str), c: (Key, &str)) -> String {
1123 match lang {
1124 Lang::En => format!(
1125 "{} {} · {} {} · {} {}",
1126 t(lang, a.0),
1127 a.1,
1128 t(lang, b.0),
1129 b.1,
1130 t(lang, c.0),
1131 c.1
1132 ),
1133 Lang::Tr => format!(
1134 "{}: {} · {}: {} · {}: {}",
1135 t(lang, a.0),
1136 a.1,
1137 t(lang, b.0),
1138 b.1,
1139 t(lang, c.0),
1140 c.1
1141 ),
1142 }
1143}
1144
1145/// "Unsupported key type \"dsa\"" in the viewer's language.
1146pub fn unsupported_key_type(lang: Lang, algorithm: &str) -> String {
1147 match lang {
1148 Lang::En => format!("{} {algorithm:?}", t(lang, Key::ErrKeyUnsupportedType)),
1149 Lang::Tr => format!("{}: {algorithm:?}", t(lang, Key::ErrKeyUnsupportedType)),
1150 }
1151}
1152
1153/// "Two-factor authentication is on. Unused recovery codes: 7."
1154pub fn two_factor_on_line(lang: Lang, unused: u64) -> String {
1155 match lang {
1156 Lang::En => format!("Two-factor authentication is on. Unused recovery codes: {unused}."),
1157 Lang::Tr => format!(
1158 "İki faktörlü kimlik doğrulama açık. Kullanılmamış kurtarma kodu sayısı: {unused}."
1159 ),
1160 }
1161}
1162
1163/// The message for an "about" text longer than `max` characters.
1164pub fn about_too_long(lang: Lang, max: usize) -> String {
1165 match lang {
1166 Lang::En => format!("About must be at most {max} characters."),
1167 Lang::Tr => format!("Hakkında en fazla {max} karakter olabilir."),
1168 }
1169}
1170
1171/// The message for a password shorter than `min` characters.
1172pub fn password_too_short(lang: Lang, min: usize) -> String {
1173 match lang {
1174 Lang::En => format!("Password must be at least {min} characters."),
1175 Lang::Tr => format!("Parola en az {min} karakter olmalı."),
1176 }
1177}
1178
1179/// The message for a password longer than `max` characters.
1180pub fn password_too_long(lang: Lang, max: usize) -> String {
1181 match lang {
1182 Lang::En => format!("Password must be at most {max} characters."),
1183 Lang::Tr => format!("Parola en fazla {max} karakter olabilir."),
1184 }
1185}
1186
1187/// The message for a token name outside 1 to `max` characters.
1188pub fn token_name_length(lang: Lang, max: usize) -> String {
1189 match lang {
1190 Lang::En => format!("Name must be 1 to {max} characters."),
1191 Lang::Tr => format!("Ad 1 ile {max} karakter arasında olmalı."),
1192 }
1193}
1194
1195/// The last line of a notification email: why it was sent, and where to change that.
1196pub fn mail_footer(lang: Lang, reason: &str, settings_url: &str) -> String {
1197 match lang {
1198 Lang::En => format!(
1199 "You are receiving this because {reason}. Manage email notifications: {settings_url}"
1200 ),
1201 Lang::Tr => format!(
1202 "Bu e-postayı şu nedenle alıyorsunuz: {reason}. E-posta bildirimlerini yönetin: {settings_url}"
1203 ),
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use http::HeaderValue;
1211
1212 fn headers(value: &'static str) -> HeaderMap {
1213 let mut map = HeaderMap::new();
1214 map.insert(ACCEPT_LANGUAGE, HeaderValue::from_static(value));
1215 map
1216 }
1217
1218 #[test]
1219 fn every_key_has_text_in_both_languages() {
1220 for key in ALL {
1221 assert!(!t(Lang::En, *key).is_empty(), "{key:?} has no English text");
1222 assert!(!t(Lang::Tr, *key).is_empty(), "{key:?} has no Turkish text");
1223 }
1224 }
1225
1226 #[test]
1227 fn all_lists_each_key_once() {
1228 for (i, key) in ALL.iter().enumerate() {
1229 assert!(!ALL[i + 1..].contains(key), "{key:?} is listed twice");
1230 }
1231 }
1232
1233 #[test]
1234 fn stored_language_wins_over_the_browser() {
1235 let turkish = headers("tr");
1236 assert_eq!(resolve(Some("en"), &turkish), Lang::En);
1237 assert_eq!(resolve(Some("tr"), &HeaderMap::new()), Lang::Tr);
1238 }
1239
1240 #[test]
1241 fn auto_and_missing_preferences_follow_the_browser() {
1242 assert_eq!(resolve(Some("auto"), &headers("tr-TR,en;q=0.5")), Lang::Tr);
1243 assert_eq!(resolve(None, &headers("tr")), Lang::Tr);
1244 assert_eq!(resolve(None, &HeaderMap::new()), Lang::En);
1245 }
1246
1247 #[test]
1248 fn accept_language_picks_the_highest_quality_supported_tag() {
1249 assert_eq!(resolve(None, &headers("de,en;q=0.8,tr;q=0.9")), Lang::Tr);
1250 assert_eq!(resolve(None, &headers("en;q=0.9,tr;q=0.5")), Lang::En);
1251 assert_eq!(resolve(None, &headers("tr;q=0,en")), Lang::En);
1252 assert_eq!(resolve(None, &headers("de,fr;q=0.9")), Lang::En);
1253 assert_eq!(resolve(None, &headers("tr;q=oops,en;q=0.1")), Lang::En);
1254 }
1255
1256 #[test]
1257 fn unknown_stored_code_is_english() {
1258 assert_eq!(Lang::from_code("fr"), Lang::En);
1259 assert_eq!(Lang::from_code("tr"), Lang::Tr);
1260 }
1261
1262 #[test]
1263 fn html_codes_match_the_languages() {
1264 assert_eq!(Lang::En.code(), "en");
1265 assert_eq!(Lang::Tr.code(), "tr");
1266 }
1267}