Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@BlueEventHorizon

Go言語でiOS開発ツールを作成する:Localizable.stringsファイルからenumを生成する

More than 1 year has passed since last update.

はじめに

 Go言語でiOS開発ツールを作成する というモチベーションは、まずGo言語が自分にとってとても興味深い言語であるいうところから始まっています。もともとLinux上でC言語を使って組込製品などを作ってきた人間にとっては、Go言語はすごく分かりやすくて便利な言語だという感触がありました。しかも高速(らしい)です。
 実は以前にSwiftでMacで動くiOS開発ツールは作った経験があるのですが、今回Go言語でチャレンジした感想は、Swiftに比べてもGo言語はとても優れているということです。(特にMacOSやLinuxで動くツールを作成する場合)

目標

Localizable.strings

"clode_window" = "閉じる";
"delete something" = "削除する";
"cancel" = "キャンセル";
"OK" = "OK";
"_Only_this_" = "Only this";

例えば上記のような Localizable.strings から下記のようなswiftファイルを生成します。

LocalizableStrings.swift

import Foundation

enum LocalizableStrings: String {
    case clodeWindow = "clode_window",
    case deleteSomething = "delete something",
    case cancel = "cancel",
    case OK = "OK",
    case OnlyThis = "_Only_this_",
}

環境構築

go言語の開発環境の構築に関してはいろいろなところで語られているので、詳細は述べません。
ただ、vscodeを使った開発環境はとても良いです。基本的な機能ではありますが、break pointが張れたりするのが嬉しいです。

macの場合は、HomeBrewを使ってコマンドラインの環境も構築しておきます。

terminal
$ brew install go
$ go version

GO言語は、GOPATH で指定されたパスに、ライブラリーや実行ファイルをダウンロードします。
go version 1.8以降はしてされない場合は、ホームディレクトリーの go ディレクトリーが使われます。

.bashrc
$ export GOPATH=$HOME"/go"
$ export PATH="$GOPATH/bin:$PATH"

VSCodeでGoのデバッグをするのにdelveをインストールしておきます。

terminal
$ go get -u github.com/derekparker/delve/cmd/dlv

Go言語で実装

コマンドライン引数を取得する

まずコマンドライン引数を取得する必要があります。

go
flag.Parse()
topDir := flag.Arg(0)
enumName := flag.Arg(1)

flag.Parse()で、コマンドライン引数を拾うことができます。Swiftで同じ機能を書くより簡単で、C言語より分かりやすいです。
fmt.Printfや、fmt.SprintfもC言語に慣れていれば、容易に理解できます。

ディレクトリーを再帰的にスキャンする

ディレクトリーを再帰的にスキャンするコードは以下のように書けます。
この関数は、ディレクトリーを再帰的にスキャンしながら、
1)関係ないディレクトリーはスキップし、
2)analyzerという関数コールバックを実行していきます。
3)そしてtextsというスライスの参照に対して結果を書き込んでいきます。

textsは文字列スライスですので、参照ししなくては、呼び出し元に結果を返すことはできません。

go
package main

import (
    "io/ioutil"
    "path/filepath"
    "strings"
)

func Scandir(dir string, analyzer func(string, *[]string), texts *[]string) {
    files, err := ioutil.ReadDir(dir)
    if err != nil {
        panic(err)
    }

    for _, file := range files {
        name := file.Name()
        path := filepath.Join(dir, name)
        if file.IsDir() {
            // . xcodeprojディレクトリはスキャンしない
            if strings.HasSuffix(name, ".xcodeproj") {
                continue
            }
            // .xcworkspaceディレクトリはスキャンしない
            if strings.HasSuffix(name, ".xcworkspace") {
                continue
            }
            // buildディレクトリはスキャンしない
            if name == "build" {
                continue
            }
            // Carthageディレクトリはスキャンしない
            if name == "Carthage" {
                continue
            }
            // Podsディレクトリはスキャンしない
            if name == "Pods" {
                continue
            }
            Scandir(filepath.Join(path), analyzer, texts)
            continue
        }

        analyzer(path, texts)
    }
}

Localizable.stringsを解析する

Localizable.strings だけではなく、ファイル拡張子が .strings であれば解析を行います。
ここでは、
1)スペースを取り除き
2)// 以降はコメントなので削除
3)= が存在し、最後が ; であれば先頭の "で囲まれた領域を文字列スライスに格納します。

go
package analyzer

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func AssetAnalyzer(path string, texts *[]string) {
    // xxx.strings のみを解析
    if !strings.HasSuffix(path, ".strings") {
        return
    }

    // ファイルをOpenする
    file, err := os.Open(path)
    // 読み取り時の例外処理
    if err != nil {
        fmt.Println("error")
    }
    // 関数が終了した際に確実に閉じるようにする
    defer file.Close()

    sc := bufio.NewScanner(file)
    for i := 1; sc.Scan(); i++ {
        if err := sc.Err(); err != nil {
            // エラー処理
            break
        }

        text := sc.Text()
        text = strings.TrimSpace(text)
        index := strings.Index(text, "//")
        if index > 0 {
            // コメントを削除する
            text = text[:index]
            text = strings.TrimSpace(text)
        }

        // 構文を検査し、 `=` から後ろ、 `"` 、空白を削除 
        if strings.HasSuffix(text, ";") {
            index := strings.Index(text, "=")
            if index > 0 {
                text = text[:index]
                text = strings.TrimSpace(text)
                text = strings.Trim(text, "\"")
            }

            // 既に同じキーがスライスに存在すれば、result = true 
            var result bool = false
            for _, element := range *texts {
                if element == text {
                    result = true
                    break
                }
            }

            if result == false {
                // 初めてのキーなので、スライスに格納
                *texts = append(*texts, text)
            }
        }
    }
}

キャメルケースの文字列を生成する

空白は全て _ に変換しておきます。その上で下記の関数に通すと、スネークケースをキャメルケースに変換することができます。(説明いらないですね・・・)

go

func convertToCamelCase(text string) string {
    var keyword string
    var foundUnderScore = false
    for i := 0; i < len(text); i++ {
        letter := text[i : i+1]
        if letter == "_" {
            foundUnderScore = true
            continue
        }
        if foundUnderScore {
            foundUnderScore = false
            keyword = keyword + strings.ToUpper(letter)
        } else {
            keyword = keyword + letter
        }
    }

    return keyword
}

main文を書く

最後にmain文をあげておきます。

go
package main

import (
    "flag"
    "fmt"
    "strings"

    "./analyzer"
)

func main() {
    flag.Parse()
    topDir := flag.Arg(0)
    enumName := flag.Arg(1)
    if topDir == "" {
        topDir = "./"
    }
    if enumName == "" {
        enumName = "LocalizableStrings"
    }

    output(fmt.Sprintf("import Foundation\n\n"))
    output(fmt.Sprintf("enum %s: String {\n", enumName))

    texts := make([]string, 100, 500)
    Scandir(topDir, analyzer.LocalisableStringsAnalyzer, &texts)
    for _, text := range texts {
        if text == "" {
            continue
        }
        // 空白はアンダースコアに置換
        keyword := strings.Replace(text, " ", "_", -1)
        keyword = convertToCamelCase(keyword)
        output(fmt.Sprintf("    case %s = \"%s\",\n", keyword, text))
    }
    output(fmt.Sprintf("}\n"))
}

func output(text string) {
    fmt.Print(text)
}

GitHub

GitHubにて継続して開発中です。

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
BlueEventHorizon
⚔️ios engineer ❤️science fiction 💚XCode 🌲innovation and entrepreneurship 🏃‍♂️golang beginner
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?