Upgrade to Pro — share decks privately, control downloads, hide ads and more …

リリース済みのサービスで多言語化を行う時に考えること - tenntenn Conference 2022

リリース済みのサービスで多言語化を行う時に考えること - tenntenn Conference 2022

この資料はtenntenn Conference 2022にて発表を行った際に用いた資料です。

■ tenntenn Conferenceとは
tenntenn Conferenceはtenntennが主催し、そしてすべてのセッションがtenntennによる登壇のカンファレンスです。

イベントページ:https://tenntenn.connpass.com/event/226562/
ハッシュタグ:#tennconn
資料(Googleスライド):https://tenn.in/i18n
動画:https://tenn.in/i18n-video
再生リスト:https://tenn.in/conn22-videolist

■ 内容
このセッションではリリース済みのサービスにおける多言語化について考察します。また、Goで多言語化を行うための基礎的な知識について解説を行います。静的解析を使って多言語化を簡単にする方法についても紹介しています。

・多言語化
・golang.org/x/text/language
・静的解析

■ 登壇者&主催者

・名前:tenntenn / 上田拓也
・HP:https://tenntenn.dev
・Twitter:https://twitter.com/tenntenn

メルカリ/メルペイ所属。バックエンドエンジニアとして日々Goを書いている。Google Developer Expert (Go)。一般社団法人Gophers Japan代表。Go Conference主催者。大学時代にGoに出会い、それ以来のめり込む。人類をGopherにしたいと考え、Goの普及に取り組んでいる。複数社でGoに関する技術アドバイザーをしている。マスコットのGopherの絵を描くのも好き。

■ Gopher道場 自習室

https://gopherdojo.org/studyroom/

Gopher道場とは、実践的なGoを体系的に学べる場です。
Gopher道場 自習室では、以下のようなコンテンツや学びの場を提供します。

・Gopher道場の講義を録画した動画(10時間以上分)
・Slackにおける受講者同士のコミュニティ
・Gopher道場卒業生による課題のレビュー(ボランティアでご協力頂いているのでベストエフォートです)

■ Meety(カジュアル面談)

・ソフトウェアエンジニアの地方移住ってどうなの?:https://meety.net/matches/jyZgDkEEwmMk
・メルカリグループにおけるGoの使いどころ:https://meety.net/matches/LbeVbIACxLqk
・地方からの技術コミュニティへの貢献:https://meety.net/matches/gVeMtImLkWJE

■ お仕事の依頼について

副業にて技術顧問やアドバイザーなどを行っています。過去の実績や問い合わせフォームは以下のURLからご確認ください。
https://tenntenn.dev/ja/job/

#golang #tenntenn #tennconn #Go言語

tenntenn - Takuya Ueda

January 15, 2022
Tweet

More Decks by tenntenn - Takuya Ueda

Other Decks in Technology

Transcript

  1. 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
  2. 上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert

    (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team
  3. 多言語化は難しい ▪ 翻訳だけではない • 右から読むか左から読むか • 通貨 • タイムゾーン •

    単位や表記方法 ◦ 例:数字の区切り(カンマかピリオド) • 法律 • ユーザごとに変わるかもしれない ◦ アメリカに住んでるけど、日本語で表示 • タイムゾーン、通貨、法律などはアメリカ、言語は日本語
  4. 翻訳だけでも難しい ▪ 翻訳フローをどうするのか • どうやってアップデートしていくか? • どうやって文言を管理するのか? ▪ どこまでやるか? •

    複数形 • 活用形 • 人によって代名詞が違う ◦ he, she, they, … ▪ 文字数が違う • 英語にするとフォントが小さくなりすぎる
  5. Goにおける多言語化 ▪ golang.org/x/text/languageパッケージ • https://pkg.go.dev/golang.org/x/text/language • 辞書管理機能 • Accept-Languageヘッダのパース •

    翻訳がない場合のフォールバック機能 ▪ go-i18n • https://technickcal.com/go-i18n/ • 多機能な多言語化のためのライブラリ • 複数形なども対応している • 文言を取り出す機能もついている ◦ アップデートが楽
  6. 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) }
  7. 文言の設定 ▪ 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)
  8. リリース済みサービスの多言語化 ▪ 静的解析を行い文字列定数を取得 • 静的単一代入形式に変換する • 値が文字列なものを列挙する ▪ 抜き出した文言をラップする •

    抜き出した文言にキーを割り当てる • TODO("文言", "キー", "コメント")のような関数でラップする ◦ 文言をそのまま返す関数 ▪ 文言とキーが正しくマッチするかテストする • TODO関数でラップされてる部分を静的解析で抜き出す • 文言とキーが正しい組み合わせか確認する ▪ TODO関数を置換していく • S(言語, "キー")のような関数呼び出しに置換する
  9. ソースコードから文字列を取得 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 })
  10. 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)
  11. まとめ ▪ 多言語化は難しい • 途中からやるのは難しい ◦ 事前に織り込んでおくと楽 • やり方はいろいろある ◦

    文言自体を辞書キーにするパターンもある • DB内の文言はマイグレーションが必要 • プッシュ通知とか大変 ◦ 受け取り手の言語設定によって分けるか? • アプリ側でも対応が必要