🗃️

go言語で外部のjsonファイルを扱おう!便利にね。(目的は多言語化の下地)

2022/03/26に公開

まえがき

はい。この記事はとても重要です。プログラマーには癖というものがどうしても各自あります。
このとき色んな環境下において、これまで幾多と動いてきたのになんで動かないの?ってことがあります。私はこのgolangで無駄にはまってしまいました。でもはまっていたおかげで、右往左往しますね?その右往左往のおかげで、いままでチマチマ手で構築していたものをサクッとできる便利サービスにも出会いました。そんな日々挑戦を続ける皆さまに贈る言葉です。これはキミの物語だ!

目的は多言語化

多言語化する上で、パッケージとかあったんですが、まぁ大規模でもないしいれてみたけど、腑に落ちなくて1からやってみることにしました。構成としては以下になります。

lang.json
{
    "tool": {
        "ja": "ツール",
        "en": "Tool",
        "cn": "ツール"
    },
    "game": {
        "ja": "ゲーム",
        "en": "Game",
        "cn": "ゲーム"
    },
    "media": {
        "ja": "メディア",
        "en": "Media",
        "cn": "メディア"
    },
    "lab": {
        "ja": "ラボ",
        "en": "Lab",
        "cn": "ラボ"
    },
    "lang": {
        "ja": "Lang",
        "en": "Lang",
        "cn": "Lang"
    }
}

あなたのjson大丈夫?

私の癖といいますか、jsonは手でゴリゴリ書いて、構文チェックもjsなんで、ブラウザでチェックしてそれでOKということが多かったです。それ以外ではPHPでしょうか。今回はjsonのチェックをしてくれるサービスです。これが地味に私にとって重要でした。

https://lab.syncer.jp/Tool/JSON-Viewer/

ま、他にもjson構文チェックのWebサービスはあると思います。では何が私にとって重要だったかといいますと、空白をタブにしてしまうことです。恐らくですが、今回のgoパッケージはjsonのスペースは意識してもタブで余白を空けるというケースを意識してないのだと思います。どんなパッケージかって「import "encoding/json"」の「json.Unmarsha」です。このWebサービスはコピー用に半角スペース版で出力しなおしてくれます。これをつかって気づきました。json構文に問題がないのになぜ動かないのか数時間別のところに問題があると思って、いろいろ調べてしまいました。

goの型宣言は手間だけど便利なサービスがある

これまで私はgoの型宣言は入れ子だと面倒だなーという認識でした。最近は他の言語もそうですけどね。手動でチマチマうっとおしいなーって。でもありますね。便利なのが。これです。

https://mholt.github.io/json-to-go/

こんな書き方ができるんですね(笑)

結果はこれ

type AutoGenerated struct {
	Tool struct {
		Ja string `json:"ja"`
		En string `json:"en"`
		Cn string `json:"cn"`
	} `json:"tool"`
	Game struct {
		Ja string `json:"ja"`
		En string `json:"en"`
		Cn string `json:"cn"`
	} `json:"game"`
	Media struct {
		Ja string `json:"ja"`
		En string `json:"en"`
		Cn string `json:"cn"`
	} `json:"media"`
	Lab struct {
		Ja string `json:"ja"`
		En string `json:"en"`
		Cn string `json:"cn"`
	} `json:"lab"`
	Lang struct {
		Ja string `json:"ja"`
		En string `json:"en"`
		Cn string `json:"cn"`
	} `json:"lang"`
}

あぁ便利極まりないです。

ちなみに多言語化は1ファイルに

いろいろ検索しましたが、こじんまりしたサイトで言語ごとファイル分ける必要もないなっておもって、キャプチャの様な構成にしました。

書き方はこのままにはしません

今回はja, en, cnと入れ子の方が統一なので、これは参考として以下のように圧縮しました。

type I18n struct {
	Tool I18nnest `json:"tool"`
	Game I18nnest `json:"game"`
	Media I18nnest `json:"media"`
	Lab I18nnest `json:"lab"`
	Lang I18nnest `json:"lang"`
}

type I18nnest struct {
	Ja string `json:"ja"`
	En string `json:"en"`
	Cn string `json:"cn"`
}

欠点としては単語が増えるたびに型も追加しないといけないところですね。

ある意味本題の外部jsonを読む書き方

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"os"
	"fmt"
)

type I18n struct {
	Tool I18nnest `json:"tool"`
	Game I18nnest `json:"game"`
	Media I18nnest `json:"media"`
	Lab I18nnest `json:"lab"`
	Lang I18nnest `json:"lang"`
}

type I18nnest struct {
	Ja string `json:"ja"`
	En string `json:"en"`
	Cn string `json:"cn"`
}

func main() {
...
...
	//多言語json
	json_lang_file, err := ioutil.ReadFile("/PATH!!!!/lang.json")
	if err != nil {
		log.Println("ReadError: ", err)
		os.Exit(1)
	}
	//fmt.Println(string(json_lang_file))
...

これで、まず読めます。が、string(json_lang_file)と、あるとおり文字列です。
なので作った型でjson.Unmarshalします。

...
		var langlist I18n
		json.Unmarshal(json_lang_file, &langlist)
		fmt.Println(langlist.Tool.Ja)
		fmt.Println(langlist.Tool)
		fmt.Println(langlist.Lab.Ja)
...
ツール
{ツール Tool ツール}
ラボ

みたいに出力されます。

おまけ?

言語を一つのjsonにしましたが、これはGO言語をシンプルにする場合は、分けた方がキレイかもしれません。例えばgoにはtemplateで実行できる関数を設置できますが...まるでphpのsmartyみたいに。参考を貼ります

https://blog.narumium.net/2019/04/23/【go】ginのhtmlテンプレート機能/

こっちではnl2brの例があります。
https://zenn.dev/ajapa/articles/6471ac0c612fda

でもそれを今回の場合は multitemplate 使ってまして、go template の FuncMap を使うために、ginでは router.SetFuncMap すると教わるけどさらに multitemplate の場合は、

https://github.com/gin-contrib/multitemplate/issues/2

func createRender() multitemplate.Renderer {
	r := multitemplate.NewRenderer()
...
	//r.AddFromFiles("templateA", ...
	r.AddFromFilesFuncs("templateA", template.FuncMap{ "nl2br": nl2br, "loudLang": loudLang }, "templates/***", ...)
...

はい、いちいちFuncMapいれます。いいんですけどね...なかなかですよね。
これをする理由はgo templateはmapとかのキーを指定するとき変数で代入見たいのが出来ません。例えばrangeでループさせるとかで対応できますが、テンプレート側がうるさくなってしまいます。そこで、

func loudLang(word I18nnest, lang string) string {
	if lang == "Ja" {
		return word.Ja
	} else if lang == "En" {
		return word.En
	} else if lang == "Cn" {
		return word.Cn
	} else {
		return word.Ja
	}
}

こうして、上から読んでもラうと分かりますが、入れ子の型(I18nnest)を引数にいれてもらって、あとは言語の引数をif文で判定します。これをテンプレ側で呼べると

template/aaa.html
{{ loudLang .langlist.Tool .lang }}

こうです。分かりますか?loudLangが関数で.langlistは外部から読み込んだjsonのオブジェクトをテンプレに渡した変数です。.langlist.Toolとすることで、ネストした部分だけ第1引数にいれています。第2引数は言語の指定ですね。一応これで動きます。

まとめ

癖でjsonのスペースをタブにしていたので悪戦苦闘してしまった。
でも、jsonチェックツールを通すことを学んだ。
そして、型を出力してくれる便利ツールに出会った。
さらに、避けていたテンプレ側の関数を設置した。
そんなところです。

Discussion