メタデータAPIを使って翻訳データを抽出してみた

はじめに

皆さんはSalesforceの翻訳データを見るときに何を使っていますか?
一般的にはSalesforceの設定画面から言語を選び、コンポーネントを選んで言語ごとに選ぶか、エクスポートしてstfファイルを確認するケースが多いかと思います。

国際化対応をしていくと、各言語の翻訳データを一覧で見ることが多々あるので、APEXでどうにかできないか色々調べてみました。

翻訳データ抽出方法

翻訳データはメタデータAPIを使うことで抽出できそうです。
※メタデータ API 開発者ガイド を参照
なので今回はメタデータAPIをつかって一覧表示をしてみたいと思います。

全体の流れ

1.事前準備
2.メタデータからマスター値の取得
3.メタデータから翻訳データの取得

という形で処理を進めます。

1.事前準備

まずは、メタデータ取得用、画面表示用などに使うインナークラス系を定義します。
各クラスの定義

・LangTypePickParam マスターラベルと翻訳値の選択リスト値を格納するクラス
・MetadataPort メタデータを読み込むためのクラス
・ReadResult readMetadata コールの結果情報インターフェイス
・ReadResponseElement メタデータのResponseインターフェイス
・SessionHeader_element セッションヘッダクラス
・ReadMetadata_element メタデータコンポーネント読み取りクラス
・ReadTranslationsResponse_element ReadTranslationsResponse_element
・ReadTranslationsResult 結果情報を取得クラス
・Metadata すべてのメタデータ型の基本クラス
・Translations 使用言語翻訳クラス
・GlobalPicklist グローバル選択リストメタデータクラス
・GlobalPicklistValue グローバル選択リスト値クラス
・GlobalPicklistTranslation グローバル選択リストの翻訳

今回未使用だがメタデータ取得時に定義しておかなければいけないクラス
・CustomApplicationTranslation カスタムアプリケーション翻訳クラス
・CustomLabelTranslation カスタム表示ラベル翻訳クラス
・CustomTabTranslation カスタムタブ翻訳クラス
・PicklistValueTranslation 選択リスト値翻訳クラス
・GlobalQuickActionTranslation グローバルなクイックアクション翻訳クラス

インナークラスサンプルコード

    public class LangTypePickParam {
        public String master {get; private set;}
        public String ja {get; private set;}
        public String en_US {get; private set;}
        public String zh_CN {get; private set;}
    }
    
    public class MetadataPort {
        public String endpoint_x = URL.getSalesforceBaseUrl().toExternalForm() + '/services/Soap/m/37.0';
        public Map<String,String> inputHttpHeaders_x;
        public Map<String,String> outputHttpHeaders_x;
        public String clientCertName_x;
        public String clientCert_x;
        public String clientCertPasswd_x;
        public Integer timeout_x;
        public SessionHeader_element SessionHeader;

        private String SessionHeader_hns = 'SessionHeader=http://soap.sforce.com/2006/04/metadata';
        private String AllOrNoneHeader_hns = 'AllOrNoneHeader=http://soap.sforce.com/2006/04/metadata';
        private String[] ns_map_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata', 'Translation'};
    
        public ReadResult ReadMetadata(String type_x,String[] fullNames) {
            ReadMetadata_element request_x = new ReadMetadata_element();
            request_x.type_x = type_x;
            request_x.fullNames = fullNames;
            ReadResponseElement response_x;
            Map<String, ReadResponseElement> response_map_x = new Map<String, ReadResponseElement>();
            response_map_x.put('response_x', response_x);
            WebServiceCallout.invoke(
              this,
              request_x,
              response_map_x,
              new String[]{endpoint_x,
              '',
              'http://soap.sforce.com/2006/04/metadata',
              'readMetadata',
              'http://soap.sforce.com/2006/04/metadata',
              'readMetadataResponse',
              'Translation2Controller.read' + type_x + 'Response_element'}
            );
            response_x = response_map_x.get('response_x');
            return response_x.getResult();
        }
    }

    public interface ReadResult {
       Metadata[] getRecords();
    }
    
    public interface ReadResponseElement {
        ReadResult getResult();
    }

    public class SessionHeader_element {
        public String sessionId;
        private String[] sessionId_type_info = new String[]{'sessionId','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'sessionId'};
    }
    
    public class ReadMetadata_element {
        public String type_x;
        public String[] fullNames;
        private String[] type_x_type_info = new String[]{'type','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] fullNames_type_info = new String[]{'fullNames','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'type_x','fullNames'};
    }

    public class ReadTranslationsResponse_element implements ReadResponseElement {
        public ReadTranslationsResult result;
        public ReadResult getResult() { return result; }
        private String[] result_type_info = new String[]{'result','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'result'};
    }

    public class ReadTranslationsResult implements ReadResult {
        public Translations[] records;
        public Metadata[] getRecords() { return records; }
        private String[] records_type_info = new String[]{'records','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'records'};
    }
    
    // 以下すべてのクラスは、ドキュメント「メタデータAPI開発者ガイド」に記載
    /**
     * すべてのメタデータ型の基本クラス
     */
    public virtual class Metadata {
        public String fullName;
    }
    
    /**
     * Metadata メタデータ型を拡張し、そのfullName 項目を継承します。
     */
    public class Translations extends Metadata {
        public String type = 'Translations';
        public String fullName;
        private String[] fullName_type_info = new String[]{'fullName','http://soap.sforce.com/2006/04/metadata',null,'0','1','false'};
        
        // カスタムアプリケーション翻訳のリスト
        public CustomApplicationTranslation[] customApplications;
        
        // カスタム表示ラベル翻訳のリスト
        public CustomLabelTranslation[] customLabels;
        
        // カスタムタブ翻訳のリスト
        public CustomTabTranslation[] customTabs;
        
        // グローバル選択リストのリスト
        public GlobalPicklistTranslation[] globalPicklists;
        
        // グローバル (オブジェクト固有ではない) クイックアクションのリスト
        public GlobalQuickActionTranslation[] quickActions;
        private String[] customApplications_type_info = new String[]{'customApplications','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] customLabels_type_info = new String[]{'customLabels','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] customTabs_type_info = new String[]{'customTabs','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] globalPicklists_type_info = new String[]{'globalPicklists','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] quickActions_type_info = new String[]{'quickActions','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] type_att_info = new String[]{'xsi:type'};
        private String[] field_order_type_info = new String[]{'fullName', 'customApplications','customDataTypeTranslations','customLabels','customPageWebLinks','customTabs','globalPicklists','quickActions','reportTypes','scontrols'};
    }

    /**
     * グローバル選択リストのメタデータ
     */
    public class GlobalPicklist extends Metadata {
        public String type = 'GlobalPicklist';
        public String fullName;
        private String[] fullName_type_info = new String[]{'fullName','http://soap.sforce.com/2006/04/metadata',null,'0','1','false'};
        public String description;
        public GlobalPicklistValue[] globalPicklistValues;
        public String masterLabel;
        public Boolean sorted;
        private String[] description_type_info = new String[]{'description','http://soap.sforce.com/2006/04/metadata',null,'0','1','false'};
        private String[] globalPicklistValues_type_info = new String[]{'globalPicklistValues','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] masterLabel_type_info = new String[]{'masterLabel','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] sorted_type_info = new String[]{'sorted','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] type_att_info = new String[]{'xsi:type'};
        private String[] field_order_type_info = new String[]{'fullName', 'description','globalPicklistValues','masterLabel','sorted'};
    }

    public virtual class GlobalPicklistValue extends Metadata {
        public String color;
        public Boolean default_x;
        public String description;
        public Boolean isActive;
    }
    
    public class CustomApplicationTranslation {
        public String label;
        public String name;
        private String[] label_type_info = new String[]{'label','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] name_type_info = new String[]{'name','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'label','name'};
    }
    
    public class CustomLabelTranslation {
        public String label;
        public String name;
        private String[] label_type_info = new String[]{'label','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] name_type_info = new String[]{'name','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'label','name'};
    }
    
    public class CustomTabTranslation {
        public String label;
        public String name;
        private String[] label_type_info = new String[]{'label','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] name_type_info = new String[]{'name','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'label','name'};
    }

    public class GlobalPicklistTranslation {
        public String name;
        public PicklistValueTranslation[] picklistValues;
        private String[] name_type_info = new String[]{'name','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] picklistValues_type_info = new String[]{'picklistValues','http://soap.sforce.com/2006/04/metadata',null,'0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'name','picklistValues'};
    }
    
    public class PicklistValueTranslation {
        public String masterLabel;
        public String translation;
        private String[] masterLabel_type_info = new String[]{'masterLabel','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] translation_type_info = new String[]{'translation','http://soap.sforce.com/2006/04/metadata',null,'0','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'masterLabel','translation'};
    }
    public class GlobalQuickActionTranslation {
        public String label;
        public String name;
        private String[] label_type_info = new String[]{'label','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] name_type_info = new String[]{'name','http://soap.sforce.com/2006/04/metadata',null,'1','1','false'};
        private String[] apex_schema_type_info = new String[]{'http://soap.sforce.com/2006/04/metadata','true','false'};
        private String[] field_order_type_info = new String[]{'label','name'};
    }

2.メタデータからマスター値の取得

まずはメタデータから選択リストのマスター値の取得をします。

        Translations master = 
            (Translations) service.readMetadata('Translations',langType).getRecords()[0];
        
        LangTypePickParam ltpp;
        List<LangTypePickParam> LangTypePickParamList;
        
        // 選択リストの項目名の種類だけループ
        for(GlobalPicklistTranslation GlobalPicklistTranslation : master.globalPicklists) {
            
            LangTypePickParamList = new List<LangTypePickParam>();

            // 選択リスト値の値の数分だけループ
            for(PicklistValueTranslation pickListValue : GlobalPicklistTranslation.picklistValues) {
                
                ltpp = new LangTypePickParam();
                ltpp.Master = pickListValue.masterLabel;
                LangTypePickParamList.add(ltpp);
            }

            if(!pickListMap.containsKey(GlobalPicklistTranslation.name)) {
                this.pickListMap.put(GlobalPicklistTranslation.name, LangTypePickParamList);
                this.pickListDecisionMap.put(GlobalPicklistTranslation.name, false);
            }
        }

先頭の
Translations master =
 (Translations) service.readMetadata('Translations',langType).getRecords()[0];
の処理のなかで、WebService経由でメタデータを取得しています。
※このデータは翻訳データが大量に入っているのでグローバル選択リストのデータのみを更新対象としています。

3.メタデータから翻訳データの取得

各言語毎にメタデータを読み取り、表示用PickListMapに追記する形で格納していきます。
この際、翻訳データが存在しないグローバル選択リストを表示させないために、翻訳値が存在しないデータはpickListDecisionMapにも格納します。

        for(Integer i = 0 ; i < langType.size() ; i++) {
            // メタデータから翻訳に関係する情報を取得する
            Translations translation;
            translation = (Translations) service.readMetadata('Translations',langType).getRecords()[i];

            // 選択リストの項目名の種類だけループ
            for(GlobalPicklistTranslation GlobalPicklistTranslation : translation.globalPicklists) {
                if(pickListMap.containsKey(GlobalPicklistTranslation.name)) {
                    transDecisionFlag = false;
                    for(Integer j = 0 ; j < GlobalPicklistTranslation.picklistValues.size() ; j++) {
                        ltpp = new LangTypePickParam();
                        ltpp = pickListMap.get(GlobalPicklistTranslation.name).get(j);
                        if(langType[i] == 'ja') {
                            ltpp.ja = GlobalPicklistTranslation.picklistValues[j].translation;
                            if(!String.isBlank(ltpp.ja)) {
                                transDecisionFlag = true;
                            }
                        } else if(langType[i] == 'en_US') {
                            ltpp.en_US = GlobalPicklistTranslation.picklistValues[j].translation;
                            if(!String.isBlank(ltpp.en_US)) {
                                transDecisionFlag = true;
                            }
                        } else if(langType[i] == 'zh_CN') {
                            ltpp.zh_CN = GlobalPicklistTranslation.picklistValues[j].translation;
                            if(!String.isBlank(ltpp.zh_CN)) {
                                transDecisionFlag = true;
                            }
                        }
                        this.pickListMap.get(GlobalPicklistTranslation.name).set(j,ltpp);
                        if(transDecisionFlag) {
                            this.pickListDecisionMap.put(GlobalPicklistTranslation.name, transDecisionFlag);
                        }
                    }
                }
            }
        }

こうして、PickListMapに表示用の選択リストの翻訳データが格納されました。

これをページ側で表示すると以下のように表示されます。
画面

<apex:page controller="Translation2Controller" cache="false" id="thePage">
    <apex:outputPanel id="errPnl">
        <apex:pageMessages escape="false" />
    </apex:outputPanel>
    <apex:form >
        <apex:actionStatus onstart="blockUI()" onstop="unblockUI()" id="loading"/>
        <apex:pageBlock title="グローバル選択リスト">
            <table class="tbl">
                <caption><b>グローバル選択リスト翻訳テーブル</b></caption>
                <tr>
                    <th>グローバル選択リスト名</th>
                    <th>MasterLabel</th>
                    <th>日本語</th>
                    <th>英語</th>
                    <th>中国語</th>
                </tr>
                <apex:repeat value="{!pickListMap}" var="key">
                    <apex:outputPanel rendered="{!pickListDecisionMap[key]}">
                    <tr>
                        <td bgcolor="#FFFFFF">
                            {!key}
                        </td>
                        <td bgcolor="#FFFFFF">
                            <apex:repeat value="{!pickListMap[key]}" var="val">
                                <apex:outputText value="{!val.master}<br/>" escape="false"/>
                            </apex:repeat>
                        </td>
                        <td bgcolor="#FFFFFF">
                            <apex:repeat value="{!pickListMap[key]}" var="val">
                                <apex:outputText value="{!val.ja}<br/>" escape="false"/>
                            </apex:repeat>
                        </td>
                        <td bgcolor="#FFFFFF">
                            <apex:repeat value="{!pickListMap[key]}" var="val">
                                <apex:outputText value="{!val.en_US}<br/>" escape="false"/>
                            </apex:repeat>
                        </td>
                        <td bgcolor="#FFFFFF">
                            <apex:repeat value="{!pickListMap[key]}" var="val">
                                <apex:outputText value="{!val.zh_CN}<br/>" escape="false"/>
                            </apex:repeat>
                        </td>
                    </tr>
                    </apex:outputPanel>
                </apex:repeat>
            </table>
        </apex:pageBlock>
    </apex:form>
</apex:page>

まとめ

メタデータAPIを使って、無事APEX内で翻訳データを抜き取って表示することができました。
これを応用すれば、このデータをカスタムオブジェクトにマスター化することで、データ連携時に複数の翻訳されているデータを他システムに連携したり、Apexで特定の言語で翻訳したデータをレコードに書き込むなどに使えるのではないかなあと思います。