Skip to main content

Hướng dẫn toàn diện về quốc tế hóa Go

Từ tệp thông báo đến phân giải ngôn ngữ an toàn với goroutine: thiết lập i18n trong ứng dụng Go bằng go-i18n rồi tự động hóa bản dịch bằng AI.

1

Cài đặt go-i18n

go-i18n là thư viện quốc tế hóa phổ biến nhất cho Go. Thư viện dùng quy tắc số nhiều CLDR và mẫu Go để nội suy biến, đồng thời hỗ trợ tệp thông báo JSON, TOML và YAML. Bạn cũng cần golang.org/x/text để so khớp thẻ ngôn ngữ.

go-i18n v2 yêu cầu Go 1.16+. Gói golang.org/x/text cung cấp khả năng phân tích cú pháp và so khớp thẻ ngôn ngữ BCP 47 mà go-i18n dùng nội bộ để chọn quy tắc số nhiều.
Terminal
go get -u github.com/nicksnyder/go-i18n/v2/i18n
go get -u golang.org/x/text/language
2

Tạo tệp thông báo

Tạo một tệp JSON cho mỗi ngôn ngữ trong thư mục locales. Mỗi thông báo có một ID và một hoặc nhiều dạng số nhiều. Go-i18n dùng cú pháp mẫu Go (hai dấu ngoặc nhọn với dấu chấm ở đầu) để nội suy biến.

locales/en.json
{
  "HelloWorld": { "other": "Hello, World!" },
  "Greeting": { "other": "Hello, {{.Name}}!" },
  "ItemCount": {
    "one": "{{.Count}} item",
    "other": "{{.Count}} items"
  },
  "WelcomeBack": {
    "other": "Welcome back, {{.Name}}. You have {{.Count}} messages."
  }
}
Dùng ID thông báo có tính mô tả như 'ItemCount' hoặc 'WelcomeBack' thay vì đường dẫn phân tách bằng dấu chấm. go-i18n dùng ID phẳng chứ không dùng khóa lồng nhau. Giữ ID ở dạng PascalCase để phù hợp với quy ước Go.
3

Tải Bundle

Bundle là sổ đăng ký trung tâm của go-i18n. Hãy tạo một Bundle khi khởi động, đăng ký định dạng tệp và tải mọi tệp thông báo. Bundle an toàn với goroutine — chỉ cần tạo một lần rồi dùng chung trong toàn ứng dụng.

main.go
package main

import (
    "encoding/json"
    "fmt"
    "github.com/nicksnyder/go-i18n/v2/i18n"
    "golang.org/x/text/language"
)

func main() {
    // 1. Create a bundle with a default language
    bundle := i18n.NewBundle(language.English)
    // 2. Register the unmarshal function for your file format
    bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
    // 3. Load message files
    bundle.MustLoadMessageFile("locales/en.json")
    bundle.MustLoadMessageFile("locales/ja.json")
    bundle.MustLoadMessageFile("locales/de.json")
    // 4. Create a localizer and localize a message
    localizer := i18n.NewLocalizer(bundle, "ja")
    msg := localizer.MustLocalize(&i18n.LocalizeConfig{
        MessageID: "HelloWorld",
    })
    fmt.Println(msg) // "こんにちは、世界!"
}
Nếu thấy 'message not found', hãy kiểm tra ba điều: 1) Tệp thông báo đã được tải bằng LoadMessageFile hoặc MustLoadMessageFile. 2) Phần mở rộng tệp khớp với hàm giải tuần tự đã đăng ký. 3) MessageID trong LocalizeConfig khớp chính xác với khóa trong tệp JSON của bạn (phân biệt chữ hoa chữ thường).
4

Dùng Localizer

Tạo một Localizer cho mỗi yêu cầu theo ngôn ngữ ưu tiên của người dùng. Localizer phân giải thông báo từ Bundle, kết xuất mẫu bằng công cụ text/template của Go và chọn dạng số nhiều chính xác dựa trên PluralCount.

Using the Localizer
// Simple message
msg := localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID: "HelloWorld",
})

// Message with template data
msg := localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID: "Greeting",
    TemplateData: map[string]interface{}{
        "Name": "Alice",
    },
})
// "Hello, Alice!" (en) or "こんにちは、Aliceさん!" (ja)

// Plural + template data
msg := localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID: "ItemCount",
    PluralCount: 5,
    TemplateData: map[string]interface{}{
        "Count": 5,
    },
})
// "5 items" (en) or "5個のアイテム" (ja)

// Combined: plurals + multiple variables
msg := localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID: "WelcomeBack",
    TemplateData: map[string]interface{}{
        "Name":  "Alice",
        "Count": 3,
    },
})
HTTP handler with locale detection
func handler(w http.ResponseWriter, r *http.Request) {
    // Accept-Language: ja,en;q=0.9,de;q=0.8
    accept := r.Header.Get("Accept-Language")

    // NewLocalizer accepts multiple languages — first match wins
    localizer := i18n.NewLocalizer(bundle, accept)

    msg := localizer.MustLocalize(&i18n.LocalizeConfig{
        MessageID: "HelloWorld",
    })

    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.Write([]byte(msg))
}
NewLocalizer chấp nhận nhiều chuỗi ngôn ngữ và thử theo thứ tự. Truyền trực tiếp tiêu đề Accept-Language: i18n.NewLocalizer(bundle, r.Header.Get("Accept-Language")). go-i18n tự động phân tích tiêu đề và so khớp với các bản dịch hiện có.
5

Xử lý quy tắc số nhiều

go-i18n triển khai quy tắc số nhiều CLDR cho mọi ngôn ngữ. Tiếng Anh có 2 dạng (one, other). Tiếng Ả Rập có 6 dạng (zero, one, two, few, many, other). Tiếng Nhật có 1 dạng (other). Hãy định nghĩa mọi dạng cần thiết trong tệp thông báo — go-i18n chọn dạng chính xác dựa trên PluralCount.

Plural forms by language
// English: 2 forms (one, other)
{
  "ItemCount": {
    "one": "{{.Count}} item",
    "other": "{{.Count}} items"
  }
}

// Arabic: 6 forms (zero, one, two, few, many, other)
{
  "ItemCount": {
    "zero": "لا عناصر",
    "one": "عنصر واحد",
    "two": "عنصران",
    "few": "{{.Count}} عناصر",
    "many": "{{.Count}} عنصرًا",
    "other": "{{.Count}} عنصر"
  }
}

// Japanese: 1 form (other)
{
  "ItemCount": {
    "other": "{{.Count}}個のアイテム"
  }
}
Using plural rules
// go-i18n selects the correct plural form based on PluralCount
localizer := i18n.NewLocalizer(bundle, "ar")

msg := localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID:   "ItemCount",
    PluralCount: 3,
    TemplateData: map[string]interface{}{
        "Count": 3,
    },
})
// Arabic "few" form: "3 عناصر"

msg = localizer.MustLocalize(&i18n.LocalizeConfig{
    MessageID:   "ItemCount",
    PluralCount: 1,
    TemplateData: map[string]interface{}{
        "Count": 1,
    },
})
// Arabic "one" form: "عنصر واحد"
PluralCount và TemplateData tách biệt nhau. PluralCount chọn dạng số nhiều còn TemplateData cung cấp giá trị để kết xuất mẫu. Nếu cần số lượng trong văn bản thông báo, hãy truyền vào cả hai: PluralCount: n và TemplateData: map[string]interface{'}'{"Count": n}.
6

Phát hiện ngôn ngữ

Trong ứng dụng web, hãy phát hiện ngôn ngữ ưu tiên của người dùng từ nhiều nguồn: tham số truy vấn, cookie, tiêu đề Accept-Language hoặc phân đoạn đường dẫn URL. Dùng golang.org/x/text/language.Matcher để thương lượng ngôn ngữ theo chuẩn BCP 47.

Locale detection middleware
// detectLocale resolves the user's preferred language.
// Priority: query param > cookie > Accept-Language header > default
func detectLocale(r *http.Request, matcher language.Matcher) string {
    // 1. Explicit query parameter: ?lang=ja
    if lang := r.URL.Query().Get("lang"); lang != "" {
        tag, _, _ := matcher.Match(language.Make(lang))
        return tag.String()
    }
    // 2. Cookie from previous selection
    if cookie, err := r.Cookie("lang"); err == nil {
        tag, _, _ := matcher.Match(language.Make(cookie.Value))
        return tag.String()
    }
    // 3. Accept-Language header
    accept := r.Header.Get("Accept-Language")
    if accept != "" {
        tags, _, _ := language.ParseAcceptLanguage(accept)
        if len(tags) > 0 {
            tag, _, _ := matcher.Match(tags...)
            return tag.String()
        }
    }
    return "en" // 4. Default
}

// Usage:
matcher := language.NewMatcher([]language.Tag{
    language.English, language.Japanese,
    language.German, language.Spanish,
})

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    locale := detectLocale(r, matcher)
    localizer := i18n.NewLocalizer(bundle, locale)
    // ... use localizer
})
language.NewMatcher trả về kết quả khớp nhất trong các ngôn ngữ bạn hỗ trợ chứ không phải tùy chọn thô của người dùng. Nếu người dùng yêu cầu 'pt-BR' nhưng bạn chỉ hỗ trợ 'pt', trình so khớp sẽ trả về chính xác 'pt'. Nếu không có trình so khớp, bạn phải tự viết logic dự phòng cho từng biến thể khu vực.
7

Khắc phục phương án dự phòng theo từng khóa bằng go-locale-chain

go-i18n có một hạn chế đã biết: sau khi một ngôn ngữ khớp (đã tải bất kỳ bản dịch nào), khóa bị thiếu sẽ không chuyển sang ngôn ngữ tiếp theo trong chuỗi. Người dùng pt-BR với tệp pt-BR mới dịch một phần sẽ nhận chuỗi trống thay vì dùng pt-PT hoặc pt làm phương án dự phòng. go-locale-chain khắc phục vấn đề này bằng cách phân giải dự phòng theo từng khóa trên 75 chuỗi ngôn ngữ tích hợp sẵn.

Terminal
go get github.com/i18n-agent/go-locale-chain
go-locale-chain with go-i18n
package main

import (
    "encoding/json"
    "fmt"

    "github.com/nicksnyder/go-i18n/v2/i18n"
    localechain "github.com/i18n-agent/go-locale-chain"
    "golang.org/x/text/language"
)

func main() {
    // 1. Configure fallback chains (call once at startup)
    localechain.Configure()

    // 2. Set up go-i18n bundle as usual
    bundle := i18n.NewBundle(language.English)
    bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
    bundle.MustLoadMessageFile("locales/en.json")
    bundle.MustLoadMessageFile("locales/pt.json")
    bundle.MustLoadMessageFile("locales/pt-PT.json")
    bundle.MustLoadMessageFile("locales/pt-BR.json")

    // 3. Resolve with per-key fallback
    result, _ := localechain.ResolveWithLoader("pt-BR", func(locale string) (map[string]string, error) {
        localizer := i18n.NewLocalizer(bundle, locale)
        messages := make(map[string]string)
        for _, id := range []string{"hello", "goodbye", "thanks"} {
            msg, err := localizer.Localize(&i18n.LocalizeConfig{MessageID: id})
            if err == nil {
                messages[id] = msg
            }
        }
        return messages, nil
    })

    fmt.Println(result["hello"])   // "Olá (BR)"   — from pt-BR
    fmt.Println(result["goodbye"]) // "Adeus (PT)" — fallback to pt-PT
    fmt.Println(result["thanks"])  // "Obrigado"   — fallback to pt
}
go-locale-chain là gói Go mã nguồn mở không có phần phụ thuộc bên ngoài. Gói này bổ trợ cho go-i18n — dùng go-i18n để tải thông báo, xử lý số nhiều và kết xuất mẫu, đồng thời dùng go-locale-chain để phân giải chuỗi dự phòng đúng cách.
Standalone usage (no go-i18n)
// Standalone: zero external dependencies, works with any format
localechain.Configure()

result, _ := localechain.ResolveWithLoader("es-MX", func(locale string) (map[string]string, error) {
    data, err := os.ReadFile(fmt.Sprintf("locales/%s.json", locale))
    if err != nil {
        return nil, err // Locale file doesn't exist — skip
    }
    var msgs map[string]string
    json.Unmarshal(data, &msgs)
    return msgs, nil
})
// es-MX -> es-419 -> es: each key resolved from most specific locale
Dùng ConfigureWithOverrides() để tùy chỉnh các chuỗi cụ thể mà vẫn giữ giá trị mặc định. Ví dụ: đơn giản hóa pt-BR để chỉ dùng pt làm phương án dự phòng hoặc thêm chuỗi cho ngôn ngữ không có trong giá trị mặc định như sv-FI -> sv.
8

Tự động hóa bản dịch

Sau khi hoàn tất thiết lập i18n, hãy dùng AI để dịch các tệp ngôn ngữ. Tạo bản dịch cho mọi ngôn ngữ đích từ tệp nguồn tiếng Anh ngay trong IDE hoặc quy trình CI/CD của bạn.

Terminal
# In your IDE, ask your AI assistant:
> Translate locales/en.json to German, Japanese, and Spanish

✓ locales/de.json created (1.2s)
✓ locales/ja.json created (1.5s)
✓ locales/es.json created (1.1s)

# Or use the CLI in CI/CD:
npx i18n-agent translate locales/en.json --lang de,ja,es
Dịch tăng dần — khi thêm ID thông báo mới vào tệp nguồn, chỉ dịch các khóa mới thay vì tạo lại mọi tệp. Cách này bảo toàn các bản dịch đã được con người duyệt.

Tự động hóa chất lượng bản dịch

Phát hiện khóa bị thiếu và phần giữ chỗ bị hỏng trước khi phát hành bằng i18n-validate. Kiểm thử giao diện bằng bản dịch giả lập với i18n-pseudo trước khi có bản dịch thật.

Lỗi thường gặp

PluralCount và TemplateData không khớp

PluralCount chọn dạng số nhiều nhưng không chèn giá trị vào mẫu. Bạn cũng phải truyền số lượng trong TemplateData để giá trị xuất hiện trong thông báo đã kết xuất. Nếu không có TemplateData, '{'{.Count}'}' sẽ được kết xuất thành '<no value>'.

Thiếu RegisterUnmarshalFunc

LoadMessageFile âm thầm không trả về thông báo nào nếu bạn quên gọi bundle.RegisterUnmarshalFunc() cho định dạng tệp. Luôn đăng ký json.Unmarshal (hoặc toml/yaml) trước khi tải tệp.

Phương án dự phòng theo từng khóa không hoạt động

NewLocalizer của go-i18n chấp nhận nhiều ngôn ngữ nhưng sau khi một ngôn ngữ khớp với một khóa, các khóa bị thiếu sẽ trả về chuỗi trống thay vì dùng phương án dự phòng. Hãy dùng go-locale-chain để khắc phục bằng cơ chế chuyển tiếp đúng cách theo từng khóa.

Cú pháp mẫu: {'{.Var}'} chứ không phải {'{Var}'}

go-i18n dùng cú pháp text/template của Go. Biến phải có dấu chấm ở đầu: {'{.Name}'} chứ không phải {'{Name}'}. Dấu chấm tham chiếu đến ánh xạ TemplateData. Thiếu dấu chấm sẽ gây lỗi thực thi mẫu.

Cấu trúc tệp đề xuất

Project Structure
my-go-app/
├── locales/
│   ├── en.json           # Source language (English)
│   ├── de.json           # German translations
│   ├── ja.json           # Japanese translations
│   ├── es.json           # Spanish translations
│   ├── pt.json           # Portuguese (base)
│   ├── pt-PT.json        # Portuguese (Portugal)
│   └── pt-BR.json        # Portuguese (Brazil)
├── i18n/
│   ├── bundle.go         # Bundle initialization
│   ├── detect.go         # Locale detection logic
│   └── middleware.go      # HTTP middleware for locale
├── main.go
├── go.mod
└── go.sum

Dùng thử i18n Agent ngay

Thả tệp bản dịch của bạn vào đây

JSON, YAML, PO, XML, CSV, Markdown, Properties

hoặc nhấp để duyệt

Ngôn ngữ đích

Không cần đăng kýBáo giá tức thì

Phương án dự phòng ngôn ngữ với go-locale-chain

Khi thiếu khóa dịch trong một ngôn ngữ khu vực như pt-BR, go-i18n chuyển thẳng sang ngôn ngữ mặc định thay vì kiểm tra ngôn ngữ mẹ pt trước.

Terminal
go get github.com/i18n-agent/go-locale-chain
Configuration
import localechain "github.com/i18n-agent/go-locale-chain"

chain := localechain.New(localechain.Config{
    Fallbacks: map[string][]string{
        "pt-BR":      {"pt", "en"},
        "zh-Hant-HK": {"zh-Hant", "zh", "en"},
    },
})

Xem Hướng dẫn về phương án dự phòng ngôn ngữ để biết danh sách đầy đủ các framework được hỗ trợ và 75 chuỗi tích hợp sẵn. Learn more →

Câu hỏi thường gặp về Go i18n