//! Internationalization: language selection and UI string translation.
//!
//! Ferrit renders its UI server-side, so every user-facing string is looked up
//! here through [`t`] for the visitor's selected [`Lang`]. The language is chosen
//! per request (cookie first, then the `Accept-Language` header, then English)
//! and can be switched via the `/setlang/:code` route, which stores a cookie.
use axum::http::HeaderMap;
/// A supported UI language.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Lang {
/// English.
En,
/// Simplified Chinese (简体中文).
ZhCn,
/// Traditional Chinese (繁體中文).
ZhTw,
/// Japanese (日本語).
Ja,
}
impl Lang {
/// All languages, in the order shown in the switcher.
pub const ALL: [Lang; 4] = [Lang::ZhCn, Lang::ZhTw, Lang::En, Lang::Ja];
/// The canonical cookie/URL code for this language.
pub fn code(self) -> &'static str {
match self {
Lang::En => "en",
Lang::ZhCn => "zh-CN",
Lang::ZhTw => "zh-TW",
Lang::Ja => "ja",
}
}
/// The value for the `<html lang="…">` attribute.
pub fn html_lang(self) -> &'static str {
self.code()
}
/// The label shown in the language switcher.
pub fn label(self) -> &'static str {
match self {
Lang::En => "EN",
Lang::ZhCn => "中文",
Lang::ZhTw => "繁體",
Lang::Ja => "日本語",
}
}
/// Parse a language code (case-insensitive, accepting common variants).
pub fn from_code(s: &str) -> Lang {
let s = s.to_ascii_lowercase();
if s.starts_with("zh") {
if s.contains("tw") || s.contains("hk") || s.contains("hant") || s.contains("mo") {
Lang::ZhTw
} else {
Lang::ZhCn
}
} else if s.starts_with("ja") {
Lang::Ja
} else {
Lang::En
}
}
}
/// Determine the language for a request: cookie, then `Accept-Language`, then English.
pub fn detect(headers: &HeaderMap) -> Lang {
if let Some(raw) = headers.get("cookie").and_then(|v| v.to_str().ok()) {
for part in raw.split(';') {
let part = part.trim();
if let Some(v) = part.strip_prefix("ferrit_lang=") {
return Lang::from_code(v);
}
}
}
if let Some(al) = headers.get("accept-language").and_then(|v| v.to_str().ok()) {
// Take the first language tag (ignoring q-weights, good enough here).
if let Some(first) = al.split(',').next() {
let tag = first.split(';').next().unwrap_or("").trim();
if !tag.is_empty() {
return Lang::from_code(tag);
}
}
}
Lang::En
}
/// A translatable UI message key.
#[derive(Clone, Copy)]
pub enum Msg {
Tagline,
NavNew,
NavLogout,
NavSignIn,
NavRegister,
NavExplore,
Dashboard,
YourRepos,
NewRepoBtn,
ExploreRepos,
AnonDesc,
GetStarted,
RegisteredUsers,
NoRepos,
Private,
Created,
Username,
Password,
Email,
Optional,
InvalidLogin,
CreateNewRepo,
RepoName,
Description,
PrivateRepo,
CreateRepo,
NewRepository,
Joined,
Repositories,
Clone,
QuickSetup,
EmptyRepoHint,
Branch,
CommitsWord,
Latest,
EmptyTree,
BinaryPrefix,
BinarySuffix,
CommitsTitle,
NoCommits,
NotFoundTitle,
NotFoundBody,
Error,
GitInitFailed,
}
/// Translate `msg` into `lang`.
pub fn t(lang: Lang, msg: Msg) -> &'static str {
use Lang::*;
use Msg::*;
match msg {
Tagline => match lang {
En => "self-hosted Git, in Rust",
ZhCn => "用 Rust 编写的自托管 Git 服务",
ZhTw => "以 Rust 編寫的自架 Git 服務",
Ja => "Rust 製のセルフホスト Git サービス",
},
NavNew => match lang {
En => "+ New",
ZhCn => "+ 新建",
ZhTw => "+ 新增",
Ja => "+ 新規",
},
NavLogout => match lang {
En => "Logout",
ZhCn => "登出",
ZhTw => "登出",
Ja => "ログアウト",
},
NavSignIn => match lang {
En => "Sign in",
ZhCn => "登录",
ZhTw => "登入",
Ja => "ログイン",
},
NavRegister => match lang {
En => "Register",
ZhCn => "注册",
ZhTw => "註冊",
Ja => "登録",
},
NavExplore => match lang {
En => "Explore",
ZhCn => "探索",
ZhTw => "探索",
Ja => "探索",
},
Dashboard => match lang {
En => "Dashboard",
ZhCn => "仪表盘",
ZhTw => "儀表板",
Ja => "ダッシュボード",
},
YourRepos => match lang {
En => "Your repositories",
ZhCn => "你的仓库",
ZhTw => "你的儲存庫",
Ja => "あなたのリポジトリ",
},
NewRepoBtn => match lang {
En => "+ New repository",
ZhCn => "+ 新建仓库",
ZhTw => "+ 新增儲存庫",
Ja => "+ 新規リポジトリ",
},
ExploreRepos => match lang {
En => "Explore repositories",
ZhCn => "探索仓库",
ZhTw => "探索儲存庫",
Ja => "リポジトリを探索",
},
AnonDesc => match lang {
En => "A minimal, independent reimplementation of a git forge: accounts, repositories, a code browser and full clone/push over HTTP.",
ZhCn => "一个极简、独立实现的 Git 代码托管平台:账户、仓库、代码浏览器,以及完整的 HTTP clone/push 支持。",
ZhTw => "一個極簡、獨立實作的 Git 程式碼託管平台:帳戶、儲存庫、程式碼瀏覽器,以及完整的 HTTP clone/push 支援。",
Ja => "アカウント、リポジトリ、コードブラウザ、HTTP 経由の clone/push に対応した、ミニマルで独立実装の Git フォージです。",
},
GetStarted => match lang {
En => "Get started",
ZhCn => "开始使用",
ZhTw => "開始使用",
Ja => "はじめる",
},
RegisteredUsers => match lang {
En => "Registered users:",
ZhCn => "已注册用户:",
ZhTw => "已註冊使用者:",
Ja => "登録ユーザー数:",
},
NoRepos => match lang {
En => "No repositories yet.",
ZhCn => "还没有仓库。",
ZhTw => "還沒有儲存庫。",
Ja => "まだリポジトリがありません。",
},
Private => match lang {
En => "private",
ZhCn => "私有",
ZhTw => "私有",
Ja => "プライベート",
},
Created => match lang {
En => "Created",
ZhCn => "创建于",
ZhTw => "建立於",
Ja => "作成",
},
Username => match lang {
En => "Username",
ZhCn => "用户名",
ZhTw => "使用者名稱",
Ja => "ユーザー名",
},
Password => match lang {
En => "Password",
ZhCn => "密码",
ZhTw => "密碼",
Ja => "パスワード",
},
Email => match lang {
En => "Email",
ZhCn => "邮箱",
ZhTw => "電子郵件",
Ja => "メールアドレス",
},
Optional => match lang {
En => "(optional)",
ZhCn => "(可选)",
ZhTw => "(選填)",
Ja => "(任意)",
},
InvalidLogin => match lang {
En => "Invalid username or password",
ZhCn => "用户名或密码错误",
ZhTw => "使用者名稱或密碼錯誤",
Ja => "ユーザー名またはパスワードが正しくありません",
},
CreateNewRepo => match lang {
En => "Create a new repository",
ZhCn => "创建新仓库",
ZhTw => "建立新儲存庫",
Ja => "新しいリポジトリを作成",
},
RepoName => match lang {
En => "Repository name",
ZhCn => "仓库名称",
ZhTw => "儲存庫名稱",
Ja => "リポジトリ名",
},
Description => match lang {
En => "Description",
ZhCn => "描述",
ZhTw => "描述",
Ja => "説明",
},
PrivateRepo => match lang {
En => "Private repository",
ZhCn => "私有仓库",
ZhTw => "私有儲存庫",
Ja => "プライベートリポジトリ",
},
CreateRepo => match lang {
En => "Create repository",
ZhCn => "创建仓库",
ZhTw => "建立儲存庫",
Ja => "リポジトリを作成",
},
NewRepository => match lang {
En => "New repository",
ZhCn => "新建仓库",
ZhTw => "新增儲存庫",
Ja => "新規リポジトリ",
},
Joined => match lang {
En => "Joined",
ZhCn => "加入于",
ZhTw => "加入於",
Ja => "登録日",
},
Repositories => match lang {
En => "Repositories",
ZhCn => "仓库",
ZhTw => "儲存庫",
Ja => "リポジトリ",
},
Clone => match lang {
En => "Clone",
ZhCn => "克隆",
ZhTw => "複製",
Ja => "クローン",
},
QuickSetup => match lang {
En => "Quick setup",
ZhCn => "快速开始",
ZhTw => "快速開始",
Ja => "クイックセットアップ",
},
EmptyRepoHint => match lang {
En => "This repository is empty. Push your first commit:",
ZhCn => "该仓库为空。推送你的第一个提交:",
ZhTw => "此儲存庫為空。推送你的第一個提交:",
Ja => "このリポジトリは空です。最初のコミットをプッシュしましょう:",
},
Branch => match lang {
En => "branch",
ZhCn => "分支",
ZhTw => "分支",
Ja => "ブランチ",
},
CommitsWord => match lang {
En => "commits",
ZhCn => "次提交",
ZhTw => "次提交",
Ja => "件のコミット",
},
Latest => match lang {
En => "Latest:",
ZhCn => "最新:",
ZhTw => "最新:",
Ja => "最新:",
},
EmptyTree => match lang {
En => "Empty tree.",
ZhCn => "空目录。",
ZhTw => "空目錄。",
Ja => "空のツリーです。",
},
BinaryPrefix => match lang {
En => "Binary file (",
ZhCn => "二进制文件(",
ZhTw => "二進位檔案(",
Ja => "バイナリファイル(",
},
BinarySuffix => match lang {
En => " bytes) — not shown.",
ZhCn => " 字节)— 不予显示。",
ZhTw => " 位元組)— 不予顯示。",
Ja => " バイト)— 表示しません。",
},
CommitsTitle => match lang {
En => "Commits",
ZhCn => "提交",
ZhTw => "提交",
Ja => "コミット",
},
NoCommits => match lang {
En => "No commits yet.",
ZhCn => "还没有提交。",
ZhTw => "還沒有提交。",
Ja => "まだコミットがありません。",
},
NotFoundTitle => match lang {
En => "Not found",
ZhCn => "未找到",
ZhTw => "找不到",
Ja => "見つかりません",
},
NotFoundBody => match lang {
En => "Nothing here.",
ZhCn => "这里什么都没有。",
ZhTw => "這裡什麼都沒有。",
Ja => "何もありません。",
},
Error => match lang {
En => "Error",
ZhCn => "错误",
ZhTw => "錯誤",
Ja => "エラー",
},
GitInitFailed => match lang {
En => "Repository metadata created but git init failed:",
ZhCn => "仓库元数据已创建,但 git init 失败:",
ZhTw => "儲存庫中繼資料已建立,但 git init 失敗:",
Ja => "リポジトリのメタデータは作成されましたが、git init に失敗しました:",
},
}
}