Go國(guó)際化:翻譯管理

本文將介紹使用golang.org/x/text包實(shí)現(xiàn)web應(yīng)用的國(guó)際化儿奶。包括以下內(nèi)容:

  • 如何使用golang.org/x/text/message和golang.org/x/text/language包翻譯Go代碼中需打印的信息。
  • 如何使用gotext工具自動(dòng)從代碼中提取需要翻譯的消息到JSON文件中。
  • 如何使用gotext工具來(lái)解析翻譯的JSON文件并創(chuàng)建一個(gè)包含翻譯信息的目錄。
  • 如何管理翻譯中的變量并提供多元化版本。

特別提醒:golang.org/x下的包是官方Go項(xiàng)目的一部分,但不在主要的Go標(biāo)準(zhǔn)庫(kù)中。它們的標(biāo)準(zhǔn)比標(biāo)準(zhǔn)庫(kù)包更寬松今魔,這意味著它們不受Go兼容性承諾的約束(即它們的api可能會(huì)改變),文檔也不一定總是完整的障贸。

創(chuàng)建應(yīng)用

為了便于理解涡贱,我們以一個(gè)簡(jiǎn)單的在線書(shū)店應(yīng)用為例。將一步一步地構(gòu)建代碼演示惹想。這里書(shū)店應(yīng)用只包含一個(gè)首頁(yè)问词,我們將基于URL路徑中包含的區(qū)域標(biāo)識(shí)符來(lái)本地化頁(yè)面內(nèi)容。應(yīng)用支持三種不同的區(qū)域:英國(guó)嘀粱、德國(guó)和中國(guó)激挪。

URL 本地化
localhost:4018/en-gb 英國(guó)
localhost:4018/de-de 德國(guó)
localhost:4018/zh-cn 中國(guó)

我們將跟隨慣例使用BCP47語(yǔ)言標(biāo)簽作為URL中的本地化標(biāo)識(shí)符辰狡。為了方便說(shuō)明BCP47語(yǔ)言標(biāo)簽一般使用{language}-{region}格式,即語(yǔ)言--區(qū)域垄分。語(yǔ)言部分是ISO639-1碼宛篇,區(qū)域是兩個(gè)字母的國(guó)家代碼來(lái)源于 ISO_3166-1。傳統(tǒng)的做法是將區(qū)域大寫(xiě)(如en-GB)薄湿,但BCP 47標(biāo)簽在技術(shù)上是不區(qū)分大小寫(xiě)的叫倍,我們可以在url中使用全小寫(xiě)版本。

搭建一個(gè)web應(yīng)用程序

如果您想跟隨應(yīng)用程序的創(chuàng)建豺瘤,請(qǐng)運(yùn)行以下命令來(lái)設(shè)置一個(gè)新的項(xiàng)目目錄吆倦。

$ mkdir bookstore
$ cd bookstore
$ go mod init bookstore.example.com
go: creating new go.mod: module bookstore.example.com

此時(shí),你的工作目錄中應(yīng)該創(chuàng)建了go.mod文件坐求,其包含模塊路徑:bookstore.example.com蚕泽。

接下來(lái)創(chuàng)建cmd/www目錄來(lái)存放書(shū)店web應(yīng)用,然后創(chuàng)建main.go和handlers.go文件桥嗤,如下所示:

$ mkdir -p cmd/www
$ touch cmd/www/main.go  cmd/www/handlers.go

項(xiàng)目目錄應(yīng)該是如下結(jié)構(gòu):

├── cmd
│   └── www
│       ├── handlers.go
│       └── main.go
└── go.mod

先從main.go文件開(kāi)始须妻,添加代碼來(lái)聲明應(yīng)用程序路由并啟動(dòng)HTTP服務(wù)器。因?yàn)閼?yīng)用程序URL路徑使用(動(dòng)態(tài)的)區(qū)域設(shè)置作為前綴——比如/en-gb/bestsellers或/zh-cn/bestsellers——如果應(yīng)用程序使用一個(gè)支持URL路徑動(dòng)態(tài)值的第三方路由泛领,是很簡(jiǎn)單的荒吏。本文使用pat(一種路由器創(chuàng)建庫(kù)),也可以使用chi或gorilla/mux之類的替換渊鞋。
下面打開(kāi)main.go文件添加以下代碼:

File: cmd/www/main.go
package main

import (
    "log"
    "net/http"

    "github.com/bmizerany/pat"
)

func main() {
    // 初始化一個(gè)路由實(shí)例司倚,并為主頁(yè)添加路徑和處理程序。
    mux := pat.New()
    mux.Get("/:locale", http.HandlerFunc(handleHome))

    // 使用路由實(shí)例啟動(dòng)HTTP服務(wù)器篓像。
    log.Println("starting server on :4018...")
    err := http.ListenAndServe(":4018", mux)
    log.Fatal(err)
}

然后在cmd/www/handlers.go文件中,添加hanldeHome()函數(shù)來(lái)提取URL中區(qū)域標(biāo)識(shí)符皿伺,并將其通過(guò)HTTP響應(yīng)返回员辩。

File: cmd/www/handlers.go
package main

import (
    "fmt"
    "net/http"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    // 從URL路徑提取語(yǔ)言區(qū)域,需根據(jù)你選擇的路由實(shí)現(xiàn)方式修改這行代碼.
    locale := r.URL.Query().Get(":locale")

    //如果所在區(qū)域語(yǔ)言有匹配的鸵鸥,在響應(yīng)中回顯區(qū)域設(shè)置奠滑。否則返回404
    switch locale {
    case "en-gb", "de-de", "zh-cn":
        fmt.Fprintf(w, "The locale is %s\n", locale)
    default:
        http.NotFound(w, r)
    }
}

完成以上代碼后,運(yùn)行g(shù)o mod tidy來(lái)整理go.mod文件并下載必要的依賴包妒穴,然后執(zhí)行web應(yīng)用:

?  go mod tidy
go: finding module for package github.com/bmizerany/pat
go: downloading github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f
go: found github.com/bmizerany/pat in github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f
?   go run ./cmd/www
2021/10/07 09:57:03 starting server on :4018...

如果你使用curl向應(yīng)用程序發(fā)出一些請(qǐng)求宋税,你應(yīng)該會(huì)發(fā)現(xiàn)相應(yīng)的區(qū)域設(shè)置會(huì)像這樣返回給你:

?  curl localhost:4018/en-gb
The locale is en-gb
?  curl localhost:4018/de-de
The locale is de-de
?  curl localhost:4018/zh-cn
The locale is zh-cn

提取和翻譯文本內(nèi)容

現(xiàn)在已經(jīng)為我們的web應(yīng)用程序打下了基礎(chǔ),讓我們進(jìn)入本文的核心部分讼油,并更新handleHome()函數(shù)杰赛,以便它能夠根據(jù)特定的區(qū)域翻譯“Welcome!”消息。在這個(gè)項(xiàng)目中矮台,我們將在應(yīng)用程序中使用英式英語(yǔ)(en-GB)作為默認(rèn)的“源”或“基礎(chǔ)”語(yǔ)言乏屯,但需要為其他地區(qū)提供德語(yǔ)和中文的歡迎信息翻譯版本根时。

為此,我們需要導(dǎo)入golang.org/x/text/language和golang.org/x/text/message包辰晕,并更新handleHome()函數(shù)來(lái)完成以下兩件事:
1蛤迎、創(chuàng)建一個(gè)language標(biāo)簽來(lái)標(biāo)識(shí)我們需要翻譯的目標(biāo)語(yǔ)言。language包包含提前為通用語(yǔ)言變量定義的標(biāo)簽含友,但我發(fā)現(xiàn)使用language.MustParse()函數(shù)創(chuàng)建標(biāo)簽更容易替裆。你可以使用這個(gè)包為BCP47中的值創(chuàng)建一個(gè)language.Tag,例如language.MustParse("zh-CN")窘问。
2辆童、 一旦有了語(yǔ)言標(biāo)簽,就可以使用message. Newprinter()函數(shù)來(lái)創(chuàng)建message.Printer實(shí)例來(lái)輸出對(duì)應(yīng)語(yǔ)言的消息南缓。
如果你跟隨本文操作胸遇,請(qǐng)繼續(xù)并更新你的cmd/www/handlers.go文件,以包含以下代碼:

File: cmd/www/handlers.go
package main

import (
    "net/http"

    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    locale := r.URL.Query().Get(":locale")

    // Declare variable to hold the target language tag.
    var lang language.Tag

    // 使用 language.MustParse()為區(qū)域設(shè)置分配適當(dāng)?shù)恼Z(yǔ)言標(biāo)簽汉形。
    switch locale {
    case "en-gb":
        lang = language.MustParse("en-GB")
    case "de-de":
        lang = language.MustParse("de-DE")
    case "zh-cn":
        lang = language.MustParse("zh-CN")
    default:
        http.NotFound(w, r)
        return
    }

    // 使用對(duì)應(yīng)語(yǔ)言初始化一個(gè)message.Printer實(shí)例
    p := message.NewPrinter(lang)
    // 將歡迎信息翻譯成目標(biāo)語(yǔ)言纸镊。
    p.Fprintf(w, "Welcome!\n")
}

再次執(zhí)行g(shù)o mod tidy下載必要的依賴:

$ go mod tidy
go: finding module for package golang.org/x/text/message
go: finding module for package golang.org/x/text/language
go: downloading golang.org/x/text v0.3.7
go: found golang.org/x/text/language in golang.org/x/text v0.3.7
go: found golang.org/x/text/message in golang.org/x/text v0.3.7

然后啟動(dòng)應(yīng)用:

?  go run ./cmd/www
2021/10/07 10:13:19 starting server on :4018...

當(dāng)你向任何支持的url發(fā)出請(qǐng)求時(shí),應(yīng)該會(huì)看到這樣的(未翻譯的)歡迎消息:

$ curl localhost:4018/en-gb
Welcome!

$ curl localhost:4018/de-de
Welcome!

$ curl localhost:4018/zh-cn
Welcome!

因此概疆,在所有情況下逗威,我們都能在en-GB源語(yǔ)言中看到“Welcome”消息。這是因?yàn)槲覀冃枰獮镚o的消息包提供我們想要使用的實(shí)際翻譯岔冀。如果沒(méi)有實(shí)際的翻譯凯旭,它將退回到用源語(yǔ)言顯示消息。

有許多方法可以為Go的消息包提供翻譯使套,但對(duì)于大多數(shù)重要的應(yīng)用程序來(lái)說(shuō)罐呼,使用一些自動(dòng)化工具來(lái)幫助您管理任務(wù)是明智的。幸運(yùn)的是Go提供了gotext工具來(lái)幫助解決這個(gè)問(wèn)題侦高。

如果你遵循下面的方法嫉柴,請(qǐng)使用go install在你的機(jī)器上安裝gotext可執(zhí)行文件:

go install golang.org/x/text/cmd/gotext@latest

如果一切正常,該工具應(yīng)該安裝到系統(tǒng)路徑的$GOBIN目錄下奉呛,你可以像這樣運(yùn)行它:

?  ~ which gotext
/Users/wangmingjun/go/bin/gotext
?  ~ gotext
gotext is a tool for managing text in Go source code.

Usage:

    gotext command [arguments]

The commands are:

    update      merge translations and generate catalog
    extract     extracts strings to be translated from code
    rewrite     rewrites fmt functions to use a message Printer
    generate    generates code to insert translated messages

Use "gotext help [command]" for more information about a command.

Additional help topics:


Use "gotext help [topic]" for more information about that topic.

gotext工具的功能很強(qiáng)大计螺,但其中一些重要的內(nèi)容需要在開(kāi)始前提下。首先go text需要和go generate配合使用瞧壮,它不是一個(gè)獨(dú)立的命令行工具登馒。你可以將它作為一個(gè)獨(dú)立的工具來(lái)運(yùn)行,但會(huì)發(fā)生一些奇怪的事情咆槽,如果你按照預(yù)期方式使用會(huì)更加流暢陈轿。

另一件事是,文檔和幫助功能基本上不存在。關(guān)于如何使用它的最佳指導(dǎo)在倉(cāng)庫(kù)的示例中济欢,可能還有您正在閱讀的這篇文章赠堵。

本文,我們將把所有與翻譯相關(guān)的代碼存儲(chǔ)在一個(gè)新的interanal/translations包中法褥。我們可以將web應(yīng)用程序的所有翻譯代碼保存在cmd/www下茫叭,但根據(jù)我(有限)的經(jīng)驗(yàn),我發(fā)現(xiàn)使用單獨(dú)的interanal/translations包更好半等。它有助于分離關(guān)注點(diǎn)揍愁,并使在同一項(xiàng)目中的不同應(yīng)用程序之間重用相同的翻譯。

如果您按照下面的步驟操作杀饵,請(qǐng)繼續(xù)創(chuàng)建新目錄和translations.go文件:

$ mkdir -p internal/translations
$ touch internal/translations/translations.go

此時(shí)莽囤,你的項(xiàng)目結(jié)構(gòu)應(yīng)該如下所示:

├── cmd
│   └── www
│       ├── handlers.go
│       └── main.go
├── go.mod
├── go.sum
└── internal
    └── translations
        └── translations.go

下面,打開(kāi)internal/translations/translations.go文件并添加go generate命令使用gotext來(lái)從應(yīng)用中提取需要翻譯的消息切距。

File: internal/translations/translations.go
package translations

//go:generate gotext -srclang=en-GB update -out=catalog.go -lang=en-GB,de-DE,zh-CN bookstore.example.com/cmd/www

在這個(gè)命令中有很多參數(shù)朽缎,讓我們快速介紹下。

  • -srclang參數(shù)指定應(yīng)用使用BCP 47標(biāo)簽作為基礎(chǔ)語(yǔ)言谜悟,在本文中基礎(chǔ)語(yǔ)言是en-GB
  • update是我們需要執(zhí)行g(shù)otext中的函數(shù)话肖。除了update函數(shù)還有extract、rewrite和generate函數(shù)葡幸,在翻譯流程中我們只需要執(zhí)行update函數(shù)最筒。
  • -out參數(shù)指定產(chǎn)生的消息目錄所在路徑。該路徑是執(zhí)行g(shù)o generate命令所在目錄的相對(duì)路徑蔚叨。在本文中設(shè)置為catalog.go床蜘,根據(jù)本文項(xiàng)目結(jié)構(gòu),消息目錄將輸出到internal/translations/catalog.go文件中蔑水。 我們將進(jìn)一步討論消息目錄并解釋它們是什么邢锯。
  • -lang參數(shù)是BCP 47標(biāo)簽列表(指定需要翻譯的語(yǔ)言)使用逗號(hào)分離。這里不需要添加基礎(chǔ)語(yǔ)言搀别,它有助于處理文本內(nèi)容的多元化丹擎。
  • 最后,為你想要?jiǎng)?chuàng)建翻譯的包(在本例中是bookstore.example.com/cmd/www)提供了完全限定的模塊路徑领曼。如果需要,可以列出多個(gè)包蛮穿,用空格字符分隔庶骄。

當(dāng)我們執(zhí)行g(shù)o generate命令時(shí),gotext將遍歷cmd/www應(yīng)用程序的代碼践磅,并查找所有mesage.Printer的調(diào)用单刁。然后提取相關(guān)消息字符串并將其輸出到一些JSON文件中進(jìn)行翻譯。

注意:gotext只查找代碼中messge.Printer.Printf(),F(xiàn)sprintf()和Sprintf()三個(gè)基礎(chǔ)函數(shù)羔飞。其他的例如Sprint()或Print()函數(shù)會(huì)忽略肺樟。

讓我們?cè)趖ranslations.go文件調(diào)用go generate。接下來(lái)逻淌,這將執(zhí)行我們?cè)谠撐募敳堪膅otext命令么伯。

go generate ./internal/translations/translations.go
de-DE: Missing entry for "Welcome!".
zh-CN: Missing entry for "Welcome!".

很好,看來(lái)我們有進(jìn)展了卡儒。得到了一些有用的反饋田柔,表明我們的“Welcome!”信息缺少必要的德語(yǔ)和中文翻譯。
如果你看看你的項(xiàng)目的目錄結(jié)構(gòu)骨望,它現(xiàn)在應(yīng)該是這樣的:

├── cmd
│   └── www
│       ├── handlers.go
│       └── main.go
├── go.mod
├── go.sum
└── internal
    └── translations
        ├── catalog.go
        ├── locales
        │   ├── de-DE
        │   │   └── out.gotext.json
        │   ├── en-GB
        │   │   └── out.gotext.json
        │   └── zh-CN
        │       └── out.gotext.json
        └── translations.go

我們可以看到go generate命令已經(jīng)自動(dòng)生成了一個(gè)internal/translations/catalog.go文件(我們將在后面介紹)硬爆,以及一個(gè)包含每種目標(biāo)語(yǔ)言的out.gotext.json文件的locale文件夾。
讓我們看看internal/translations/locale /de-DE/out.gotext.json文件:

{
    "language": "de-DE",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": ""
        }
    ]
}

在這個(gè)JSON文件中擎鸠,相關(guān)的BCP 47語(yǔ)言標(biāo)簽定義在文件的頂部缀磕,后面是需要翻譯的消息JSON數(shù)組。消息值是源代碼中要翻譯的文本劣光,(當(dāng)前為空)翻譯值是我們應(yīng)該輸入適當(dāng)?shù)牡抡Z(yǔ)翻譯的地方袜蚕。

需要指出的是你不需要編輯翻譯文件,實(shí)際上翻譯流程應(yīng)該是:
1赎线、你生成包含需要翻譯的out.gotext.json文件廷没。
2、你將這些文件發(fā)送給翻譯人員垂寥,翻譯人員編輯JSON添加必要的翻譯信息颠黎。然后他們將更新后的文件發(fā)回給你。
3滞项、然后將這些更新后的文件以messages.gotext.json的名稱保存在相應(yīng)語(yǔ)言的文件夾中狭归。

出于演示的目的,讓我們通過(guò)將out.gotext.json文件復(fù)制到messages.gotext.json文件中文判,并更新它們以包括翻譯后的消息过椎,這樣來(lái)快速模擬這個(gè)工作流:

$ cp internal/translations/locales/de-DE/out.gotext.json internal/translations/locales/de-DE/messages.gotext.json
$ cp internal/translations/locales/zh-CN/out.gotext.json internal/translations/locales/zh-CN/messages.gotext.json

nternal/translations/locales/de-DE/messages.gotext.json

{
    "language": "de-DE",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Willkommen!"
        }
    ]
}

internal/translations/locales/zh-CN/messages.gotext.json文件

{
    "language": "zh-CN",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Bienvenue !"
        }
    ]
}

如果您愿意,還可以查看一下en-GB源語(yǔ)言的out.gotext.json文件戏仓。您將看到消息的翻譯值已經(jīng)為我們自動(dòng)填充了疚宇。
internal/translations/locales/en-GB/messages.gotext.json文件

{
    "language": "en-GB",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Welcome!",
            "translatorComment": "Copied from source.",
            "fuzzy": true
        }
    ]
}

下一步是再次運(yùn)行g(shù)o generate命令。這一次赏殃,執(zhí)行它應(yīng)該不會(huì)有任何關(guān)于缺少翻譯的警告消息敷待。

go generate ./internal/translations/translations.go

現(xiàn)在看下internal/translations/catalog.go文件,它是執(zhí)行g(shù)otext update命令自動(dòng)生成的仁热。該文件包含一個(gè)消息目錄榜揖,粗略的說(shuō)是一個(gè)消息和翻譯之間的映射關(guān)系。
我們快速看下源代碼:

// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.

package translations

import (
    "golang.org/x/text/language"
    "golang.org/x/text/message"
    "golang.org/x/text/message/catalog"
)

type dictionary struct {
    index []uint32
    data  string
}

func (d *dictionary) Lookup(key string) (data string, ok bool) {
    p, ok := messageKeyToIndex[key]
    if !ok {
        return "", false
    }
    start, end := d.index[p], d.index[p+1]
    if start == end {
        return "", false
    }
    return d.data[start:end], true
}

func init() {
    dict := map[string]catalog.Dictionary{
        "de_DE": &dictionary{index: de_DEIndex, data: de_DEData},
        "en_GB": &dictionary{index: en_GBIndex, data: en_GBData},
        "zh_CN": &dictionary{index: zh_CNIndex, data: zh_CNData},
    }
    fallback := language.MustParse("en-GB")
    cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
    if err != nil {
        panic(err)
    }
    message.DefaultCatalog = cat
}

var messageKeyToIndex = map[string]int{
    "Welcome!\n": 0,
}

var de_DEIndex = []uint32{ // 2 elements
    0x00000000, 0x00000011,
} // Size: 32 bytes

const de_DEData string = "\x04\x00\x01\n\f\x02Willkommen!"

var en_GBIndex = []uint32{ // 2 elements
    0x00000000, 0x0000000e,
} // Size: 32 bytes

const en_GBData string = "\x04\x00\x01\n\t\x02Welcome!"

var zh_CNIndex = []uint32{ // 2 elements
    0x00000000, 0x00000010,
} // Size: 32 bytes

const zh_CNData string = "\x04\x00\x01\n\v\x02Bienvenue !"

// Total table size 143 bytes (0KiB); checksum: 385F6E56

我不想在這里詳述細(xì)節(jié),因?yàn)槟阒恍璋堰@個(gè)文件當(dāng)作一個(gè)“黑盒”就可以了举哟,而且——正如文件頂部的注釋所警告的那樣——我們不應(yīng)該直接對(duì)它進(jìn)行任何更改思劳。

但是需要指出的最重要的一點(diǎn)是,這個(gè)文件包含一個(gè)init()函數(shù)妨猩,當(dāng)調(diào)用該函數(shù)時(shí)潜叛,將初始化一個(gè)包含所有翻譯和映射的新消息目錄。然后通過(guò)對(duì)message.DefaultCatalog全局變量賦值册赛。

當(dāng)我們調(diào)用message.Printer函數(shù)钠导,將根據(jù)消息目錄執(zhí)行相關(guān)翻譯的查詢。 這非常好森瘪,因?yàn)檫@意味著我們所有的翻譯都在運(yùn)行時(shí)存儲(chǔ)在內(nèi)存中牡属,任何查找都非常快速和有效扼睬。

所以逮栅,我們可以看到使用的go generate和gotext update命令實(shí)際上做兩件事。第一窗宇,它在cmd/www應(yīng)用程序中遍歷代碼措伐,并提取必要的字符串轉(zhuǎn)換為out.gotext.json文件;第二军俊,它還解析任何messages.gotext.json文件(如果存在的話)侥加,并相應(yīng)地更新消息目錄。

最后需要在cmd/www/handlers.go文件中引入internal/translations包粪躬。這確保internal/translations/translation.go中的init()函數(shù)被調(diào)用担败,并且使得更新后的默認(rèn)消息目錄能生效。因?yàn)閕nternal/translations包并沒(méi)有在代碼中直接使用镰官,因此在引入該包的時(shí)候提前,需要使用占位符"_"防止編譯報(bào)錯(cuò)。
cmd/www/handlers.go代碼:

package main

import (
    "net/http"

    // 引入internal/translations包泳唠,確保init()函數(shù)被調(diào)用
    _ "bookstore.example.com/internal/translations"

    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    locale := r.URL.Query().Get(":locale")

    var lang language.Tag

    switch locale {
    case "en-gb":
        lang = language.MustParse("en-GB")
    case "de-de":
        lang = language.MustParse("de-DE")
    case "zh-cn":
        lang = language.MustParse("zh-CN")
    default:
        http.NotFound(w, r)
        return
    }

    p := message.NewPrinter(lang)
    p.Fprintf(w, "Welcome!\n")
}

讓我們?cè)囋囈陨洗a狈网,當(dāng)您重新啟動(dòng)應(yīng)用程序并嘗試發(fā)出一些請(qǐng)求時(shí),您現(xiàn)在應(yīng)該看到“Welcome!”消息被翻譯成適當(dāng)?shù)恼Z(yǔ)言笨腥。


?   curl localhost:4018/en-gb
Welcome!

?   curl localhost:4018/de-de
Willkommen!

?   curl localhost:4018/zh-cn
歡迎光臨

翻譯中使用變量

現(xiàn)在我們已經(jīng)在應(yīng)用程序中完成了基本的翻譯工作拓哺,讓我們看一些更高級(jí)的東西,并看看如何管理在翻譯中插入變量脖母。

為了說(shuō)明士鸥,我們修改handleHome函數(shù)的HTTP響應(yīng),其包含"{N} books available" 字符串镶奉,其中{N}是一個(gè)整數(shù)指的是bookstore中書(shū)的數(shù)目础淤。
修改 cmd/www/handlers.go文件如下:

package main

...

func handleHome(w http.ResponseWriter, r *http.Request) {
    locale := r.URL.Query().Get(":locale")

    var lang language.Tag

    switch locale {
    case "en-gb":
        lang = language.MustParse("en-GB")
    case "de-de":
        lang = language.MustParse("de-DE")
    case "zh-cn":
        lang = language.MustParse("zh-CN")
    default:
        http.NotFound(w, r)
        return
    }

    // 定義一個(gè)變量來(lái)保存書(shū)的數(shù)量。 在實(shí)際應(yīng)用中數(shù)量需查詢數(shù)據(jù)庫(kù)的到
    var totalBookCount = 1252794

    p := message.NewPrinter(lang)
    p.Fprintf(w, "Welcome!\n")

    //使用Fprintf() 函數(shù)在響應(yīng)中添加書(shū)數(shù)量
    p.Fprintf(w, "%d books available\n", totalBookCount)
}

保存更改哨苛,然后使用go generate輸出新的out.gotext.json文件鸽凶。您應(yīng)該看到新的 翻譯缺失警告消息,如下所示:

$ go generate ./internal/translations/translations.go
de-DE: Missing entry for "{TotalBookCount} books available".
zh-CN: Missing entry for "{TotalBookCount} books available".

看下internal/translations/locales/de-DE/out.gotext.json文件

{
    "language": "de-DE",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Willkommen!"
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation": "",
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

這里需要指出的第一件事是建峭,“Welcome!”消息的翻譯已經(jīng)貫穿整個(gè)流程玻侥,并且已經(jīng)出現(xiàn)在out.gotext.json文件中。這顯然是非常重要的亿蒸,因?yàn)檫@意味著當(dāng)我們將文件發(fā)送給翻譯人員時(shí)凑兰,他們將不需要再次提供翻譯。

第二件事是現(xiàn)在有一個(gè)新消息的條目边锁。我們可以看到它的形式是“{TotalBookCount} books available”姑食,使用Go代碼中的(大寫(xiě)的)變量名作為占位符參數(shù)。在編寫(xiě)代碼時(shí)茅坛,您應(yīng)該記住這一點(diǎn)音半,并嘗試使用對(duì)翻譯人員有意義的合理的描述性變量名。占位符數(shù)組還提供關(guān)于每個(gè)占位符值的附加信息贡蓖,最有用的部分可能是類型值(在本例中曹鸠,它告訴翻譯人員TotalBookCount值是整數(shù))。

因此下一步是將這些新的out.gotext.json文件發(fā)送給翻譯人員進(jìn)行翻譯斥铺。同樣彻桃,我們將在這里模擬,將它們復(fù)制到messages.gotext.json文件晾蜘,并添加翻譯邻眷,如下所示:

$ cp internal/translations/locales/de-DE/out.gotext.json internal/translations/locales/de-DE/messages.gotext.json
$ cp internal/translations/locales/zh-CN/out.gotext.json internal/translations/locales/zh-CN/messages.gotext.json

internal/translations/locales/de-DE/messages.gotext.json文件

{
    "language": "de-DE",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Willkommen!"
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation": "{TotalBookCount} Bücher erh?ltlich",
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

internal/translations/locales/zh-CN/messages.gotext.json文件

{
    "language": "zh-CN",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "歡迎光臨"
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation": "總共{TotalBookCount}本書(shū)可閱讀",
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

確保兩個(gè)message .gotext.json文件都已保存,然后運(yùn)行g(shù)o generate以更新消息目錄笙纤。運(yùn)行時(shí)應(yīng)該沒(méi)有任何警告耗溜。

$ go generate ./internal/translations/translations.go

當(dāng)你重新啟動(dòng)cmd/www應(yīng)用程序并再次發(fā)出一些HTTP請(qǐng)求時(shí),你現(xiàn)在應(yīng)該看到新的翻譯消息如下:

?  curl localhost:4018/en-gb
Welcome!
1,252,794 books available

?   curl localhost:4018/de-de
Willkommen!
1.252.794 Bücher erh?ltlich

?   curl localhost:4018/zh-cn
歡迎光臨
總共1,252,794本書(shū)可閱讀

效果不錯(cuò)省容,正如我們通過(guò)message.Printer翻譯抖拴,其很智能地將對(duì)應(yīng)的變量值也輸出到翻譯中而且根據(jù)語(yǔ)言進(jìn)行格式化。我們看到在中文和英文中數(shù)字是用"," 分隔的腥椒,而在德語(yǔ)中使用"."分隔阿宅。

處理多元化

如果我們的書(shū)店只有一本書(shū)可讀,會(huì)發(fā)生什么呢笼蛛?讓我們更新handleHome()函數(shù)洒放,使totalBookCount值為1,修改 cmd/www/handlers.go文件:

func handleHome(w http.ResponseWriter, r *http.Request) {
    locale := r.URL.Query().Get(":locale")

    var lang language.Tag

    switch locale {
    case "en-gb":
        lang = language.MustParse("en-GB")
    case "de-de":
        lang = language.MustParse("de-DE")
    case "zh-cn":
        lang = language.MustParse("zh-CN")
    default:
        http.NotFound(w, r)
        return
    }

    // 定義一個(gè)變量來(lái)保存書(shū)的數(shù)量滨砍。 在實(shí)際應(yīng)用中數(shù)量需查詢數(shù)據(jù)庫(kù)的到
    var totalBookCount = 1

    p := message.NewPrinter(lang)
    p.Fprintf(w, "Welcome!\n")

    //使用Fprintf() 函數(shù)在響應(yīng)中添加書(shū)數(shù)量
    p.Fprintf(w, "%d books available\n", totalBookCount)
}

你可以想象往湿,當(dāng)我們重新啟動(dòng)應(yīng)用程序并向localhost:4018/en-gb發(fā)出請(qǐng)求時(shí)會(huì)發(fā)生什么妖异。

$ curl localhost:4018/en-gb
Welcome!
1 books available

很明顯看到“1 books available”,這在英語(yǔ)語(yǔ)法中并不正確领追,因books是復(fù)數(shù)他膳。1本書(shū)的正確顯示應(yīng)該是單數(shù)才合理例如“1 book available”或者“One book available”效果更好。

我們可以根據(jù)messages.gotext.json文件中變量的值指定替代翻譯绒窑。
更新internal/translations/locales/en-GB/messages.gotext.json文件:

{
    "language": "en-GB",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Welcome!",
            "translatorComment": "Copied from source.",
            "fuzzy": true
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation": {
                "select": {
                    "feature": "plural",
                    "arg": "TotalBookCount",
                    "cases": {
                        "=1": {
                            "msg": "One book available"
                        },
                        "other": {
                            "msg": "{TotalBookCount} books available"
                        }
                    }
                }
            },
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

現(xiàn)在棕孙,我們不再將翻譯值設(shè)置為簡(jiǎn)單的字符串,而是將其設(shè)置為一個(gè)JSON對(duì)象些膨,該對(duì)象指示消息目錄根據(jù)TotalBookCount占位符的值使用不同的翻譯蟀俊。這里的關(guān)鍵部分是case值,它包含占位符的不同值所使用的翻譯订雾。支持的案例規(guī)則是:

分類 描述
"=x" x是整數(shù)等于占位符中的值|
"<x" x是整數(shù)小于占位符中的值
"other" 其他情況肢预,類似switch中的default語(yǔ)句|

我們更新下其他的message.gotext.json文件:
internal/translations/locales/de-DE/messages.gotext.json

{
    "language": "de-DE",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "Willkommen!"
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation": {
                "select": {
                    "feature": "plural",
                    "arg": "TotalBookCount",
                    "cases": {
                        "=1": {
                            "msg": "Ein Buch erh?ltlich"
                        },
                        "other": {
                            "msg": "{TotalBookCount} Bücher erh?ltlich"
                        }
                    }
                }
            },
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

internal/translations/locales/zh-CN/messages.gotext.json文件

{
    "language": "zh-CN",
    "messages": [
        {
            "id": "Welcome!",
            "message": "Welcome!",
            "translation": "歡迎光臨"
        },
        {
            "id": "{TotalBookCount} books available",
            "message": "{TotalBookCount} books available",
            "translation":  {
                "select": {
                    "feature": "plural",
                    "arg": "TotalBookCount",
                    "cases": {
                        "=1": {
                            "msg": "只有1本書(shū)可讀"
                        },
                        "other": {
                            "msg": "總共{TotalBookCount}本書(shū)可讀"
                        }
                    }
                }
            },
            "placeholders": [
                {
                    "id": "TotalBookCount",
                    "string": "%[1]d",
                    "type": "int",
                    "underlyingType": "int",
                    "argNum": 1,
                    "expr": "totalBookCount"
                }
            ]
        }
    ]
}

一旦這些文件被保存,再次使用go generate更新消息目錄:

go generate ./internal/translations/translations.go

重啟服務(wù)然后發(fā)起HTTP請(qǐng)求洼哎,可以看到如下信息:

?  ~ curl localhost:4018/en-gb
Welcome!
One book available

?  ~ curl localhost:4018/de-de
Willkommen!
Ein Buch erh?ltlich

?  ~ curl localhost:4018/zh-cn
歡迎光臨
只有1本書(shū)可讀

可以將book的數(shù)量修改大點(diǎn)查看響應(yīng)內(nèi)容:

curl localhost:4018/en-gb
Welcome!
1,252,794 books available

結(jié)果和預(yù)期的一樣误甚。

創(chuàng)建本地化抽象

在本文的最后一部分,我們將創(chuàng)建一個(gè)新的internal/localizer包谱净,它將對(duì)我們處理語(yǔ)言窑邦、打印和翻譯的所有代碼抽象出來(lái)。如果你跟隨本文操作壕探,繼續(xù)創(chuàng)建internal/localizer目錄包含localizer.go文件冈钦。

$ mkdir -p internal/localizer
$ touch internal/localizer/localizer.go

此時(shí),你的項(xiàng)目結(jié)構(gòu)應(yīng)該如下所示:

├── cmd
│   └── www
│       ├── handlers.go
│       └── main.go
├── go.mod
├── go.sum
└── internal
    ├── localizer
    │   └── localizer.go
    └── translations
        ├── catalog.go
        ├── locales
        │   ├── de-DE
        │   │   ├── messages.gotext.json
        │   │   └── out.gotext.json
        │   ├── en-GB
        │   │   ├── messages.gotext.json
        │   │   └── out.gotext.json
        │   └── fr-CH
        │       ├── messages.gotext.json
        │       └── out.gotext.json
        └── translations.go

然后在localizer.go文件中添加如下代碼:

package localizer

import (
    // 引入internal/translations包李请,確保 init()函數(shù)被調(diào)用
    _ "bookstore.example.com/internal/translations"

    "golang.org/x/text/language"
    "golang.org/x/text/message"
)

// 定義Localizer 類型保存相關(guān)本地化ID (和URL中使用類似)
// 創(chuàng)建不可導(dǎo)出的message.Printer實(shí)例
type Localizer struct {
    ID      string
    printer *message.Printer
}

// 初始化一個(gè)片瞧筛,其中保存了我們支持的每個(gè)區(qū)域設(shè)置的初始化本地化器類型。
var locales = []Localizer{
    {
        // 德國(guó)
        ID:      "de-de",
        printer: message.NewPrinter(language.MustParse("de-DE")),
    },
    {
        // 中國(guó)
        ID:      "zh-cn",
        printer: message.NewPrinter(language.MustParse("zh-CN")),
    },
    {
        //英國(guó)
        ID:      "en-gb",
        printer: message.NewPrinter(language.MustParse("en-GB")),
    },
}

// Get() 函數(shù)接收一個(gè)本地化ID导盅,并返回對(duì)應(yīng)本地化實(shí)例
// 如果本地化ID不支持將返回空较幌,false作為第二個(gè)參數(shù)值。
func Get(id string) (Localizer, bool) {
    for _, locale := range locales {
        if id == locale.ID {

            return locale, true
        }
    }

    return Localizer{}, false
}

// 為本地化類型增加Translate()方法白翻,該方法對(duì)消息和參數(shù)進(jìn)行包裝
func (l Localizer) Translate(key message.Reference, args ...interface{}) string {
    return l.printer.Sprintf(key, args...)
}

下面我們更新cmd/www/handlers.go文件來(lái)使用新建的Localizer類型乍炉, 同時(shí),讓handleHome()函數(shù)打印一個(gè)額外的“Launching soon!”消息滤馍。

package main

import (
    "fmt" // New import
    "net/http"

    "bookstore.example.com/internal/localizer" // New import
)

func handleHome(w http.ResponseWriter, r *http.Request) {
    //基于URL中的區(qū)域設(shè)置ID初始化一個(gè)新的本地化器岛琼。
    l, ok := localizer.Get(r.URL.Query().Get(":locale"))
    if !ok {
        http.NotFound(w, r)
        return
    }

    var totalBookCount = 1_252_794

    // 使用Translate()方法.
    fmt.Fprintln(w, l.Translate("Welcome!"))
    fmt.Fprintln(w, l.Translate("%d books available", totalBookCount))

    //增加 "Launching soon!"消息.
    fmt.Fprintln(w, l.Translate("Launching soon!"))
}

需指出的是,我們?cè)谶@里使用的Translate()方法不僅僅是一個(gè)語(yǔ)法糖巢株。你可能還記得我之前寫(xiě)過(guò)的警告:

當(dāng)gotext遍歷你的代碼時(shí)槐瑞,它實(shí)際上只查找對(duì)message.Printer.Printf()、Fprintf()和Sprintf()的調(diào)用——基本上這三個(gè)方法都以f結(jié)尾阁苞。忽略所有其他方法困檩,如Sprint()或Println()祠挫。

將所有翻譯都統(tǒng)一使用Translate()方法完成,其實(shí)現(xiàn)使用Sprintf悼沿,可以避免我們自己在使用過(guò)程中誤用Sprint或Println茸歧,導(dǎo)致gotext無(wú)法提取翻譯信息到j(luò)son文件。
下面我們?cè)俅螆?zhí)行g(shù)o generate:

go generate ./internal/translations/translations.go
de-DE: Missing entry for "Launching soon!".
zh-CN: Missing entry for "Launching soon!".

我們可以看到显沈,gotext已經(jīng)足夠智能,可以遍歷整個(gè)代碼庫(kù)并確定需要翻譯的字符串逢唤,甚至當(dāng)我們將message.Printer.Sprintf()調(diào)用抽象到另一個(gè)包中的helper函數(shù)時(shí)也是如此拉讯。

請(qǐng)繼續(xù)將out.gotext.json文件復(fù)制到message.gotext.json文件,并為新的“l(fā)aunch soon!”消息添加必要的翻譯鳖藕。然后記住再次運(yùn)行g(shù)o generate并重新啟動(dòng)web應(yīng)用程序魔慷。

當(dāng)你再次發(fā)出HTTP請(qǐng)求時(shí),你的響應(yīng)應(yīng)該是這樣的:

?  ~ curl localhost:4018/en-gb
Welcome!
1,252,794 books available
Launching soon!

?  ~ curl localhost:4018/de-de
Willkommen!
1.252.794 Bücher erh?ltlich
Bald verfügbar!

?  ~ curl localhost:4018/zh-cn
歡迎光臨
總共1,252,794本書(shū)可讀
即將啟動(dòng)

附加信息

路由沖突

在這篇文章的開(kāi)頭著恩,我故意不推薦使用httprouter院尔,盡管它是一個(gè)非常優(yōu)秀和流行的路由器。這是因?yàn)槭褂脛?dòng)態(tài)區(qū)域作為URL路徑的第一部分可能會(huì)導(dǎo)致與其他不需要地區(qū)前綴的應(yīng)用程序路由沖突喉誊,如/static/css/main.css或/admin/login邀摆。httprouter包不允許沖突路由,這使得在這個(gè)場(chǎng)景中使用它很尷尬伍茄。如果您確實(shí)希望使用httprouter栋盹,或者希望避免應(yīng)用程序中的路由沖突,您可以將區(qū)域設(shè)置作為查詢字符串參數(shù)例如:/category/travel?locale=gb

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末敷矫,一起剝皮案震驚了整個(gè)濱河市例获,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌曹仗,老刑警劉巖榨汤,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異怎茫,居然都是意外死亡收壕,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)轨蛤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)啼器,“玉大人,你說(shuō)我怎么就攤上這事俱萍《丝牵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵枪蘑,是天一觀的道長(zhǎng)损谦。 經(jīng)常有香客問(wèn)我岖免,道長(zhǎng),這世上最難降的妖魔是什么照捡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任颅湘,我火速辦了婚禮,結(jié)果婚禮上栗精,老公的妹妹穿的比我還像新娘闯参。我一直安慰自己,他們只是感情好悲立,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鹿寨。 她就那樣靜靜地躺著,像睡著了一般薪夕。 火紅的嫁衣襯著肌膚如雪脚草。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天原献,我揣著相機(jī)與錄音馏慨,去河邊找鬼。 笑死姑隅,一個(gè)胖子當(dāng)著我的面吹牛写隶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讲仰,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼樟澜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了叮盘?” 一聲冷哼從身側(cè)響起秩贰,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柔吼,沒(méi)想到半個(gè)月后毒费,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愈魏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年觅玻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片培漏。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溪厘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牌柄,到底是詐尸還是另有隱情畸悬,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布珊佣,位于F島的核電站蹋宦,受9級(jí)特大地震影響披粟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冷冗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一守屉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒿辙,春花似錦拇泛、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至习瑰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秽荤,已是汗流浹背甜奄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窃款,地道東北人课兄。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晨继,于是被迫代替她去往敵國(guó)和親烟阐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Internationalization (i18n) 國(guó)際化 (i18n) If you are working...
    王義杰閱讀 3,035評(píng)論 0 0
  • 【百度云搜索紊扬,搜各種資料:http://bdy.lqkweb.com】 【搜網(wǎng)盤(pán)蜒茄,搜各種資料:http://www...
    攻城獅筆記閱讀 2,278評(píng)論 0 3
  • 國(guó)際化基礎(chǔ)知識(shí) 國(guó)際化與本地化 國(guó)際化與本地化,或者說(shuō)全球化餐屎,其目的是讓你的站點(diǎn)支持多個(gè)國(guó)家和區(qū)域檀葛。其中國(guó)際化是指...
    jsAllen閱讀 5,910評(píng)論 0 3
  • 這一篇要寫(xiě)的是scratch-gui的多語(yǔ)言國(guó)際化翻譯脂倦。LLK團(tuán)隊(duì)為了讓非英語(yǔ)地區(qū)的同學(xué)參與scratch虾宇,構(gòu)建了...
    LiviSun閱讀 1,477評(píng)論 0 1
  • File types(文件類型) go命令檢查目錄中特定文件的集合。它根據(jù)文件的擴(kuò)展名表示要檢查的文件臭墨。這些擴(kuò)展名...
    Cxb168閱讀 1,740評(píng)論 0 0