Ferrit Explore
中文·繁體·EN·日本語 Sign in Register
cielxl / ferrit / src / i18n.rs
//! 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 に失敗しました:",
        },
    }
}