SEO//13 최소 읽기 시간

Go(Golang) i18n: 국제화 패턴과 라이브러리

Eray Gündoğmuş
공유

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/languageBCP 47 언어 태그 파싱 및 매칭
golang.org/x/text/messagei18n이 적용된 Printf 스타일 포매팅 메시지
golang.org/x/text/number숫자 포매팅 (복수형, 통화)
golang.org/x/text/collate로케일 인식 문자열 정렬
golang.org/x/text/unicode/normUnicode 정규화
github.com/nicksnyder/go-i18n/v2번역 파일 관리, 복수형 처리
github.com/BurntSushi/tomlTOML 파싱 (일반적인 번역 형식)

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 전송을 하나의 개발자 중심 플랫폼으로 결합합니다. 스프레드시트 관리를 중단하고 모든 언어로 출시를 시작하세요.

무료로 시작하기 → · 기능 살펴보기 · 문서 읽기

Comments

Loading comments...