//! Per-viewer UI strings, following im's `i18n.rs` strategy: one variant per UI phrase, an //! exhaustive `t` match so a missing translation fails to compile rather than falling through //! at render time, and small `fmt`-style helpers for sentences that carry arguments. //! //! A page gets its language with `lang(cx, viewer)` and passes it to the strings it draws: //! `t(lang, Key::Explore)`. The shell stamps `` from the same value. //! //! Wave 1 covers the shell, the auth pages, first-run setup and the settings pages. Other //! pages still hold English literals until their own wave. Pages are being moved to per-group //! key modules in `i18n/.rs`, each with its own `K` enum and `t` match, so a group can //! be translated without touching the others. use http::HeaderMap; use http::header::ACCEPT_LANGUAGE; use topcoat::context::Cx; use topcoat::router::request::headers; use forge_core::User; pub mod admin; pub mod code; pub mod community; pub mod integrations; pub mod issues; pub mod misc; pub mod orgs; pub mod pulls; pub mod pulls_review; pub mod releases_ci; pub mod repo_admin; /// A UI language. English is the default; Turkish is the only other one for now. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Lang { En, Tr, } impl Lang { /// Parses a stored language code. `auto` and anything unknown fall back to English. pub fn from_code(code: &str) -> Lang { match code { "tr" => Lang::Tr, _ => Lang::En, } } /// The primary language subtag of a language tag (`tr-TR`, `en-US`), or `None` when this /// build has no translation for it. fn from_tag(tag: &str) -> Option { let primary = tag.split('-').next()?.trim(); match primary.to_ascii_lowercase().as_str() { "tr" => Some(Lang::Tr), "en" => Some(Lang::En), _ => None, } } /// The `` value. pub fn code(self) -> &'static str { match self { Lang::En => "en", Lang::Tr => "tr", } } } /// The language for a request. The viewer's stored preference wins when it names a language, /// then the browser's `Accept-Language`, then English. `user_language` is the stored column, and /// `auto` means "no preference". pub fn resolve(user_language: Option<&str>, request_headers: &HeaderMap) -> Lang { match user_language { Some(code) if code != "auto" => Lang::from_code(code), _ => accept_language(request_headers).unwrap_or(Lang::En), } } /// The language for the current request, from the viewer (if any) and the request headers. pub fn lang(cx: &Cx, viewer: Option<&User>) -> Lang { resolve(viewer.map(|u| u.language.as_str()), headers(cx)) } /// The best-quality `Accept-Language` entry this build can serve. Entries with `q=0` or a bad /// quality are skipped, and so are tags it has no translation for. fn accept_language(request_headers: &HeaderMap) -> Option { let mut best: Option<(f32, Lang)> = None; for value in request_headers.get_all(ACCEPT_LANGUAGE) { let Ok(text) = value.to_str() else { continue; }; for item in text.split(',') { let mut parts = item.split(';'); let tag = parts.next().unwrap_or_default().trim(); let quality = parts .find_map(|p| p.trim().strip_prefix("q=")) .map_or(Some(1.0), |q| q.parse::().ok()); let Some(quality) = quality.filter(|q| *q > 0.0) else { continue; }; let Some(lang) = Lang::from_tag(tag) else { continue; }; if best.is_none_or(|(q, _)| quality > q) { best = Some((quality, lang)); } } } best.map(|(_, lang)| lang) } /// One variant per UI phrase. A typo'd key fails to compile rather than falling through to /// nothing at render time. #[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Key { // Navigation and the user menu. Explore, Notifications, NewRepository, Profile, SshKeys, GpgKeys, AccessTokens, Sessions, Security, AdminUsers, AdminAuditLog, AdminRunners, LogOut, LogIn, SiteMenu, // Shared form words. Username, Password, Email, EmailOptional, Code, Verify, Save, Delete, Revoke, Never, Name, Expires, Upload, Remove, Create, // Log in and two-factor. TwoFactorCheck, TwoFactorCheckSub, ErrInvalidLogin, ErrThrottled, ErrInvalidCode, // First-run setup. Setup, WelcomeToForge, SetupSub, AtLeast12Chars, ConfirmPassword, CreateAdmin, // Shared password and two-factor errors. ErrPasswordsDiffer, ErrWrongPassword, ErrWrongConfirmCode, ErrWrongCode, // Profile. OkUsernameUpdated, OkAvatarUpdated, OkAvatarRemoved, OkProfileSaved, CurrentUsername, UsernameHint, Avatar, AvatarAlt, Image, AvatarHint, RemoveAvatar, AboutAndTimezone, About, AboutHint, Timezone, ErrChooseImage, // Account security. OkPasswordChanged, OkEmailSaved, OkTwoFactorOn, OkTwoFactorOff, OkRecoveryCodesNew, SecurityLog, SecurityLogRest, RecoveryCodes, RecoveryCodesSave, CurrentPassword, NewPassword, NewPasswordHint, ConfirmNewPassword, ChangePassword, SaveEmail, TwoFactorAuth, RegenerateCodes, RegenerateCodesHint, TurnOffTwoFactor, TurnOffTwoFactorButton, TwoFactorPendingIntro, SecretPrefix, LinkPrefix, CodeFromApp, ConfirmAndTurnOn, TwoFactorOffIntro, Enable2fa, DeleteAccount, DeleteAccountIntro, TypeUsernameToConfirm, TransferOrDeleteRepos, ErrConfirmUsername, // SSH and GPG keys. NoSshKeys, NoGpgKeys, AddSshKeyTitle, AddGpgKeyTitle, TitleOptional, PublicKeyLabel, PublicKeyHint, AddKeyButton, ArmoredPublicKeyLabel, ArmoredHint, GpgUnavailable, // Access tokens. YourNewToken, CopyNow, UseAsPassword, NoAccessTokens, NewToken, ExpiresIn30, ExpiresIn90, ExpiresIn365, ExpiresNever, ScopeNote, CreateToken, // Web sessions. UnknownBrowser, ThisSession, RevokeAllOthers, // Metadata lines (see the helpers below for the words in between). Added, LastUsed, Created, SignedIn, LastSeen, ExpiresLabel, // Parse problems for SSH keys (`settings_keys::KeyProblem`). ErrKeyEmpty, ErrKeyTooLong, ErrKeyMissingBody, ErrKeyRsa, ErrKeyUnsupportedType, ErrKeyBadBase64, ErrKeyTruncated, ErrKeyMismatch, // Account mail pages: reset, register, verify, invites. ForgotTitle, ForgotSub, ForgotSubmit, ForgotSent, ResetTitle, ResetSub, ResetSubmit, ResetInvalid, ResetDone, RegisterTitle, RegisterSub, RegisterInviteSub, RegisterNeedsInvite, RegisterInvalidInvite, RegisterSubmit, VerifyTitle, VerifyConfirmSub, VerifyConfirm, VerifyDone, VerifyInvalid, VerifyVerified, VerifyUnverified, VerifyResend, OkVerifySent, InvitesTitle, InvitesSub, InvitesSubmit, InvitesSent, InvitesOff, MailVerbOpened, MailVerbCommented, MailVerbClosed, MailVerbReopened, MailVerbMerged, MailVerbReviewed, MailVerbAssigned, MailVerbPushed, MailVerbMentioned, MailSomeone, MailReasonMention, MailReasonAssign, MailReasonReviewRequested, MailReasonAuthor, MailReasonComment, MailReasonSubscribed, MailReasonWatching, MenuNotificationEmails, EmailNotifyLabel, EmailNotifyHelp, EmailNotifyUnverified, EmailNotifyServerOff, EmailNotifySaved, } /// Every key, for the exhaustiveness test. Keep it in step with the enum. #[cfg(test)] pub const ALL: &[Key] = &[ Key::Explore, Key::Notifications, Key::NewRepository, Key::Profile, Key::SshKeys, Key::GpgKeys, Key::AccessTokens, Key::Sessions, Key::Security, Key::AdminUsers, Key::AdminAuditLog, Key::AdminRunners, Key::LogOut, Key::LogIn, Key::SiteMenu, Key::Username, Key::Password, Key::Email, Key::EmailOptional, Key::Code, Key::Verify, Key::Save, Key::Delete, Key::Revoke, Key::Never, Key::Name, Key::Expires, Key::Upload, Key::Remove, Key::Create, Key::TwoFactorCheck, Key::TwoFactorCheckSub, Key::ErrInvalidLogin, Key::ErrThrottled, Key::ErrInvalidCode, Key::Setup, Key::WelcomeToForge, Key::SetupSub, Key::AtLeast12Chars, Key::ConfirmPassword, Key::CreateAdmin, Key::ErrPasswordsDiffer, Key::ErrWrongPassword, Key::ErrWrongConfirmCode, Key::ErrWrongCode, Key::OkUsernameUpdated, Key::OkAvatarUpdated, Key::OkAvatarRemoved, Key::OkProfileSaved, Key::CurrentUsername, Key::UsernameHint, Key::Avatar, Key::AvatarAlt, Key::Image, Key::AvatarHint, Key::RemoveAvatar, Key::AboutAndTimezone, Key::About, Key::AboutHint, Key::Timezone, Key::ErrChooseImage, Key::OkPasswordChanged, Key::OkEmailSaved, Key::OkTwoFactorOn, Key::OkTwoFactorOff, Key::OkRecoveryCodesNew, Key::SecurityLog, Key::SecurityLogRest, Key::RecoveryCodes, Key::RecoveryCodesSave, Key::CurrentPassword, Key::NewPassword, Key::NewPasswordHint, Key::ConfirmNewPassword, Key::ChangePassword, Key::SaveEmail, Key::TwoFactorAuth, Key::RegenerateCodes, Key::RegenerateCodesHint, Key::TurnOffTwoFactor, Key::TurnOffTwoFactorButton, Key::TwoFactorPendingIntro, Key::SecretPrefix, Key::LinkPrefix, Key::CodeFromApp, Key::ConfirmAndTurnOn, Key::TwoFactorOffIntro, Key::Enable2fa, Key::DeleteAccount, Key::DeleteAccountIntro, Key::TypeUsernameToConfirm, Key::TransferOrDeleteRepos, Key::ErrConfirmUsername, Key::NoSshKeys, Key::NoGpgKeys, Key::AddSshKeyTitle, Key::AddGpgKeyTitle, Key::TitleOptional, Key::PublicKeyLabel, Key::PublicKeyHint, Key::AddKeyButton, Key::ArmoredPublicKeyLabel, Key::ArmoredHint, Key::GpgUnavailable, Key::YourNewToken, Key::CopyNow, Key::UseAsPassword, Key::NoAccessTokens, Key::NewToken, Key::ExpiresIn30, Key::ExpiresIn90, Key::ExpiresIn365, Key::ExpiresNever, Key::ScopeNote, Key::CreateToken, Key::UnknownBrowser, Key::ThisSession, Key::RevokeAllOthers, Key::Added, Key::LastUsed, Key::Created, Key::SignedIn, Key::LastSeen, Key::ExpiresLabel, Key::ErrKeyEmpty, Key::ErrKeyTooLong, Key::ErrKeyMissingBody, Key::ErrKeyRsa, Key::ErrKeyUnsupportedType, Key::ErrKeyBadBase64, Key::ErrKeyTruncated, Key::ErrKeyMismatch, Key::ForgotTitle, Key::ForgotSub, Key::ForgotSubmit, Key::ForgotSent, Key::ResetTitle, Key::ResetSub, Key::ResetSubmit, Key::ResetInvalid, Key::ResetDone, Key::RegisterTitle, Key::RegisterSub, Key::RegisterInviteSub, Key::RegisterNeedsInvite, Key::RegisterInvalidInvite, Key::RegisterSubmit, Key::VerifyTitle, Key::VerifyConfirmSub, Key::VerifyConfirm, Key::VerifyDone, Key::VerifyInvalid, Key::VerifyVerified, Key::VerifyUnverified, Key::VerifyResend, Key::OkVerifySent, Key::InvitesTitle, Key::InvitesSub, Key::InvitesSubmit, Key::InvitesSent, Key::InvitesOff, Key::MailVerbOpened, Key::MailVerbCommented, Key::MailVerbClosed, Key::MailVerbReopened, Key::MailVerbMerged, Key::MailVerbReviewed, Key::MailVerbAssigned, Key::MailVerbPushed, Key::MailVerbMentioned, Key::MailSomeone, Key::MailReasonMention, Key::MailReasonAssign, Key::MailReasonReviewRequested, Key::MailReasonAuthor, Key::MailReasonComment, Key::MailReasonSubscribed, Key::MailReasonWatching, Key::MenuNotificationEmails, Key::EmailNotifyLabel, Key::EmailNotifyHelp, Key::EmailNotifyUnverified, Key::EmailNotifyServerOff, Key::EmailNotifySaved, ]; /// The phrase a key names, in a user's language. pub fn t(lang: Lang, key: Key) -> &'static str { use Key::*; use Lang::*; match (key, lang) { (Explore, En) => "Explore", (Explore, Tr) => "Keşfet", (Notifications, En) => "Notifications", (Notifications, Tr) => "Bildirimler", (NewRepository, En) => "New repository", (NewRepository, Tr) => "Yeni depo", (Profile, En) => "Profile", (Profile, Tr) => "Profil", (SshKeys, En) => "SSH keys", (SshKeys, Tr) => "SSH anahtarları", (GpgKeys, En) => "GPG keys", (GpgKeys, Tr) => "GPG anahtarları", (AccessTokens, En) => "Access tokens", (AccessTokens, Tr) => "Erişim belirteçleri", (Sessions, En) => "Sessions", (Sessions, Tr) => "Oturumlar", (Security, En) => "Security", (Security, Tr) => "Güvenlik", (AdminUsers, En) => "Admin: users", (AdminUsers, Tr) => "Yönetici: kullanıcılar", (AdminAuditLog, En) => "Admin: audit log", (AdminAuditLog, Tr) => "Yönetici: denetim günlüğü", (AdminRunners, En) => "Admin: runners", (AdminRunners, Tr) => "Yönetici: çalıştırıcılar", (LogOut, En) => "Log out", (LogOut, Tr) => "Çıkış yap", (SiteMenu, En) => "Menu", (SiteMenu, Tr) => "Menü", (LogIn, En) => "Log in", (LogIn, Tr) => "Giriş yap", (Username, En) => "Username", (Username, Tr) => "Kullanıcı adı", (Password, En) => "Password", (Password, Tr) => "Parola", (Email, En) => "Email", (Email, Tr) => "E-posta", (EmailOptional, En) => "Email (optional)", (EmailOptional, Tr) => "E-posta (isteğe bağlı)", (Code, En) => "Code", (Code, Tr) => "Kod", (Verify, En) => "Verify", (Verify, Tr) => "Doğrula", (Save, En) => "Save", (Save, Tr) => "Kaydet", (Delete, En) => "Delete", (Delete, Tr) => "Sil", (Revoke, En) => "Revoke", (Revoke, Tr) => "İptal et", (Never, En) => "never", (Never, Tr) => "hiç", (Name, En) => "Name", (Name, Tr) => "Ad", (Expires, En) => "Expires", (Expires, Tr) => "Bitiş", (Upload, En) => "Upload", (Upload, Tr) => "Yükle", (Remove, En) => "Remove", (Remove, Tr) => "Kaldır", (Create, En) => "Create", (Create, Tr) => "Oluştur", (TwoFactorCheck, En) => "Two-factor check", (TwoFactorCheck, Tr) => "İki faktörlü doğrulama", (TwoFactorCheckSub, En) => { "Enter the 6-digit code from your authenticator app, or one of your recovery codes." } (TwoFactorCheckSub, Tr) => { "Doğrulama uygulamanızdaki 6 haneli kodu ya da kurtarma kodlarından birini girin." } (ErrInvalidLogin, En) => "invalid username or password", (ErrInvalidLogin, Tr) => "Kullanıcı adı ya da parola yanlış.", (ErrThrottled, En) => "Too many attempts. Try again later.", (ErrThrottled, Tr) => "Çok fazla deneme yapıldı. Biraz sonra tekrar deneyin.", (ErrInvalidCode, En) => { "That code is not right. Check your authenticator app, or use a recovery code." } (ErrInvalidCode, Tr) => { "Bu kod doğru değil. Doğrulama uygulamanızı kontrol edin ya da bir kurtarma kodu kullanın." } (Setup, En) => "Setup", (Setup, Tr) => "Kurulum", (WelcomeToForge, En) => "Welcome to forge", (WelcomeToForge, Tr) => "forge'a hoş geldiniz", (SetupSub, En) => { "Create the first administrator account. This page closes after it is created." } (SetupSub, Tr) => "İlk yönetici hesabını oluşturun. Hesap oluşturulunca bu sayfa kapanır.", (AtLeast12Chars, En) => "At least 12 characters.", (AtLeast12Chars, Tr) => "En az 12 karakter.", (ConfirmPassword, En) => "Confirm password", (ConfirmPassword, Tr) => "Parolayı doğrula", (CreateAdmin, En) => "Create admin", (CreateAdmin, Tr) => "Yönetici oluştur", (ErrPasswordsDiffer, En) => "The passwords do not match.", (ErrPasswordsDiffer, Tr) => "Parolalar eşleşmiyor.", (ErrWrongPassword, En) => "Current password is incorrect.", (ErrWrongPassword, Tr) => "Mevcut parola yanlış.", (ErrWrongConfirmCode, En) => { "That code is not right. Check the time on your device and try again." } (ErrWrongConfirmCode, Tr) => { "Bu kod doğru değil. Cihazınızdaki saati kontrol edip tekrar deneyin." } (ErrWrongCode, En) => { "That code is not right. Use a code from your authenticator app or a recovery code." } (ErrWrongCode, Tr) => { "Bu kod doğru değil. Doğrulama uygulamanızdaki bir kodu ya da bir kurtarma kodunu kullanın." } (OkUsernameUpdated, En) => "Username updated.", (OkUsernameUpdated, Tr) => "Kullanıcı adı güncellendi.", (OkAvatarUpdated, En) => "Avatar updated.", (OkAvatarUpdated, Tr) => "Avatar güncellendi.", (OkAvatarRemoved, En) => "Avatar removed.", (OkAvatarRemoved, Tr) => "Avatar kaldırıldı.", (OkProfileSaved, En) => "Profile saved.", (OkProfileSaved, Tr) => "Profil kaydedildi.", (CurrentUsername, En) => "Current username: ", (CurrentUsername, Tr) => "Mevcut kullanıcı adı: ", (UsernameHint, En) => { "Only the capitalization can be changed. Your lookup name stays the same." } (UsernameHint, Tr) => { "Yalnızca büyük/küçük harf değiştirilebilir. Giriş adınız aynı kalır." } (Avatar, En) => "Avatar", (Avatar, Tr) => "Avatar", (AvatarAlt, En) => "Your avatar", (AvatarAlt, Tr) => "Avatarınız", (Image, En) => "Image", (Image, Tr) => "Görsel", (AvatarHint, En) => { "PNG, JPEG, WebP or GIF, up to 5 MiB. Images are cropped to a square and resized to 256 by 256 pixels." } (AvatarHint, Tr) => { "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." } (RemoveAvatar, En) => "Remove avatar", (RemoveAvatar, Tr) => "Avatarı kaldır", (AboutAndTimezone, En) => "About and timezone", (AboutAndTimezone, Tr) => "Hakkında ve saat dilimi", (About, En) => "About", (About, Tr) => "Hakkında", (AboutHint, En) => "At most 2000 characters. Markdown is supported.", (AboutHint, Tr) => "En fazla 2000 karakter. Markdown desteklenir.", (Timezone, En) => "Timezone", (Timezone, Tr) => "Saat dilimi", (ErrChooseImage, En) => "Choose an image to upload.", (ErrChooseImage, Tr) => "Yüklemek için bir görsel seçin.", (OkPasswordChanged, En) => "Password changed. Your other sessions were signed out.", (OkPasswordChanged, Tr) => "Parola değiştirildi. Diğer oturumlarınız kapatıldı.", (OkEmailSaved, En) => "Email saved.", (OkEmailSaved, Tr) => "E-posta kaydedildi.", (OkTwoFactorOn, En) => "Two-factor authentication is on.", (OkTwoFactorOn, Tr) => "İki faktörlü kimlik doğrulama açık.", (OkTwoFactorOff, En) => "Two-factor authentication is off.", (OkTwoFactorOff, Tr) => "İki faktörlü kimlik doğrulama kapalı.", (OkRecoveryCodesNew, En) => "New recovery codes were created. The old ones no longer work.", (OkRecoveryCodesNew, Tr) => "Yeni kurtarma kodları oluşturuldu. Eskileri artık çalışmaz.", (SecurityLog, En) => "Security log", (SecurityLog, Tr) => "Güvenlik günlüğü", (SecurityLogRest, En) => " lists your sign-ins and changes to your account.", (SecurityLogRest, Tr) => " girişlerini ve hesabınızdaki değişiklikleri listeler.", (RecoveryCodes, En) => "Recovery codes", (RecoveryCodes, Tr) => "Kurtarma kodları", (RecoveryCodesSave, En) => { "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." } (RecoveryCodesSave, Tr) => { "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." } (CurrentPassword, En) => "Current password", (CurrentPassword, Tr) => "Mevcut parola", (NewPassword, En) => "New password", (NewPassword, Tr) => "Yeni parola", (NewPasswordHint, En) => "At least 10 characters. Your other sessions will be signed out.", (NewPasswordHint, Tr) => "En az 10 karakter. Diğer oturumlarınız kapatılır.", (ConfirmNewPassword, En) => "Confirm new password", (ConfirmNewPassword, Tr) => "Yeni parolayı doğrula", (ChangePassword, En) => "Change password", (ChangePassword, Tr) => "Parolayı değiştir", (SaveEmail, En) => "Save email", (SaveEmail, Tr) => "E-postayı kaydet", (TwoFactorAuth, En) => "Two-factor authentication", (TwoFactorAuth, Tr) => "İki faktörlü kimlik doğrulama", (RegenerateCodes, En) => "Regenerate recovery codes", (RegenerateCodes, Tr) => "Kurtarma kodlarını yenile", (RegenerateCodesHint, En) => { "Creating new codes makes the old ones stop working. Enter a code from your authenticator app to continue." } (RegenerateCodesHint, Tr) => { "Yeni kodlar oluşturmak eskilerini geçersiz kılar. Devam etmek için doğrulama uygulamanızdaki bir kodu girin." } (TurnOffTwoFactor, En) => "Turn off two-factor authentication", (TurnOffTwoFactor, Tr) => "İki faktörlü kimlik doğrulamayı kapat", (TurnOffTwoFactorButton, En) => "Turn off two-factor", (TurnOffTwoFactorButton, Tr) => "İki faktörlüyü kapat", (TwoFactorPendingIntro, En) => { "Add this account to your authenticator app. Enter the secret, or open the link on a device that has the app." } (TwoFactorPendingIntro, Tr) => { "Bu hesabı doğrulama uygulamanıza ekleyin. Anahtarı girin ya da uygulamanın yüklü olduğu bir cihazda bağlantıyı açın." } (SecretPrefix, En) => "Secret: ", (SecretPrefix, Tr) => "Anahtar: ", (LinkPrefix, En) => "Link: ", (LinkPrefix, Tr) => "Bağlantı: ", (CodeFromApp, En) => "Code from the app", (CodeFromApp, Tr) => "Uygulamadaki kod", (ConfirmAndTurnOn, En) => "Confirm and turn on", (ConfirmAndTurnOn, Tr) => "Doğrula ve aç", (TwoFactorOffIntro, En) => { "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." } (TwoFactorOffIntro, Tr) => { "İ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." } (Enable2fa, En) => "Enable 2FA", (Enable2fa, Tr) => "2FA'yı aç", (DeleteAccount, En) => "Delete account", (DeleteAccount, Tr) => "Hesabı sil", (DeleteAccountIntro, En) => { "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." } (DeleteAccountIntro, Tr) => { "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." } (TypeUsernameToConfirm, En) => "Type your username to confirm", (TypeUsernameToConfirm, Tr) => "Onaylamak için kullanıcı adınızı yazın", (TransferOrDeleteRepos, En) => { "Transfer or delete these repositories before deleting your account:" } (TransferOrDeleteRepos, Tr) => "Hesabınızı silmeden önce bu depoları devredin ya da silin:", (ErrConfirmUsername, En) => "Type your username exactly to confirm.", (ErrConfirmUsername, Tr) => "Onaylamak için kullanıcı adınızı birebir yazın.", (NoSshKeys, En) => "No SSH keys yet.", (NoSshKeys, Tr) => "Henüz SSH anahtarı yok.", (NoGpgKeys, En) => "No GPG keys yet.", (NoGpgKeys, Tr) => "Henüz GPG anahtarı yok.", (AddSshKeyTitle, En) => "Add an SSH key", (AddSshKeyTitle, Tr) => "SSH anahtarı ekle", (AddGpgKeyTitle, En) => "Add a GPG key", (AddGpgKeyTitle, Tr) => "GPG anahtarı ekle", (TitleOptional, En) => "Title (optional)", (TitleOptional, Tr) => "Başlık (isteğe bağlı)", (PublicKeyLabel, En) => "Public key", (PublicKeyLabel, Tr) => "Ortak anahtar", (PublicKeyHint, En) => "Paste the contents of id_ed25519.pub. RSA keys are not accepted.", (PublicKeyHint, Tr) => { "id_ed25519.pub dosyasının içeriğini yapıştırın. RSA anahtarları kabul edilmez." } (AddKeyButton, En) => "Add key", (AddKeyButton, Tr) => "Anahtar ekle", (ArmoredPublicKeyLabel, En) => "Public key (ASCII armored)", (ArmoredPublicKeyLabel, Tr) => "Ortak anahtar (ASCII zırhlı)", (ArmoredHint, En) => "Paste the output of gpg --armor --export with your key ID.", (ArmoredHint, Tr) => { "Anahtar kimliğinizle birlikte gpg --armor --export çıktısını yapıştırın." } (GpgUnavailable, En) => "GPG verification is unavailable on this server", (GpgUnavailable, Tr) => "GPG doğrulaması bu sunucuda kullanılamıyor", (YourNewToken, En) => "Your new token", (YourNewToken, Tr) => "Yeni belirteciniz", (CopyNow, En) => "Copy it now. It is shown only once and cannot be recovered later.", (CopyNow, Tr) => "Şimdi kopyalayın. Yalnızca bir kez gösterilir ve sonradan geri alınamaz.", (UseAsPassword, En) => { "Use it as the password when a git client asks for one, with any username." } (UseAsPassword, Tr) => { "Bir Git istemcisi parola istediğinde, herhangi bir kullanıcı adıyla birlikte bunu parola olarak kullanın." } (NoAccessTokens, En) => "No access tokens yet.", (NoAccessTokens, Tr) => "Henüz erişim belirteci yok.", (NewToken, En) => "New token", (NewToken, Tr) => "Yeni belirteç", (ExpiresIn30, En) => "In 30 days", (ExpiresIn30, Tr) => "30 gün sonra", (ExpiresIn90, En) => "In 90 days", (ExpiresIn90, Tr) => "90 gün sonra", (ExpiresIn365, En) => "In 365 days", (ExpiresIn365, Tr) => "365 gün sonra", (ExpiresNever, En) => "Never", (ExpiresNever, Tr) => "Hiçbir zaman", (ScopeNote, En) => "Scope: repo (read and write repositories you can access).", (ScopeNote, Tr) => "Kapsam: repo (erişebildiğiniz depoları okuyabilir ve yazabilirsiniz).", (CreateToken, En) => "Create token", (CreateToken, Tr) => "Belirteç oluştur", (UnknownBrowser, En) => "Unknown browser", (UnknownBrowser, Tr) => "Bilinmeyen tarayıcı", (ThisSession, En) => "This session", (ThisSession, Tr) => "Bu oturum", (RevokeAllOthers, En) => "Revoke all other sessions", (RevokeAllOthers, Tr) => "Diğer tüm oturumları iptal et", (Added, En) => "Added", (Added, Tr) => "eklendi", (LastUsed, En) => "last used", (LastUsed, Tr) => "son kullanım", (Created, En) => "Created", (Created, Tr) => "Oluşturuldu", (SignedIn, En) => "Signed in", (SignedIn, Tr) => "Giriş", (LastSeen, En) => "last seen", (LastSeen, Tr) => "son görülme", (ExpiresLabel, En) => "expires", (ExpiresLabel, Tr) => "bitiş", (ErrKeyEmpty, En) => "paste an OpenSSH public key line", (ErrKeyEmpty, Tr) => "Bir OpenSSH ortak anahtar satırı yapıştırın.", (ErrKeyTooLong, En) => "key is too long", (ErrKeyTooLong, Tr) => "Anahtar çok uzun.", (ErrKeyMissingBody, En) => "key is missing its base64 body", (ErrKeyMissingBody, Tr) => "Anahtarın base64 gövdesi eksik.", (ErrKeyRsa, En) => { "RSA keys are not accepted; please use an ed25519 key (ssh-keygen -t ed25519)" } (ErrKeyRsa, Tr) => { "RSA anahtarları kabul edilmez; lütfen bir ed25519 anahtarı kullanın (ssh-keygen -t ed25519)." } (ErrKeyUnsupportedType, En) => "unsupported key type", (ErrKeyUnsupportedType, Tr) => "Desteklenmeyen anahtar türü", (ErrKeyBadBase64, En) => "key body is not valid base64", (ErrKeyBadBase64, Tr) => "Anahtar gövdesi geçerli bir base64 değil.", (ErrKeyTruncated, En) => "key body is truncated", (ErrKeyTruncated, Tr) => "Anahtar gövdesi eksik.", (ErrKeyMismatch, En) => "key body does not match the key type", (ErrKeyMismatch, Tr) => "Anahtar gövdesi anahtar türüyle eşleşmiyor.", (ForgotTitle, En) => "Forgot password", (ForgotTitle, Tr) => "Parolamı unuttum", (ForgotSub, En) => { "Enter the address on your account. If it is a verified address, we send a reset link." } (ForgotSub, Tr) => { "Hesabınızdaki e-posta adresini girin. Doğrulanmış bir adres ise sıfırlama bağlantısı gönderilir." } (ForgotSubmit, En) => "Send reset link", (ForgotSubmit, Tr) => "Sıfırlama bağlantısı gönder", (ForgotSent, En) => { "If that address belongs to a verified account, a reset link is on its way. Check your inbox." } (ForgotSent, Tr) => { "Bu adres doğrulanmış bir hesaba aitse sıfırlama bağlantısı yolda. Gelen kutunuzu kontrol edin." } (ResetTitle, En) => "Set a new password", (ResetTitle, Tr) => "Yeni parola belirle", (ResetSub, En) => "The link works once. Choose a new password for your account.", (ResetSub, Tr) => { "Bu bağlantı yalnızca bir kez kullanılabilir. Hesabınız için yeni bir parola seçin." } (ResetSubmit, En) => "Save new password", (ResetSubmit, Tr) => "Yeni parolayı kaydet", (ResetInvalid, En) => "This reset link is invalid, used or expired. Request a new one.", (ResetInvalid, Tr) => { "Bu sıfırlama bağlantısı geçersiz, kullanılmış ya da süresi dolmuş. Yeni bir tane isteyin." } (ResetDone, En) => { "Your password is changed. All other sessions were signed out. Log in with the new password." } (ResetDone, Tr) => { "Parolanız değiştirildi. Diğer tüm oturumlar kapatıldı. Yeni parolanızla giriş yapın." } (RegisterTitle, En) => "Create an account", (RegisterTitle, Tr) => "Hesap oluştur", (RegisterSub, En) => { "Pick a username and a password. We send a link to confirm your address." } (RegisterSub, Tr) => { "Bir kullanıcı adı ve parola seçin. Adresinizi doğrulamak için bir bağlantı göndereceğiz." } (RegisterInviteSub, En) => { "You were invited. This account uses the address the invite was sent to." } (RegisterInviteSub, Tr) => { "Davet edildiniz. Bu hesap, davetin gönderildiği adresi kullanır." } (RegisterNeedsInvite, En) => { "Registration is by invitation only. Ask an admin for an invite link." } (RegisterNeedsInvite, Tr) => { "Kayıt yalnızca davetle yapılır. Bir yöneticiden davet bağlantısı isteyin." } (RegisterInvalidInvite, En) => "This invite is invalid, already used or expired.", (RegisterInvalidInvite, Tr) => { "Bu davet geçersiz, daha önce kullanılmış ya da süresi dolmuş." } (RegisterSubmit, En) => "Create account", (RegisterSubmit, Tr) => "Hesap oluştur", (VerifyTitle, En) => "Confirm your email", (VerifyTitle, Tr) => "E-posta adresini doğrula", (VerifyConfirmSub, En) => "Confirm that this address is yours.", (VerifyConfirmSub, Tr) => "Bu adresin size ait olduğunu doğrulayın.", (VerifyConfirm, En) => "Confirm address", (VerifyConfirm, Tr) => "Adresi doğrula", (VerifyDone, En) => "Your email address is verified.", (VerifyDone, Tr) => "E-posta adresin doğrulandı.", (VerifyInvalid, En) => "This verification link is invalid, used or expired.", (VerifyInvalid, Tr) => "Bu doğrulama bağlantısı geçersiz, kullanılmış ya da süresi dolmuş.", (VerifyVerified, En) => "Verified", (VerifyVerified, Tr) => "Doğrulandı", (VerifyUnverified, En) => { "Not verified. Password reset mail is only sent to verified addresses." } (VerifyUnverified, Tr) => { "Doğrulanmadı. Parola sıfırlama e-postaları yalnızca doğrulanmış adreslere gönderilir." } (VerifyResend, En) => "Send the verification link again", (VerifyResend, Tr) => "Doğrulama bağlantısını yeniden gönder", (OkVerifySent, En) => "We sent a verification link to your address.", (OkVerifySent, Tr) => "Adresinize bir doğrulama bağlantısı gönderildi.", (InvitesTitle, En) => "Invite someone", (InvitesTitle, Tr) => "Birini davet et", (InvitesSub, En) => { "We email an invite link. It works once, for that address only, and expires in 7 days." } (InvitesSub, Tr) => { "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." } (InvitesSubmit, En) => "Send invite", (InvitesSubmit, Tr) => "Davet gönder", (InvitesSent, En) => "The invite was sent.", (InvitesSent, Tr) => "Davet gönderildi.", (InvitesOff, En) => { "Invites need registration set to invite or open. Change registration in the config first." } (InvitesOff, Tr) => { "Davet göndermek için kayıt ayarı invite ya da open olmalı. Önce yapılandırmada kayıt ayarını değiştirin." } (MailVerbOpened, En) => "opened this", (MailVerbCommented, En) => "commented on this", (MailVerbClosed, En) => "closed this", (MailVerbReopened, En) => "reopened this", (MailVerbMerged, En) => "merged this", (MailVerbReviewed, En) => "reviewed this", (MailVerbAssigned, En) => "assigned you to this", (MailVerbPushed, En) => "pushed new commits", (MailVerbMentioned, En) => "mentioned you", (MailSomeone, En) => "Someone", (MailReasonMention, En) => "you were mentioned", (MailReasonAssign, En) => "you were assigned", (MailReasonReviewRequested, En) => "your review was requested", (MailReasonAuthor, En) => "you opened it", (MailReasonComment, En) => "you commented on it", (MailReasonSubscribed, En) => "you are subscribed to it", (MailReasonWatching, En) => "you watch this repository", (MenuNotificationEmails, En) => "Notification emails", (EmailNotifyLabel, En) => "Email me about notifications", (EmailNotifyHelp, En) => { "Each notification is sent to your verified address. Notifications on the site are not affected." } (EmailNotifyUnverified, En) => { "Verify your email address first. Only verified addresses get notification emails." } (EmailNotifyServerOff, En) => { "Email is turned off on this server, so no notification emails are sent." } (EmailNotifySaved, En) => "Notification settings saved.", (MailVerbOpened, Tr) => "bunu açtı", (MailVerbCommented, Tr) => "yorum yaptı", (MailVerbClosed, Tr) => "bunu kapattı", (MailVerbReopened, Tr) => "bunu yeniden açtı", (MailVerbMerged, Tr) => "bunu birleştirdi", (MailVerbReviewed, Tr) => "bunu inceledi", (MailVerbAssigned, Tr) => "sizi bunun sorumlusu olarak atadı", (MailVerbPushed, Tr) => "yeni commit'ler gönderdi", (MailVerbMentioned, Tr) => "sizden bahsetti", (MailSomeone, Tr) => "Birisi", (MailReasonMention, Tr) => "sizden bahsedildi", (MailReasonAssign, Tr) => "size atandı", (MailReasonReviewRequested, Tr) => "incelemeniz istendi", (MailReasonAuthor, Tr) => "bunu siz açtınız", (MailReasonComment, Tr) => "bunda yorum yaptınız", (MailReasonSubscribed, Tr) => "bunu takip ediyorsunuz", (MailReasonWatching, Tr) => "bu depoyu izliyorsunuz", (MenuNotificationEmails, Tr) => "Bildirim e-postaları", (EmailNotifyLabel, Tr) => "Bildirimler hakkında bana e-posta gönder", (EmailNotifyHelp, Tr) => { "Her bildirim doğrulanmış adresinize gönderilir. Sitedeki bildirimler bundan etkilenmez." } (EmailNotifyUnverified, Tr) => { "Önce e-posta adresinizi doğrulayın. Bildirim e-postaları yalnızca doğrulanmış adreslere gönderilir." } (EmailNotifyServerOff, Tr) => { "Bu sunucuda e-posta kapalı, bu yüzden bildirim e-postası gönderilmiyor." } (EmailNotifySaved, Tr) => "Bildirim ayarları kaydedildi.", } } /// A unit of elapsed time for "3 days ago". #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TimeUnit { Minute, Hour, Day, Month, Year, } /// "just now", or "az önce". pub fn just_now(lang: Lang) -> &'static str { match lang { Lang::En => "just now", Lang::Tr => "az önce", } } /// "3 days ago", or "3 gün önce". Turkish nouns stay singular after a number. pub fn ago(lang: Lang, n: i64, unit: TimeUnit) -> String { match lang { Lang::En => { let word = match unit { TimeUnit::Minute => "minute", TimeUnit::Hour => "hour", TimeUnit::Day => "day", TimeUnit::Month => "month", TimeUnit::Year => "year", }; let plural = if n == 1 { "" } else { "s" }; format!("{n} {word}{plural} ago") } Lang::Tr => { let word = match unit { TimeUnit::Minute => "dakika", TimeUnit::Hour => "saat", TimeUnit::Day => "gün", TimeUnit::Month => "ay", TimeUnit::Year => "yıl", }; format!("{n} {word} önce") } } } /// The name shown for the ghost account that owns the content of deleted users. pub fn deleted_user(lang: Lang) -> &'static str { match lang { Lang::En => "Deleted user", Lang::Tr => "Silinmiş kullanıcı", } } /// The visibility badge text: "public" or "herkese açık", "private" or "özel". pub fn visibility_label(lang: Lang, visibility: forge_core::Visibility) -> &'static str { match (lang, visibility) { (Lang::En, forge_core::Visibility::Public) => "public", (Lang::En, forge_core::Visibility::Private) => "private", (Lang::Tr, forge_core::Visibility::Public) => "herkese açık", (Lang::Tr, forge_core::Visibility::Private) => "özel", } } /// "Added 2026-01-02", with the last use appended when there is one. pub fn added_line(lang: Lang, created: &str, last_used: Option<&str>) -> String { let added = match lang { Lang::En => format!("{} {created}", t(lang, Key::Added)), Lang::Tr => format!("{created} tarihinde {}", t(lang, Key::Added)), }; match last_used { None => added, Some(used) => match lang { Lang::En => format!("{added} · {} {used}", t(lang, Key::LastUsed)), Lang::Tr => format!("{added} · {}: {used}", t(lang, Key::LastUsed)), }, } } /// A token's dates: "Created X · last used Y · expires Z". A missing use or expiry reads "never". pub fn token_meta( lang: Lang, created: &str, last_used: Option<&str>, expires: Option<&str>, ) -> String { let used = last_used.unwrap_or(t(lang, Key::Never)); let expiry = expires.unwrap_or(t(lang, Key::Never)); dated_line( lang, (Key::Created, created), (Key::LastUsed, used), (Key::ExpiresLabel, expiry), ) } /// A web session's dates: "Signed in X · last seen Y · expires Z". pub fn session_meta(lang: Lang, signed_in: &str, last_seen: &str, expires: &str) -> String { dated_line( lang, (Key::SignedIn, signed_in), (Key::LastSeen, last_seen), (Key::ExpiresLabel, expires), ) } /// Three labelled dates joined with " · ". English reads "Label value"; Turkish reads "Etiket: value". fn dated_line(lang: Lang, a: (Key, &str), b: (Key, &str), c: (Key, &str)) -> String { match lang { Lang::En => format!( "{} {} · {} {} · {} {}", t(lang, a.0), a.1, t(lang, b.0), b.1, t(lang, c.0), c.1 ), Lang::Tr => format!( "{}: {} · {}: {} · {}: {}", t(lang, a.0), a.1, t(lang, b.0), b.1, t(lang, c.0), c.1 ), } } /// "Unsupported key type \"dsa\"" in the viewer's language. pub fn unsupported_key_type(lang: Lang, algorithm: &str) -> String { match lang { Lang::En => format!("{} {algorithm:?}", t(lang, Key::ErrKeyUnsupportedType)), Lang::Tr => format!("{}: {algorithm:?}", t(lang, Key::ErrKeyUnsupportedType)), } } /// "Two-factor authentication is on. Unused recovery codes: 7." pub fn two_factor_on_line(lang: Lang, unused: u64) -> String { match lang { Lang::En => format!("Two-factor authentication is on. Unused recovery codes: {unused}."), Lang::Tr => format!( "İki faktörlü kimlik doğrulama açık. Kullanılmamış kurtarma kodu sayısı: {unused}." ), } } /// The message for an "about" text longer than `max` characters. pub fn about_too_long(lang: Lang, max: usize) -> String { match lang { Lang::En => format!("About must be at most {max} characters."), Lang::Tr => format!("Hakkında en fazla {max} karakter olabilir."), } } /// The message for a password shorter than `min` characters. pub fn password_too_short(lang: Lang, min: usize) -> String { match lang { Lang::En => format!("Password must be at least {min} characters."), Lang::Tr => format!("Parola en az {min} karakter olmalı."), } } /// The message for a password longer than `max` characters. pub fn password_too_long(lang: Lang, max: usize) -> String { match lang { Lang::En => format!("Password must be at most {max} characters."), Lang::Tr => format!("Parola en fazla {max} karakter olabilir."), } } /// The message for a token name outside 1 to `max` characters. pub fn token_name_length(lang: Lang, max: usize) -> String { match lang { Lang::En => format!("Name must be 1 to {max} characters."), Lang::Tr => format!("Ad 1 ile {max} karakter arasında olmalı."), } } /// The last line of a notification email: why it was sent, and where to change that. pub fn mail_footer(lang: Lang, reason: &str, settings_url: &str) -> String { match lang { Lang::En => format!( "You are receiving this because {reason}. Manage email notifications: {settings_url}" ), Lang::Tr => format!( "Bu e-postayı şu nedenle alıyorsunuz: {reason}. E-posta bildirimlerini yönetin: {settings_url}" ), } } #[cfg(test)] mod tests { use super::*; use http::HeaderValue; fn headers(value: &'static str) -> HeaderMap { let mut map = HeaderMap::new(); map.insert(ACCEPT_LANGUAGE, HeaderValue::from_static(value)); map } #[test] fn every_key_has_text_in_both_languages() { for key in ALL { assert!(!t(Lang::En, *key).is_empty(), "{key:?} has no English text"); assert!(!t(Lang::Tr, *key).is_empty(), "{key:?} has no Turkish text"); } } #[test] fn all_lists_each_key_once() { for (i, key) in ALL.iter().enumerate() { assert!(!ALL[i + 1..].contains(key), "{key:?} is listed twice"); } } #[test] fn stored_language_wins_over_the_browser() { let turkish = headers("tr"); assert_eq!(resolve(Some("en"), &turkish), Lang::En); assert_eq!(resolve(Some("tr"), &HeaderMap::new()), Lang::Tr); } #[test] fn auto_and_missing_preferences_follow_the_browser() { assert_eq!(resolve(Some("auto"), &headers("tr-TR,en;q=0.5")), Lang::Tr); assert_eq!(resolve(None, &headers("tr")), Lang::Tr); assert_eq!(resolve(None, &HeaderMap::new()), Lang::En); } #[test] fn accept_language_picks_the_highest_quality_supported_tag() { assert_eq!(resolve(None, &headers("de,en;q=0.8,tr;q=0.9")), Lang::Tr); assert_eq!(resolve(None, &headers("en;q=0.9,tr;q=0.5")), Lang::En); assert_eq!(resolve(None, &headers("tr;q=0,en")), Lang::En); assert_eq!(resolve(None, &headers("de,fr;q=0.9")), Lang::En); assert_eq!(resolve(None, &headers("tr;q=oops,en;q=0.1")), Lang::En); } #[test] fn unknown_stored_code_is_english() { assert_eq!(Lang::from_code("fr"), Lang::En); assert_eq!(Lang::from_code("tr"), Lang::Tr); } #[test] fn html_codes_match_the_languages() { assert_eq!(Lang::En.code(), "en"); assert_eq!(Lang::Tr.code(), "tr"); } }