Slide 1

Slide 1 text

The Go gopher was designed by Renée French. The gopher stickers was made by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license. リリース済みのサービスで 多言語化を行う時に考えること 2022/01/15(土) tenntenn Conference 2022 資料:https://tenn.in/i18n 動画:https://tenn.in/i18n-video

Slide 2

Slide 2 text

上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team

Slide 3

Slide 3 text

この活動を支える企業 ■ Now Do株式会社 ● https://nowdo.jp/ ● 採用情報:https://nowdo.breezy.hr/p/655051c7d789-web-app-engineer

Slide 4

Slide 4 text

私の多言語化の経験 ■ ゲームの海外展開(KLab時代) ● 日本でリリースしているゲームを複数カ国に展開 ○ 英語、スペイン語、ドイツ語、フランス語、ポルトガル語など ● 日本語版は絶賛開発中だった ○ 追いかけながら開発

Slide 5

Slide 5 text

多言語化は難しい ■ 翻訳だけではない ● 右から読むか左から読むか ● 通貨 ● タイムゾーン ● 単位や表記方法 ○ 例:数字の区切り(カンマかピリオド) ● 法律 ● ユーザごとに変わるかもしれない ○ アメリカに住んでるけど、日本語で表示 ● タイムゾーン、通貨、法律などはアメリカ、言語は日本語

Slide 6

Slide 6 text

翻訳だけでも難しい ■ 翻訳フローをどうするのか ● どうやってアップデートしていくか? ● どうやって文言を管理するのか? ■ どこまでやるか? ● 複数形 ● 活用形 ● 人によって代名詞が違う ○ he, she, they, … ■ 文字数が違う ● 英語にするとフォントが小さくなりすぎる

Slide 7

Slide 7 text

すでにリリースされている場合 ■ ソースコード内の文言 ● 取り出してくる必要がある ● Printfなどで整形されていると大変 ■ データベースに保存されている文言 ● お知らせなどがユーザデータとして入っている場合

Slide 8

Slide 8 text

Goにおける多言語化 ■ golang.org/x/text/languageパッケージ ● https://pkg.go.dev/golang.org/x/text/language ● 辞書管理機能 ● Accept-Languageヘッダのパース ● 翻訳がない場合のフォールバック機能 ■ go-i18n ● https://technickcal.com/go-i18n/ ● 多機能な多言語化のためのライブラリ ● 複数形なども対応している ● 文言を取り出す機能もついている ○ アップデートが楽

Slide 9

Slide 9 text

Accept-Languageヘッダのパース ■ language.ParseAcceptLanguage関数を使う ● (*language.Matcher).Matchメソッドがフォールバックしてくれる var matcher = language.NewMatcher([]language.Tag{ language.Japanese, language.English}) type langKey struct{} func WithLanguge(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) tag, _, _ := matcher.Match(t...) ctx := context.WithValue(r.Context(), langKey{}, tag) h.ServeHTTP(w, r.WithContext(ctx)) }) } func LangFromContext(ctx context.Context) language.Tag { if tag, ok := ctx.Value(langKey{}).(language.Tag); ok { return tag } return language.Und } func main() { http.Handle("/", WithLanguge(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, display.Self.Name(LangFromContext(r.Context()))) }))) http.ListenAndServe(":8080", nil) }

Slide 10

Slide 10 text

文言の設定 ■ golang.org/x/text/messageパッケージを使う ● message.SetString関数で文言を設定する ○ language.Tagとキーを指定する ○ message.DefaultCatalogに登録される ● message.NewPrinter関数で言語に対応するPrinterを取得 ○ Sprintでキーを指定すると翻訳された文言を取得できる func init() { must := func(err error) { if err != nil { panic(err) } } must(message.SetString(language.Japanese, "hello", "こんにちは")) must(message.SetString(language.English, "hello", "Hello")) } p := message.NewPrinter(tag) // Printerの取得 msg := p.Sprint("hello") // キーを指定して文言を取得( Sprintfで書式指定も可) fmt.Fprintln(w, display.Self.Name(tag), msg)

Slide 11

Slide 11 text

リリース済みサービスの多言語化 ■ 静的解析を行い文字列定数を取得 ● 静的単一代入形式に変換する ● 値が文字列なものを列挙する ■ 抜き出した文言をラップする ● 抜き出した文言にキーを割り当てる ● TODO("文言", "キー", "コメント")のような関数でラップする ○ 文言をそのまま返す関数 ■ 文言とキーが正しくマッチするかテストする ● TODO関数でラップされてる部分を静的解析で抜き出す ● 文言とキーが正しい組み合わせか確認する ■ TODO関数を置換していく ● S(言語, "キー")のような関数呼び出しに置換する

Slide 12

Slide 12 text

ソースコードから文字列を取得 analysisutil.InspectFuncs(funcs, func(i int, instr ssa.Instruction) bool { for _, rand := range instr.Operands(nil) { if rand == nil { continue } cnst, _ := (*rand).(*ssa.Const) // 文字列定数かどうか調べる if cnst == nil || cnst.Value == nil || cnst.Value.Kind() != constant.String { continue } s, err := strconv.Unquote(cnst.Value.String()) if err != nil { continue } s = strings.TrimSpace(s) if s != "" && !isASCII(s) { // 非アスキー文字が使われているか判定 pos := pkg.Fset.Position(cnst.Pos()) if !pos.IsValid() { pos = pkg.Fset.Position(instr.Pos()) } if !pos.IsValid() { pos = pkg.Fset.Position(instr.Parent().Pos()) } filename, err := filepath.Rel(dir, pos.Filename) if err != nil { filename = pos.Filename } fmt.Printf("%s:%d, %s\n", filename, pos.Line, s) // 該当箇所を出力 } } return true })

Slide 13

Slide 13 text

キーが間違っていないかのテスト ■ 静的解析を使ったテスト ● 手作業で入力したTODO関数の引数があってるかチェックする ● x/tools/go/packagesパッケージで抽象構文木と型情報を取得 ● TODO関数の引数を取得 ● 引数に指定されたキーを使って辞書から文言を取得 ● 引数に指定された文言と辞書から取得したものが一致するか確認

Slide 14

Slide 14 text

TODO関数を置き換える newFile := astutil.Apply(file, func(c *astutil.Cursor) bool { call, _ := c.Node().(*ast.CallExpr) if call == nil || len(call.Args) != 2 { return true } // 第2引数がKEYで始まる文字列の場合 keyLit, _ := call.Args[1].(*ast.BasicLit) if keyLit == nil { return true } key, err := strconv.Unquote(keyLit.Value) if err != nil || !strings.HasPrefix(key, "KEY") { return true } callee := typeutil.Callee(pkg.TypesInfo, call) if callee != todo { return true } // todoはTODO関数を表すオブジェクト // TODOをSに置き換える newCall, err := parser.ParseExpr(fmt.Sprintf(`S(ctx, %q)`, key)) if err != nil { return false } c.Replace(newCall) return false }, nil)

Slide 15

Slide 15 text

デモ

Slide 16

Slide 16 text

x/textパッケージのその他の機能 ■ 多言語化関連でもたくさん機能がある パッケージ名 説明 cases 言語による大文字小文字を扱う currency 通貨 feature/plural 複数形 number 数値(カンマなのかピリオドなのかなど)

Slide 17

Slide 17 text

まとめ ■ 多言語化は難しい ● 途中からやるのは難しい ○ 事前に織り込んでおくと楽 ● やり方はいろいろある ○ 文言自体を辞書キーにするパターンもある ● DB内の文言はマイグレーションが必要 ● プッシュ通知とか大変 ○ 受け取り手の言語設定によって分けるか? ● アプリ側でも対応が必要