Go(Golang) i18n: 국제화 패턴과 라이브러리
Go(Golang) i18n: 국제화 패턴과 라이브러리 Go는 글로벌 사용자에게 서비스를 제공하는 백엔드 서비스, CLI, API에서 점점 인기를 얻고 있는 언어입니다. Go의 표준 라이브러리에는 다른 생태계만큼 포괄적인 i18n 프레임워크가 포함되어 있지 않지만,...
목차
Go(Golang) i18n: 국제화 패턴과 라이브러리
Go는 글로벌 사용자에게 서비스를 제공하는 백엔드 서비스, CLI, API에서 점점 인기를 얻고 있는 언어입니다. Go의 표준 라이브러리에는 다른 생태계만큼 포괄적인 i18n 프레임워크가 포함되어 있지 않지만, golang.org/x/text, go-i18n, 그리고 신중한 아키텍처의 조합으로 견고한 다국어 Go 애플리케이션을 구축할 수 있습니다.
이 가이드는 Go i18n 툴킷 전체를 다룹니다. x/text를 사용한 로케일 인식 포매팅부터 go-i18n을 통한 번역 관리까지, Go 서비스와 CLI에서 i18n을 구조화하는 패턴도 포함합니다.
Go의 i18n 에코시스템 개요
Go의 i18n 도구는 여러 패키지에 분산되어 있습니다:
| 패키지 | 목적 |
|---|---|
golang.org/x/text/language | BCP 47 언어 태그 파싱 및 매칭 |
golang.org/x/text/message | i18n이 적용된 Printf 스타일 포매팅 메시지 |
golang.org/x/text/number | 숫자 포매팅 (복수형, 통화) |
golang.org/x/text/collate | 로케일 인식 문자열 정렬 |
golang.org/x/text/unicode/norm | Unicode 정규화 |
github.com/nicksnyder/go-i18n/v2 | 번역 파일 관리, 복수형 처리 |
github.com/BurntSushi/toml | TOML 파싱 (일반적인 번역 형식) |
golang.org/x/text를 사용한 언어 태그 파싱
golang.org/x/text/language 패키지는 BCP 47 언어 태그를 구현합니다. 이는 HTTP Accept-Language 헤더, HTML lang 속성, 로케일 문자열에 사용되는 로케일 식별자의 표준입니다.
언어 태그 파싱 및 매칭
package main
import (
"fmt"
"golang.org/x/text/language"
)
func main() {
// 언어 태그 파싱
tag, err := language.Parse("zh-Hant-TW")
if err != nil {
panic(err)
}
fmt.Println(tag) // zh-Hant-TW
// 언어 매칭: 지원되는 언어에서 최적의 매치 찾기
supported := []language.Tag{
language.English,
language.French,
language.SimplifiedChinese,
language.TraditionalChinese,
}
matcher := language.NewMatcher(supported)
// 사용자가 zh-TW (번체 중국어, 대만)를 요청
t, _, _ := matcher.Match(language.MustParse("zh-TW"))
fmt.Println(t) // zh-Hant → 번체 중국어와 매칭
// 사용자가 es-MX (멕시코 스페인어)를 요청
t, _, _ = matcher.Match(language.MustParse("es-MX"))
fmt.Println(t) // en → 영어로 폴백 (스페인어 미지원)
}
Accept-Language 헤더 파싱
import (
"net/http"
"golang.org/x/text/language"
)
var supported = []language.Tag{
language.English,
language.French,
language.German,
language.Japanese,
}
var matcher = language.NewMatcher(supported)
func getLocale(r *http.Request) language.Tag {
// Accept-Language 헤더 파싱
accept := r.Header.Get("Accept-Language")
tags, _, err := language.ParseAcceptLanguage(accept)
if err != nil || len(tags) == 0 {
return language.English
}
tag, _, _ := matcher.Match(tags...)
return tag
}
go-i18n: 번역 관리
go-i18n은 번역 관리에 가장 널리 사용되는 Go 라이브러리입니다. 다음을 지원합니다:
- JSON, TOML, YAML 번역 파일
- 복수형 처리를 위한 ICU 메시지 형식
- 템플릿 보간
- 여러 번역 파일 로딩
설치 및 설정
go get github.com/nicksnyder/go-i18n/v2@latest
go get github.com/BurntSushi/toml
번역 파일 구조
# locales/en.toml
[welcome]
description = "신규 사용자를 위한 환영 메시지"
one = "환영합니다, {{.Name}}님! 새 알림이 {{.Count}}건 있습니다."
other = "환영합니다, {{.Name}}님! 새 알림이 {{.Count}}건 있습니다."
[item_count]
description = "목록의 항목 수"
zero = "항목 없음"
one = "{{.Count}}개 항목"
other = "{{.Count}}개 항목"
# locales/fr.toml
[welcome]
one = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelle notification."
other = "Bienvenue, {{.Name}} ! Vous avez {{.Count}} nouvelles notifications."
[item_count]
zero = "Aucun élément"
one = "{{.Count}} élément"
other = "{{.Count}} éléments"
go-i18n 초기화
package i18n
import (
"embed"
"encoding/json"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
//go:embed locales/*.toml
var localeFS embed.FS
var bundle *i18n.Bundle
func Init() error {
bundle = i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// 모든 로케일 파일 로드
entries, err := localeFS.ReadDir("locales")
if err != nil {
return err
}
for _, entry := range entries {
if _, err := bundle.LoadMessageFileFS(localeFS, "locales/"+entry.Name()); err != nil {
return fmt.Errorf("로케일 %s 로딩 중 오류: %w", entry.Name(), err)
}
}
return nil
}
// NewLocalizer는 주어진 언어 태그에 대한 로컬라이저를 생성합니다
func NewLocalizer(langs ...string) *i18n.Localizer {
return i18n.NewLocalizer(bundle, langs...)
}
메시지 번역
package handlers
import (
"net/http"
"myapp/i18n"
)
type WelcomeData struct {
Name string
Count int
}
func WelcomeHandler(w http.ResponseWriter, r *http.Request) {
// 요청 언어에 대한 로컬라이저 가져오기
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(accept, "en")
// 복수형 처리가 있는 단순 메시지
itemCount := 3
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "item_count",
PluralCount: itemCount,
TemplateData: map[string]interface{}{
"Count": itemCount,
},
})
if err != nil {
msg = "알 수 없는 수의 항목" // 폴백
}
// 이름과 복수형이 있는 메시지
welcome, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "welcome",
PluralCount: itemCount,
TemplateData: WelcomeData{
Name: "Alice",
Count: itemCount,
},
})
fmt.Fprintf(w, "%s\n%s\n", welcome, msg)
}
숫자 및 통화 포매팅
golang.org/x/text/message 패키지는 로케일 인식 숫자 포매팅을 제공합니다:
package main
import (
"fmt"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// 로케일별 프린터 생성
enPrinter := message.NewPrinter(language.English)
dePrinter := message.NewPrinter(language.German)
jaPrinter := message.NewPrinter(language.Japanese)
amount := 1234567.89
// 로케일 인식 숫자 포매팅
enPrinter.Printf("%v\n", number.Decimal(amount))
// 1,234,567.89
dePrinter.Printf("%v\n", number.Decimal(amount))
// 1.234.567,89
jaPrinter.Printf("%v\n", number.Decimal(amount))
// 1,234,567.89
// 퍼센트 포매팅
enPrinter.Printf("%.2%\n", number.Percent(0.8527))
// 85.27%
dePrinter.Printf("%.2%\n", number.Percent(0.8527))
// 85,27 %
}
날짜 및 시간 포매팅
Go의 time 패키지는 기본 날짜 포매팅을 처리하지만, 로케일 인식 날짜 포매팅을 위해서는 x/text 또는 서드파티 라이브러리가 필요합니다:
import (
"time"
"golang.org/x/text/language"
"golang.org/x/text/language/display"
)
func formatDateLocale(t time.Time, locale language.Tag) string {
// Go의 내장 시간 포매팅은 고정된 레이아웃 문자열을 사용합니다
// 로케일별 포매팅에는 strftime 동등 라이브러리를 사용하거나
// CLDR 데이터에서 패턴을 생성하세요
switch locale {
case language.English:
return t.Format("January 2, 2006")
case language.German:
return t.Format("2. January 2006") // 독일어 날짜 형식
case language.Japanese:
return t.Format("2006年01月02日")
default:
return t.Format(time.RFC3339)
}
}
프로덕션 품질의 로케일 인식 날짜 포매팅을 위해서는 요일명과 월명에 github.com/goodsign/monday와 같은 라이브러리를 사용하세요.
Go 웹 서비스에서의 i18n 구조화
미들웨어 기반 로케일 감지
package middleware
import (
"context"
"net/http"
"golang.org/x/text/language"
"myapp/i18n"
)
type contextKey string
const localizerKey contextKey = "localizer"
var supported = []language.Tag{
language.English,
language.French,
language.German,
language.Japanese,
}
var matcher = language.NewMatcher(supported)
func LocaleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 먼저 URL 쿼리 파라미터 확인 (예: ?lang=fr)
lang := r.URL.Query().Get("lang")
// 그다음 쿠키 확인
if lang == "" {
if cookie, err := r.Cookie("lang"); err == nil {
lang = cookie.Value
}
}
// Accept-Language 헤더로 폴백
if lang == "" {
lang = r.Header.Get("Accept-Language")
}
// 로컬라이저를 생성하고 컨텍스트에 연결
localizer := i18n.NewLocalizer(lang, "en")
ctx := context.WithValue(r.Context(), localizerKey, localizer)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetLocalizer는 컨텍스트에서 로컬라이저를 가져옵니다
func GetLocalizer(ctx context.Context) *i18n.Localizer {
l, ok := ctx.Value(localizerKey).(*i18n.Localizer)
if !ok {
return i18n.NewLocalizer("en")
}
return l
}
번역 가능한 콘텐츠로서의 오류 메시지
package errors
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
)
// AppError는 번역 가능한 메시지를 가진 오류입니다
type AppError struct {
MessageID string
TemplateData interface{}
Err error
}
func (e *AppError) Error() string {
return e.MessageID // 내부 식별자
}
func (e *AppError) Localize(localizer *i18n.Localizer) string {
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: e.MessageID,
TemplateData: e.TemplateData,
})
if err != nil {
return e.MessageID // 키로 폴백
}
return msg
}
// 사용 예시
var ErrNotFound = &AppError{MessageID: "error.not_found"}
var ErrUnauthorized = &AppError{MessageID: "error.unauthorized"}
Go CLI 애플리케이션의 i18n
CLI 애플리케이션은 고유한 i18n 요구사항이 있습니다: 시스템 로케일을 감지하고, 출력을 적절히 포맷하며, 터미널 인코딩을 처리해야 합니다.
package main
import (
"os"
"golang.org/x/text/language"
"myapp/i18n"
)
func detectSystemLocale() string {
// LANG 환경 변수 확인 (Unix/Linux/macOS)
if lang := os.Getenv("LANG"); lang != "" {
// LANG은 일반적으로 "en_US.UTF-8" 형식입니다
// 언어 태그 추출
parts := strings.Split(lang, ".")
if len(parts) > 0 {
// "en_US"를 "en-US" (BCP 47)로 변환
return strings.ReplaceAll(parts[0], "_", "-")
}
}
// LC_ALL, LC_MESSAGES 확인
for _, env := range []string{"LC_ALL", "LC_MESSAGES"} {
if lang := os.Getenv(env); lang != "" {
return strings.ReplaceAll(strings.Split(lang, ".")[0], "_", "-")
}
}
return "en" // 영어를 기본값으로
}
func main() {
locale := detectSystemLocale()
localizer := i18n.NewLocalizer(locale, "en")
// 모든 출력에 로컬라이저 사용
fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{
MessageID: "cli.welcome",
}))
}
추출 및 관리 도구
go-i18n은 번역 가능한 문자열을 추출하기 위한 goi18n 커맨드라인 도구를 제공합니다:
# CLI 도구 설치
go install github.com/nicksnyder/go-i18n/v2/goi18n@latest
# Go 소스에서 문자열 추출 (active.en.toml 생성)
goi18n extract -format toml -outdir locales ./...
# 새 문자열을 기존 번역에 병합
# (신규/변경된 문자열만 포함하는 translate.fr.toml 생성)
goi18n merge -format toml -outdir locales locales/active.en.toml locales/active.fr.toml
# 번역 후 translate.fr.toml을 active.fr.toml에 병합
goi18n merge -format toml -outdir locales locales/active.fr.toml locales/translate.fr.toml
이 워크플로우는 지속적인 번역 업데이트를 위한 CI/CD 로컬라이제이션 자동화와 잘 통합됩니다.
복잡한 언어의 복수형 처리
Go-i18n은 지원되는 모든 언어에 대해 CLDR 규칙을 통해 복수형 처리를 자동으로 합니다:
# locales/ru.toml (러시아어 - 4가지 복수형)
[item_count]
zero = "Нет элементов"
one = "{{.Count}} элемент" # 1, 21, 31...
few = "{{.Count}} элемента" # 2-4, 22-24...
many = "{{.Count}} элементов" # 5-20, 25-30...
other = "{{.Count}} элемента" # 소수, 기타
# locales/ar.toml (아랍어 - 6가지 복수형)
[item_count]
zero = "لا عناصر"
one = "{{.Count}} عنصر"
two = "{{.Count}} عنصران"
few = "{{.Count}} عناصر"
many = "{{.Count}} عنصرًا"
other = "{{.Count}} عنصر"
복수형 규칙에 대한 더 깊은 내용은 언어간 복수형 규칙을 참조하세요.
Go에서 i18n 테스트
package i18n_test
import (
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func TestPluralForms(t *testing.T) {
bundle := setupTestBundle(t)
tests := []struct {
lang string
count int
expected string
}{
{"en", 0, "No items"},
{"en", 1, "1 item"},
{"en", 2, "2 items"},
{"ru", 1, "1 элемент"},
{"ru", 2, "2 элемента"},
{"ru", 5, "5 элементов"},
{"ru", 11, "11 элементов"}, // 주의: 11은 "one"이 아닌 "many"를 사용
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s_%d", tt.lang, tt.count), func(t *testing.T) {
localizer := i18n.NewLocalizer(bundle, tt.lang)
got, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: "item_count",
PluralCount: tt.count,
TemplateData: map[string]interface{}{"Count": tt.count},
})
if err != nil {
t.Fatalf("로컬라이즈 오류: %v", err)
}
if got != tt.expected {
t.Errorf("취득값 %q, 기대값 %q", got, tt.expected)
}
})
}
}
포괄적인 테스트 전략은 i18n 테스트 도구, 전략 및 자동화를 참조하세요.
better-i18n으로 앱을 글로벌하게 확장하세요
better-i18n은 AI 기반 번역, git 네이티브 워크플로우, 글로벌 CDN 전송을 하나의 개발자 중심 플랫폼으로 결합합니다. 스프레드시트 관리를 중단하고 모든 언어로 출시를 시작하세요.
무료로 시작하기 → · 기능 살펴보기 · 문서 읽기