ホームページ開発ツール>Xojo / Real Studio Trial and Error・純正とCocoaのDeclareで多言語化する

 Xojo / Real Studio Trial and Error

純正とCocoaのDeclareで多言語化する

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 多言語化について、試してみました。

 なお検証には、Xojo 2017 Release 1.1を用いています。(Mac mini 2018 + macOS 10.14.6 Mojave)


 方針

 Xojoは純正の機能として、多言語に対応しています。
 やり方は、(Xojo 2017 R1.1添付のドキュメントでは)UserGuide-Development.pdfのローカライゼーションの項(P55)に載っていて、その通りにすればできます。
 ドキュメントでは言語を直接指定していますが、現在のロケール(Xojo.Core.Locale.Current.Identifier)を取得して渡すことで、「システム環境設定>言語と地域」で指定した言語(順)で表示するようにもできます。

 純正の機能なので、本来はこれを使うべきなのでしょうが、作業は全てIDE上で行う必要があり(?)、量が多くなってくると、単純作業(マウスとキーボードを行ったり来たり)の繰り返しが苦痛になってくるかもしれません。

 一方、Cocoaにも多言語の仕組みはあって、それはNSLocalizedStringを使うものです。
 概要については、例えば以下のサイトに述べられています。(iOS向けかつSwiftかつXcodeベースですが、考え方は同一です。)

 参考サイト(1):[iOS] ゼロから始める!簡単!多言語対応! - Qiita

 この方式では、キー(共通)とその対となる文字列(各国語)を指定する訳ですが、キーがない場合は、キーがそのまま使われる(オプションで代替指定も可)ので、Xcodeでは英語が基本言語であることもあってか、英語をBaseとする、というのが主流のようです。

 Xcodeには関連ファイルを作成する機能がありますが、ここでは使えませんので、原則手作業で行うことになります。
 手順としてはまず、言語ごとのファイル(テキスト形式、デフォルトの名前はLocalizable.strings、書式は上記サイト等を参照)を作っておき、次にそれらを「ロケール名+.lproj」(英語の場合はen.lproj、日本語の場合はja.lproj、等々)としたフォルダに収め、Resourcesフォルダ内に配置しておきます。(注:デバッグ時は.appが常に新造されるので、起動直後にフォルダをコピーするステップを含ませておく。)
 すると、実行時に現在のロケールを自動判別して、対応する言語で表示される。ということのようです、

 参考サイト(2):Localizable.strings で選ばれるデフォルトの言語を設定する - bekkou68 の日記(「言語設定の仕組みと NSLocalizedString の挙動」の項)

 リソースバンドルの取得は、Xcodeでは、参考サイト(2)のように、
[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"];
 即ち、mainBundleに対する操作でいいようですが、Xojoでは実験した限り動作しなかったので、mainBundleからResourcesフォルダのパスを取得後、改めてResourcesフォルダをBundleとして取得するようにしたところ、うまくいくようになりました。

 最後は、両者を併用するハイブリッドについてです。
 量も少なく、あまりいじることもないものは純正、量が多く、追加/削除が頻繁に起こったり、用語の統一を意識するようなものはNSLocalizedStringを使うという方法です。

 気をつけるのは、Xojo純正もNSLocalizedStringと同じファイル/フォルダ構成を使うという点です。
 Localizable.stringsを共用することも考えられますが、書式が異なることや、NSLocalizedStringが任意のファイルにも対応していることから、別ファイルにした方が無難と思われます。

 これらを踏まえて今回は、(1)純正、(2)NSLocalizedString、(3)ハイブリッド、の3種類を試してみることにしました。
 それぞれの仕様は以下の通りとしました。

 共通
 例1(純正)
 例2(NSLocalizedString)
 例3(ハイブリッド)

 Xojoでの実装(例1)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
  1. Xojoで新規プロジェクトを作成
  2. 以下をAppにペースト
    Sub Open() Handles Open
      // 現在のロケールのlocale code(文字列)を取得して保持
      gLocaleStr = Xojo.Core.Locale.Current.Identifier
    End Sub
    
  3. Window1にPushButton(名前はPushButton1)を追加
  4. 以下をPushButton1にペースト
    Sub Action() Handles Action
      MsgBox gkMessage1(gLocaleStr)
    End Sub
    
  5. 以下をWindow1にペースト後、ダイナミックをオンにし、値を図のようにセット(プロジェクト間では設定値もコピーされるが、テキストでは拾えないため)
    Public Const kCaption1 as String
    
     S Shot1
  6. 以下をWindow1にペースト
    Sub Open() Handles Open
      me.PushButton1.Caption=kCaption1(gLocaleStr)
    End Sub
    
  7. MeinMenuBar>ファイル(Name:FileMenu)のTextを#gkFileMenuに変更(注:名前がバッティングしなければ、モジュール名は省略可)
  8. 新規モジュール(名前は、ここでは「Globals」)を作成。
  9. 以下をGlobalsにペースト後、ダイナミックをオンにし、値を図のようにセット(5.項を参照)
    Public Const gkFileMenu as String = ファイル
    
     S Shot2
    注)デフォルト値は、指定しないとIDE上で何も表示されない。(実行時は問題なし)
  10. 以下をGlobalsにペースト後、ダイナミックをオンにし、値を図のようにセット(5.項を参照)
    Public Const gkMessage1 as String
    
     S Shot3
  11. 以下をGlobalsにペースト
    Public Property gLocaleStr as String
    
  12. ビルド設定>共有のBuildで、LanguageをJapaneseにする
     S Shot4
 実行してみたところ、現在の言語設定に対応した文字列が表示されることを確認しました。


 Xojoでの実装(例2)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
  1. プロジェクトを作成するフォルダ内にフォルダ(名前はここではLanguage)を作成
  2. Languageフォルダ内にフォルダを2個(名前はja.lprojとen.lproj)を作成
  3. 任意のテキストエディタで新規に以下の内容をコピーし、ja.lprojフォルダ内にファイル(名前はLocalizable.strings)として保存
    "ファイル" = "ファイル";
    "実行" = "実行";
    "これはテストです。" = "これはテストです。";
    
  4. 任意のテキストエディタで新規に以下の内容をコピーし、en.lprojフォルダ内にファイル(名前はLocalizable.strings)として保存
    "ファイル" = "File";
    "実行" = "Go";
    "これはテストです。" = "This is a Test.";
    
  5. Xojoで新規プロジェクトを作成
  6. 以下をAppにペースト
    Sub Open() Handles Open
      // デバッグ時の対策(プロジェクトと同じフォルダ内のLanguageフォルダからコピー)
      #if DebugBuild then
        Dim f As FolderItem
        f=GetFolderItem("").Child("Language").Child("en.lproj")
        f.CopyFileTo me.ExecutableFile.Parent.Parent.Child("Resources")
        f=GetFolderItem("").Child("Language").Child("ja.lproj")
        f.CopyFileTo me.ExecutableFile.Parent.Parent.Child("Resources")
      #endif
      
      // 初期化
      InitApp()
    End Sub
    
  7. 以下をAppにペースト
    Public Sub GerRsrcBundle()
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // mainBundleを取得
      Dim bndl As Ptr = NSClassFromString("NSBundle")  // クラスメソッドなので、まずNSBundleクラスを取得
      Declare Function mainBundle Lib "Cocoa" selector "mainBundle" (class_id As Ptr) As Ptr
      Dim mainBndl As Ptr = mainBundle(bndl)
      
      // mainBundleからResourcesフォルダのパスを取得
      Declare Function resourcePath Lib "Cocoa" selector "resourcePath" (class_id As Ptr) As CFStringRef
      Dim bPath As CFStringRef  = resourcePath(mainBndl)
      
      // ResourcesフォルダをBundleとして取得
      Declare Function bundleWithPath Lib "Cocoa" Selector "bundleWithPath:" (receiver As Ptr, path As CFStringRef) As Ptr
      gRsrcBundle = bundleWithPath(bndl, bPath)
    End Sub
    
  8. 以下をAppにペースト
    Public Sub InitApp()
      // Resourcesフォルダをバンドルとして取得
      GerRsrcBundle()
      
      // メニュー表記をローカライズ
      InitMenuText()
    End Sub
    
  9. 以下をAppにペースト
    Public Sub InitMenuText()
      // メニュー表記のローカライズ
      FileMenu.Text=gLocalizedString("ファイル")
    End Sub
    
  10. Window1にPushButton(名前はPushButton1)を追加
  11. 以下をPushButton1にペースト
    Sub Action() Handles Action
      msgBox gLocalizedString("これはテストです。")
    End Sub
    
  12. 以下をWindow1にペースト
    Sub Open() Handles Open
      me.PushButton1.Caption=gLocalizedString("実行")
    End Sub
    
  13. 新規モジュール(名前は、ここでは「Globals」)を作成。
  14. 以下をGlobalsにペースト
    Public Function gLocalizedString(msg As String) as String
      // ロケールに対応したローカライズ文字列を取得
      Declare Function localizedStringForKey Lib "Cocoa" Selector "localizedStringForKey:value:table:" (receiver As Ptr, key As CFStringRef, val As CFStringRef, tbl As CFStringRef) As CFStringRef
      Dim localStr As String = localizedStringForKey(gRsrcBundle, msg, nil, nil)
      
      return localStr
    End Function
    
  15. 以下をGlobalsにペースト
    Public Property gRsrcBundle as Ptr
    
  16. ビルド設定>共有のBuildで、LanguageをJapaneseにする
     S Shot4
    注)この方式では不要と思われる節もあるが、ドキュメントが「異なる言語設定(者)間でのプロジェクトの共有」を想定しているので、指定しておく。
 実行してみたところ、現在の言語設定に対応した文字列が表示されることを確認しました。


 Xojoでの実装(例3)
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
  1. 任意のテキストエディタで新規に以下の内容をコピーし、ja.lprojフォルダ内にファイル(名前はLocalizable2.strings)として保存
    "実行" = "実行";
    "これはテストです。" = "これはテストです。";
    
  2. 任意のテキストエディタで新規に以下の内容をコピーし、en.lprojフォルダ内にファイル(名前はLocalizable2.strings)として保存
    "実行" = "Go";
    "これはテストです。" = "This is a Test.";
    
  3. Xojoで新規プロジェクトを作成
  4. 以下をAppにペースト
    Sub Open() Handles Open
      // デバッグ時の対策(プロジェクトと同じフォルダ内のLanguageフォルダ内の各フォルダから、Localizable2.stringsのみをコピー)
      #if DebugBuild then
        Dim f As FolderItem
        f=GetFolderItem("").Child("Language").Child("en.lproj").Child("Localizable2.strings")
        f.CopyFileTo me.ExecutableFile.Parent.Parent.Child("Resources").Child("en.lproj")
        f=GetFolderItem("").Child("Language").Child("ja.lproj").Child("Localizable2.strings")
        f.CopyFileTo me.ExecutableFile.Parent.Parent.Child("Resources").Child("ja.lproj")
      #endif
      
      // 初期化
      InitApp()
    End Sub
    
  5. 以下をAppにペースト
    Public Sub GerRsrcBundle()
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // mainBundleを取得
      Dim bndl As Ptr = NSClassFromString("NSBundle")  // クラスメソッドなので、まずNSBundleクラスを取得
      Declare Function mainBundle Lib "Cocoa" selector "mainBundle" (class_id As Ptr) As Ptr
      Dim mainBndl As Ptr = mainBundle(bndl)
      
      // mainBundleからResourcesフォルダのパスを取得
      Declare Function resourcePath Lib "Cocoa" selector "resourcePath" (class_id As Ptr) As CFStringRef
      Dim bPath As CFStringRef  = resourcePath(mainBndl)
      
      // ResourcesフォルダをBundleとして取得
      Declare Function bundleWithPath Lib "Cocoa" Selector "bundleWithPath:" (receiver As Ptr, path As CFStringRef) As Ptr
      gRsrcBundle = bundleWithPath(bndl, bPath)
    End Sub
    
  6. 以下をAppにペースト
    Public Sub InitApp()
      // 現在のロケールのlocale code(文字列)を保持
      gLocaleStr = Xojo.Core.Locale.Current.Identifier
      
      // Resourcesフォルダをバンドルとして取得
      GerRsrcBundle()
      
      // メニュー表記をローカライズ
      InitMenuText()
    End Sub
    
  7. 以下をAppにペースト
    Public Sub InitMenuText()
      // メニュー表記のローカライズ
      FileMenu.Text=gkFileMenu(gLocaleStr)
    End Sub
    
  8. Window1にPushButton(名前はPushButton1)を追加
  9. 以下をPushButton1にペースト
    Sub Action() Handles Action
      msgBox gLocalizedString("これはテストです。")
    End Sub
    
  10. 以下をWindow1にペースト
    Sub Open() Handles Open
      me.PushButton1.Caption=gLocalizedString("実行")
    End Sub
    
  11. MeinMenuBar>ファイル(Name:FileMenu)のTextを#gkFileMenuに変更(注:名前がバッティングしなければ、モジュール名は省略可)
  12. 新規モジュール(名前は、ここでは「Globals」)を作成。
  13. 以下をGlobalsにペースト後、ダイナミックをオンにし、値を図のようにセット(プロジェクト間では設定値もコピーされるが、テキストでは拾えないため)
    Public Const gkFileMenu as String = ファイル
    
     S Shot2
    注)デフォルト値は、指定しないとIDE上で何も表示されない。(実行時は問題なし)
  14. 以下をGlobalsにペースト
    Public Function gLocalizedString(msg As String) as String
      // ロケールに対応したローカライズ文字列を取得
      Declare Function localizedStringForKey Lib "Cocoa" Selector "localizedStringForKey:value:table:" (receiver As Ptr, key As CFStringRef, val As CFStringRef, tbl As CFStringRef) As CFStringRef
      Dim localStr As String = localizedStringForKey(gRsrcBundle, msg, nil, "Localizable2")
      
      return localStr
    End Function
    
  15. 以下をGlobalsにペースト
    Public Property gLocaleStr as String
    
  16. 以下をGlobalsにペースト
    Public Property gRsrcBundle as Ptr
    
  17. ビルド設定>共有のBuildで、LanguageをJapaneseにする
     S Shot4
 実行してみたところ、現在の言語設定に対応した文字列が表示されることを確認しました。


 おわりに

 どの方法も、後から機能を追加するのはメンドいので、将来的に多言語化が見込まれる場合は、最初から仕込んでおくことを心がけたほうがよさそうです。
 そういう意味では今回試したように、NSLocalizedStringを使う場合はキーを日本語の表示文字列にしておくと、対応するキーだけでなくファイル自体がなくてもとりあえず日本語では表示できるので、一つのやり方かもしれません。

 また、多言語といってもせいぜい日本語と英語くらい、と、非ローマンアルファベット系言語も含めた全世界対応とでは仕込み方も異なってくるでしょうから、実用レベルでは更なる検証が必要かと思われます。

 あと、今回はフォントについては特に指定しませんでしが、NSLocalizedStringにはフォントのオプションがないことや、通常のOSインストールでは各国語のフォントがあらかじめインストールされる(?)ことから、とりあえずSystemにしておけばいいのかなという感じもしますが、よく分かりません。
試しにテキストエディットで、フランス語/ドイツ語/アラビア語/ヘブライ語の例文をブラウザからコピー&ペーストしてみたが、原文通り表示された。
この時、既使用のフォントが対応していればそれ(フランス語/ドイツ語ではヒラギノ角ゴ ProN、ヘブライ語ではLucida Grande)が、なければ対応するフォント(アラビア語ではGeeza Pro)が候補に上がるようような印象を持ったが、詳細は未調査。

 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):[iOS] ゼロから始める!簡単!多言語対応! - Qiita
 参考サイト(2):Localizable.strings で選ばれるデフォルトの言語を設定する - bekkou68 の日記(「言語設定の仕組みと NSLocalizedString の挙動」の項)


 更新履歴

 2020.04.23 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]