在實作多國語言的Web服務時,會需要去讀取客戶端傳送來的HTTP請求標頭中的Accept-Language欄位,來判斷客戶端偏好使用的語言。然而,Rocket框架並未內建解析Accept-Language欄位的功能,開發者只能自行去撰寫程式來想辦法用Accept-Language欄位的原始資料去做客戶端的語言判斷,不但麻煩又很不安全。



一個Accept-Language欄位的值可能如以下這樣:

zh-TW,zh;q=0.7,en-US;q=0.6,en;q=0.5

以上的值,代表客戶端想要優先使用台灣的繁體中文,如果沒有的話就使用任意地區的中文,如果還是沒有的話就使用美國的英文,如果又沒有的話就使用任意地區的英文。

accept-language Request Guard for Rocket Framework

「accept-language Request Guard for Rocket Framework」是筆者開發出來的套件,將可以讀取Accept-Language欄位並且進行語言判斷的功能寫成請求守衛,一勞永逸。使用accept-language這個crate來進行Accept-Language欄位值的解析,並轉成unic-langid這個crate提供的LanguageIdentifier結構體的實體來儲存。

Crates.io

https://crates.io/crates/rocket-accept-language

Cargo.toml

rocket-accept-language = "*"

使用方法

在Rocket框架啟動前,先建立出LanguageIdentifier結構實體的陣列,用來表示這個Rocket應用程式支援的語言。接著再把這個LanguageIdentifier陣列,註冊成Rocket的應用程式狀態。language_region_pairs巨集能以很小的開支(overhead)來建立Vec<LanguageIdentifier>結構實體。

在Rocket框架的路由處理程序中,藉由放置rocket_accept_language這個crate所提供的AcceptLanguage請求守衛,來解析HTTP請求標頭中的Accept-Language欄位。AcceptLanguage的結構實體提供了get_appropriate_language_region方法,可以傳入一個LanguageIdentifier陣列切片,根據Rocket應用程式有支援的語言來判斷出最符合HTTP請求的語言。

get_appropriate_language_region方法會回傳unic-langid這個crate提供的提供的LanguageRegion結構體的實體。透過language巨集和region巨集,可以用常數建立出LanguageRegion結構實體並儲存起來,方便重用。

以下是一個使用範例:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

#[macro_use]
extern crate rocket_accept_language;

use rocket::State;

use rocket_accept_language::unic_langid::subtags::{Language, Region};
use rocket_accept_language::{AcceptLanguage, LanguageIdentifier};

const LANGUAGE_ZH: Language = language!("zh");
const LANGUAGE_EN: Language = language!("en");
const LANGUAGE_JP: Language = language!("jp");
const REGION_CN: Region = region!("cn");
const REGION_TW: Region = region!("tw");

struct SupportLanguages {
    language_identifiers: Vec<LanguageIdentifier>,
}

impl SupportLanguages {
    fn as_language_identifiers(&self) -> &[LanguageIdentifier] {
        &self.language_identifiers
    }
}

#[get("/")]
fn hello(
    accept_language: &AcceptLanguage,
    support_languages: State<SupportLanguages>,
) -> &'static str {
    let (language, region) = accept_language
        .get_appropriate_language_region(support_languages.as_language_identifiers())
        .unwrap_or((LANGUAGE_EN, None));

    match language {
        LANGUAGE_ZH => {
            match region.unwrap_or(REGION_TW) {
                REGION_CN => "哈罗!",
                _ => "哈囉!",
            }
        }
        LANGUAGE_JP => "ハロー",
        _ => "Hello!",
    }
}

fn main() {
    let support_languages = SupportLanguages {
        language_identifiers: language_region_pairs!["zh-TW", "zh_CN", "jp", "en"],
    };

    rocket::ignite().manage(support_languages).mount("/", routes![hello]).launch();
}