LoginSignup
7
8

More than 5 years have passed since last update.

GooglePlayの掲載情報のローカライゼーションを自動化する

Last updated at Posted at 2017-03-25

はじめに

GooglePlayのタイトル説明文は検索順位に大きな影響を及ぼすことでよく知られています。ターゲットを日本人に絞っている場合を除いて、ほとんどの開発者が英語日本語の説明文を用意していると思います。ユーザーは母国語でアプリを検索するので、タイトル説明文をローカライズすることはASO(App Store Optimization)の観点からとても意味のあることです。
予算に余裕があるなら Developer Console から翻訳を購入したりGengoなどのサービスを使ってプロの翻訳家に依頼したいところですが、小遣い制のサラリーマンにはなかなか難しいところです。
少しぐらいおかしな文章であったとしても、説明文にそれぞれの国の人が検索するであろうキーワードが含まれてさえいればASOとしては効果があるので、Google翻訳を使って翻訳した説明文をコピペする方法がローカライゼーションの第一歩になるでしょう。

Google翻訳を使ったローカライゼーションの効果

GooglePlayでのダウンロード数は検索ランキングやレビューサイトで紹介されたなど、様々な要因が影響するため、正確に効果を計測するのは難しいですが、私の場合、約100ダウンロード/日だったのが、説明文を69ヶ国語にローカライズしたことで約2倍の200ダウンロード/日に増やすことができました。効果はアプリのジャンルや実際の説明文などによって大きく変わってくると思いますが、私の観測では効果は間違いなくあります。

ローカライゼーションのデメリット

  • アプリがローカライズされていないという理由での低評価のレビューが増えます。
  • 英語以外の言語で問い合わせが増えるのでサポートのコストが増えます。
  • Google翻訳しても意味の理解できないレビューが増えます。それがバグ報告だったり要望だったりすると気になって眠れません。日本語に翻訳するよりも英語に翻訳したほうが意味が分かることも多いです。
  • アプリがRTL言語に対応していないと、苦情のレビューが増えます。

rating.png

私がリリースしているのは電卓アプリなので、基本的に世界中の誰でも使える説明不要なアプリです。複雑な操作や設定などが必要なアプリでは、アプリがローカライズされていないことで、より酷いレビューが増える可能性があるので注意したほうがいいでしょう。

問題点

ローカライズの対象となる言語が多すぎて手作業でやるのは非常に大変です。英語、日本語を除いた対象となる言語は実に69言語にのぼります。さらに私のリリースしている電卓アプリ CalcNoteCalcNotePro は無料版と有料版でアプリを分けているので作業量はさらに2倍になります。一度全てを手作業でやってみましたが、トータルで1時間以上かかり泣きそうになりました。

解決策

Googleが提供しているAPIを使うことで翻訳からストアの掲載情報の更新まで全て自動化することが可能です。自動化の手順は以下の3ステップから構成されます。

  1. Google Spreadsheetで翻訳
  2. Google Sheets API v4を使ってプログラムから翻訳されたテキストを取得して加工
  3. Android Publisher APIを使ってGooglePlayの掲載情報を更新

今回はビルドツールにMavenを使ってコンソールから実行するタイプのJavaアプリとして作成しましたが、SDKは様々な言語用に提供されているので、使い慣れた環境のものを使うのが良いでしょう。
それでは各ステップを順番に見ていきます。

1.翻訳

翻訳にはGoogle SpreadsheetGOOGLETRANSLATE関数を使います。今回、私が実際に使用したSpreadsheetをサンプルとして共有しておきます。
シートにある各行の意味は以下の通りです。

  • B列にあるのが翻訳のベースとなる英語の説明文です。この内容がそれぞれの言語に自動翻訳されます。今回は翻訳後のテキストだけでは不安なので、翻訳後のテキストとベースの英語の説明文も併用して表示するようにします。
  • GoogleTranslate-langGOOGLETRANSLATE関数の引数に指定する言語コードです。
  • GooglePlay-langAndroid Publisher APIで掲載情報を更新する時に指定する言語コードです。
  • langは言語の説明でプログラムからは使用しません。
  • Title-Length, Short Description-Length, Long Description-Lengthはそれぞれタイトル簡単な説明詳しい説明の文字数です。それぞれ30文字80文字4000文字という制限があります。
  • Titleは掲載情報のタイトルです。Google翻訳で自動化すると簡単に30文字の制限を超えてしまうので、GOOGLETRANSLATE関数を使わず手作業で調整しました。(電卓という意味のキーワードが必ず含まれるように調整しました)
  • Short Descriptionは掲載情報の簡単な説明です。Google翻訳で自動化すると簡単に80文字の制限を超えてしまうので、GOOGLETRANSLATE関数を使わず手作業で調整しました。
  • Long Description1Long Description8は掲載情報の詳しい説明です。8個に分割しているのは意図せぬ翻訳を避けるためです。例えばLong Description2Long Description5strongタグを使って強調表示したいのですが、これをGOOGLETRANSLATE関数に渡すとstrongタグも翻訳されてしまい強調タグとして機能しなくなります。

ちなみにこのGOOGLETRANSLATE関数を使った自動翻訳は私が考案したわけではなく、TwitterのTLに流れてきた情報を参考にしただけです。最初にこれを考えた人は天才ですね。感謝です。

自動化の準備

Service Accountの作成と権限の付与

自動化するにあたり、プログラムからGoogle SpreadsheetGooglePlay Developer ConsoleにアクセスできるService Accountを作成する必要があります。

  1. ここを参考にして新しいプロジェクトを作りサービスアカウントを作成します。今回はCalcNoteTranslationというプロジェクト名にしました。
  2. 認証情報のJSONファイルをダウンロードします。このファイルはパブリックなバージョン管理システムにコミットしないように注意してください。
  3. Google Play Developer Consoleにアクセスして、先ほど作成したサービスアカウントにリリースマネージャーの権限を付与します。

依存ライブラリの追加

Google SpreadsheetGooglePlay Developer Consoleにアクセスする為のライブラリを依存関係に追加します。

pom.xml
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client</artifactId>
    <version>1.22.0</version>
</dependency>
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-sheets</artifactId>
    <version>v4-rev108-1.22.0</version>
</dependency>
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-androidpublisher</artifactId>
    <version>v2-rev40-1.22.0</version>
</dependency>
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client-gson</artifactId>
    <version>1.22.0</version>
</dependency>

2.Google Spreadsheetから翻訳文の取得と加工

以下のコードではGoogle Spreadsheetから言語毎に翻訳後のテキストを取得して、加工(改行の挿入、タグによる強調)して、ベースの英語の説明と結合して最終的な詳しい説明を生成しています。

SpreadsheetService.java
public class SpreadsheetService {

    private static final String APPLICATION_NAME = "CalcNoteTranslation";
    // SpreadsheetのIDはURLに書いてあります。
    private static final String SPREADSHEET_ID = "1wIyk1Re18cjUzLNm1XUi4nu4CaZh_IGFG37biddu1Eg";
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static final List<String> SCOPES = Arrays.asList(SheetsScopes.SPREADSHEETS_READONLY);

    private static final int ROW_GOOGLE_TRANSLATE_LANG = 0;
    private static final int ROW_GOOGLE_PLAY_LANG = 1;
    private static final int ROW_LANG = 2;
    private static final int ROW_TITLE_LENGTH = 3;
    private static final int ROW_SHORT_DESCRIPTION_LENGTH = 4;
    private static final int ROW_LONG_DESCRIPTION_LENGTH = 5;
    private static final int ROW_TITLE = 6;
    private static final int ROW_SHORT_DESCRIPTION = 7;
    private static final int ROW_LONG_DESCRIPTION_01 = 8;
    private static final int ROW_LONG_DESCRIPTION_02 = 9;
    private static final int ROW_LONG_DESCRIPTION_03 = 10;
    private static final int ROW_LONG_DESCRIPTION_04 = 11;
    private static final int ROW_LONG_DESCRIPTION_05 = 12;
    private static final int ROW_LONG_DESCRIPTION_06 = 13;
    private static final int ROW_LONG_DESCRIPTION_07 = 14;
    private static final int ROW_LONG_DESCRIPTION_08 = 15;

    private static HttpTransport HTTP_TRANSPORT;
    static {
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        } catch (Exception e) {
            HTTP_TRANSPORT = null;
        }
    }

    private final Sheets service;

    public SpreadsheetService() throws Exception {
        // credential.jsonは「自動化の準備」でダウンロードしたJSONファイルです。
        InputStream is = SpreadsheetService.class.getResourceAsStream("/credential.json");
        GoogleCredential credential = GoogleCredential.fromStream(is);
        credential = credential.createScoped(SCOPES);
        this.service = new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build();
    }
    public Collection<GooglePlayTranslation> getGooglePlayTranslations(Version version) throws IOException {
        ValueRange valueRange = service.spreadsheets().values().get(SPREADSHEET_ID, version.getSheetName() + "!B1:BS16").execute();
        List<List<Object>> rows = valueRange.getValues();
        Map<Integer, GooglePlayTranslation> translations = new TreeMap<Integer, GooglePlayTranslation>();

        for (int row = 0; row < rows.size(); row++) {
            List<Object> cols = rows.get(row);
            for (int col = 0; col < cols.size(); col++) {
                GooglePlayTranslation translation = translations.get(col);
                if (translation == null) {
                    translation = new GooglePlayTranslation();
                    translations.put(col, translation);
                }
                Object value = cols.get(col);
                switch (row) {
                case ROW_GOOGLE_PLAY_LANG:
                    translation.setLocale(value.toString().trim());
                    break;
                case ROW_TITLE:
                    translation.setTitle(value.toString().trim());
                    break;
                case ROW_SHORT_DESCRIPTION:
                    translation.setShortDescription(value.toString().trim());
                    break;
                case ROW_LONG_DESCRIPTION_02:
                case ROW_LONG_DESCRIPTION_05:
                    translation.addLongDescription("\n" + "<strong>" + value.toString().trim() + "</strong>");
                    break;
                case ROW_LONG_DESCRIPTION_04:
                case ROW_LONG_DESCRIPTION_07:
                    translation.addLongDescription("\n" + value.toString().trim());
                    break;
                case ROW_LONG_DESCRIPTION_01:
                case ROW_LONG_DESCRIPTION_03:
                case ROW_LONG_DESCRIPTION_06:
                case ROW_LONG_DESCRIPTION_08:
                    translation.addLongDescription(value.toString().trim());
                    break;
                }
            }
        }
        return translations.values();
    }
}
GooglePlayTranslation.java
public class GooglePlayTranslation {

    private static final int MAX_TITLE = 30;
    private static final int MAX_SHORT_DESCRIPTION = 80;
    private static final int MAX_LONG_DESCRIPTION = 4000;

    private String locale;
    private String title;
    private String shortDescription;
    private List<String> longDescriptions = new ArrayList<String>();
    public GooglePlayTranslation() {
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        if (title.length() > MAX_TITLE) {
            throw new IllegalArgumentException("Title is too long. length=" + title.length());
        }
        this.title = title;
    }
    public String getShortDescription() {
        return shortDescription;
    }
    public void setShortDescription(String shortDescription) {
        if (shortDescription.length() > MAX_SHORT_DESCRIPTION) {
            throw new IllegalArgumentException("Short Description is too long. length=" + shortDescription.length());
        }
        this.shortDescription = shortDescription;
    }
    public void addLongDescription(String longDescription) {
        this.longDescriptions.add(longDescription);
    }
    public String getLocale() {
        return locale;
    }
    public void setLocale(String locale) {
        this.locale = locale;
    }
    public boolean isEnBase() {
        return this.locale.startsWith("en-base");
    }
    public String getLongDescription(GooglePlayTranslation us) {
        StringBuilder sb = new StringBuilder();
        for (String longDescription : this.longDescriptions) {
            sb.append(longDescription + "\n");
        }
        sb.append("\n");
        sb.append("\n");
        sb.append("\n");
        for (String longDescription : us.longDescriptions) {
            sb.append(longDescription + "\n");
        }
        if (sb.length() > MAX_LONG_DESCRIPTION) {
            throw new RuntimeException("Long Description is too long -- length=" + sb.length());
        }
        return sb.toString();
    }
}

3.掲載情報の更新

翻訳されたアプリの掲載情報を更新するコードです。最終的にcommitしなければ変更は反映されないので途中でエラーが発生しても安心です。commitまで成功すると審査中の状態になります。

public class GooglePlayService {

    private static final String APPLICATION_NAME = "CalcNoteTranslation";
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static HttpTransport HTTP_TRANSPORT;

    static {
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        } catch (Exception e) {
            HTTP_TRANSPORT = null;
        }
    }

    private boolean DEBUG = false;
    private final AndroidPublisher service;

    public GooglePlayService() throws Exception {
        InputStream is = GooglePlayService.class.getResourceAsStream("/credential.json");
        GoogleCredential credential = GoogleCredential.fromStream(is);
        credential = credential.createScoped(AndroidPublisherScopes.all());
        this.service = new AndroidPublisher.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build();
    }

    public void update(Version version, Collection<GooglePlayTranslation> translations) throws Exception {
        GooglePlayTranslation base = null;
        Edits edits = this.service.edits();
        AppEdit edit = edits.insert(version.getPackageName(), null).execute();
        for (GooglePlayTranslation translation : translations) {
            if (translation.isEnBase()) {
                base = translation;
                continue;
            }
            if (base == null) {
                throw new RuntimeException("base language is null!!");
            }
            Listing listing = new Listing()
                .setTitle(translation.getTitle())
                .setShortDescription(translation.getShortDescription())
                .setFullDescription(translation.getLongDescription(base));
            edits.listings().update(version.getPackageName(), edit.getId(), translation.getLocale(), listing).execute();
            Thread.sleep(500);
        }
        Commit commit = edits.commit(version.getPackageName(), edit.getId());
        commit.execute();
        System.out.println("COMPLETED!!");
    }

}

その他のコード

CalcNoteはFree版とPro版で別のアプリとして提供しているので、Enumで定義しています。

Version.java
public enum Version {
    FREE("com.burton999.notecal", "Free"),
    PRO("com.burton999.notecal.pro", "Pro");
    private final String packageName;
    private final String sheetName;
    private Version(String packageName, String sheetName) {
        this.packageName = packageName;
        this.sheetName = sheetName;
    }
    public String getPackageName() {
        return packageName;
    }
    public String getSheetName() {
        return sheetName;
    }
}

最後にエントリーポイントのコードです。

Main.java
public static void main(String[] args) throws Exception {
    Version v = Version.PRO;
    SpreadsheetService spreadsheet = new SpreadsheetService();
    GooglePlayService googleplay = new GooglePlayService();
    Collection<GooglePlayTranslation> translations = spreadsheet.getGooglePlayTranslations(v);
    googleplay.update(v, translations);
}

最後に

ストアの掲載情報はアプリに新機能を追加した場合や、ASOを行う時に頻繁に更新することになるので自動化するメリットはとても大きいです。一度、頑張って仕組みを構築すれば他のアプリにも転用できるので非常にオススメです。

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8