Tools.h++ 7.0 ユーザーズガイド: 15 - 国際化

Copyright 1999 Rogue Wave Software
Copyright 1999 Sun Microsystems, Inc.

# 国際化 | # 15 |

プログラム開発者は、国際社会と関わりを持っています。どのような国においても、ソフトウェアやアプリケーションを母国の文化に適合させるという作業は、開発者に共通の問題です。さまざまな文化圏のユーザーの必要性への対応を地域化といい、ソフトウェアを地域化しやすくすることを国際化といいます。

Tools.h++ は米国で作成されました。Tools.h++ は国際化されています。この国際化とは、英字、言語、通貨、数字、日付と時刻の表記など、異なる文化圏の基本的な面を地域化するために必要な枠組みを提供するという意味です。Tools.h++ を使用すると、1 つのアプリケーションを作成するだけで、そのアプリケーションをさまざまな国に出荷できます。このアプリケーションは、実行時に、時刻、日付、文字列、通貨をその国の形式で処理することができます。

Tools.h++ では、国際化のいくつかの面が制約されてはいますが、何も強制される方針がないという特徴があります。Tools.h++ を使用すると、アプリケーションを任意に自由に設計して、クライアントや開発者の必要性に対応することができます。

## RWCString と RWWString による文字の地域化

文字の地域化は、それらを表現できるようにすることから始まります。16 ページの「8 ビットクリーン」で説明したように、Tools.h++ コードは、拡張文字セットを収容するために「8 ビットクリーン」です。英字はすべて 7 ビットで記述されるため、8 番目のビットはウムラウトやセディラなどの発音記号や特殊文字に使用されます。8 ビットでも各種言語の文字グリフすべてを表わすためには不十分な場合があるため、Tools.h++ には、複数バイトとワイド文字コード化という 2 種類の拡張機能があります。

複数バイトコード化では、1 つの文字を表現するのに 1 つまたは複数のバイトのシーケンスを使用します (ASCII 文字は 1 バイト長のままです)。このコード化は小さな領域に記憶可能ですが、インデックスや部分文字列操作には不便です。一方、ワイド文字コード化では、各文字を `wchar_t` という 16 ビットまたは 32 ビットの整数型として扱い、文字列を `wchar_t` の配列として表現します。通常、一方の形式でコード化された文字列は、他方の文字列に変換することができます。

Tools.h++ には、RWCString と RWWString という 2 つの効率の良い文字列型があります (第 2 章「文字列クラスの使用」を参照)。RWCString は 8 ビット文字の文字列を表現し、一部の複数バイト文字列もサポートします。RWWString は `wchar_t` の文字列を表現します。どちらを使用しても、メンバー関数 `collate()` と大域関数 `strXForm()` により、局所的な照合規約に関して標準 C ライブラリサポートへアクセスできます。さらに Tools.h++ では、ワイド表現と複数バイト表現の間で変換を行うことができます。ワイド文字列と複数バイト文字列のコード化の方法は、ホストシステムのものが使用されます(つまり、システムに依存します)。

しかし、文字の表現はこれよりもさらに複雑になる可能性があります。たとえば、文字は大文字か小文字か、あるいはどちらでもないか。ソートされたリストの中で、アクセント付き文字で始まる名前はどこに置かれるのか。キリル文字の名前はどうするのか。バイトストリーム上でワイド文字列はどのように表現されるのか。これらの問題にはいくつもの標準化機関や研究所が取り組んでいますが、それらの結果の移植性はまだそれほど高くありません。当分の間、Tools.h++ は、このような機関の研究結果を最大限に利用するよう努力します。

## メッセージの地域化

ユーザーの言語に対応するために、プログラムは、タイトル、メニュー項目、状態メッセージをその言語で表示しなければなりません。このようなテキストは、通常、プログラムコードとは別にメッセージカタログやリソースファイルに格納されるので、簡単に編集または置換することができます。Tools.h++ は、タイトルやメニューを直接表示するのではなく、エラーが発生したときに状態メッセージを返します。デフォルトでは、Tools.h++ は、これらのメッセージを地域化しようとしません。かわりに、エラーメッセージを各自のカタログから検索できるようにするオプションの機能を用意しています。

この機能は、次の 4 つのモードのどれかで使用することができます。

モード | 定義
--- | ---
メッセージなし | `RW_NOMSG`
`catgets()` を使用 | `RW_CATGETS`
`gettext()` を使用 | `RW_GETTEXT`
`dgettext()` を使用 | `RW_DGETTEXT`

これらの地域化技法とそのマニュアルは、プラットフォームに特定のものです。システムの提供する技法がわかったら、`<rw/compiler.h>` 内の適切なスイッチを設定して Tools.h++ のモードを指定してからライブラリをコンパイルしてください。オブジェクトコードがある場合、この選択はすでに自動的に行われています。

関数 `catgets()` は、メッセージセット番号とそのセット内のメッセージ番号の両方を使用して、地域化されたバージョンのメッセージを調べます。使用するメッセージセットの番号は、`<rw/compiler.h>` にあるマクロ `RW_MESSAGE_SET_NUMBER` に定義されています。関数 `gettext()` はメッセージそのものを使用します。メッセージとそれに対応するメッセージ番号は、付録 C に記載してあります。

`catgets()`、`gettext()`、`dgettext()` の使用方法の詳細は、コンパイラに添付のマニュアルに記載されています。

## 通貨、数字、日付、時刻の地域化

各自の文化以外の文化圏に合わせてアプリケーションを作成する場合、必ず通貨、数字、日付、時刻の表現の問題に直面します。通貨は、単価と表記の両方が異なります。数字は記述方法が異なります。たとえば、ヨーロッパとアメリカでは、ピリオドとコンマの使用方法が逆になります。プログラムでは、ベンダーとカスタマの両方の慣習に合わせて値を表記しなければなりません。

スケジューリングは、ありふれたソフトウェア機能ですが、時間と暦の計算が関係しています。グレゴリオ暦には地域によってさまざまなバージョンがあり、曜日や月の名前が違っていたり、日付を書くときの年、月、日の順序が違っていたりします。時間の表記も 12 時制と 24 時制の 2 つがあるだけでなく、タイムゾーンの規約によってさらに複雑になる可能性もあります。たとえば、夏時間 (DST) の規約は場所によってまちまちで、年によって違う地域もあります。

標準 C ライブラリには、これらの異なる形式のいくつかをサポートするための `<locale.h>` がありますが、完全ではありません。これは、文字列からこれらの型への変換にはまったく無効であり、複数のロケールに関わる変換でもほとんど役に立ちません。POSIX.1 (付録を参照) で定義された共通タイムゾーン機能も同様に制限されており、通常、別の場所について、または同じ場所の翌年についてさえ、時間の日常的な表現を計算する方法はありません。

## RWLocale と RWZone

Tools.h++ は、こうした問題への対処を抽象クラス RWLocale および RWZone によって行なっています。ユーザーが RWDate を使用している場合、気づかないうちに RWLocale を使用しています。日付または時間を文字列に (あるいはその逆に) 変換するたびに、デフォルト引数が RWLocale の参照を渡します。これは、プログラムの開始時に生成されたクラス RWLocaleDefault (RWLocale から派生したもの) の大域的なインスタンスに対する参照です。`RWLocale` を明示的に使用するときは、ユーザー独自のインスタンスを作成し、それをデフォルトの代わりに渡してください。同様に、時間を操作するときは、デフォルトの RWZone 参照が渡されますが、これは独自のものと置き換えることができます。

RWLocale または RWZone のユーザー独自のインスタンスを大域デフォルトとしてインストールすることもできます。さらに、多くのストリームでは、RWLocale のユーザー独自のインスタンスをストリームにインストールするだけで、そのストリーム上で転送される日付と時刻は適切にフォーマットされ、あるいは解析されます。特別な引数を指定する必要はありません。この処理は、「ストリームへの吹き込み」と呼ばれ、次の節で詳しく説明してあります。

次の節では、各種のデータを RWLocale と RWZone を使用して地域化する方法についていくつか例を示します。まず、日付を作成します。これは今日の日付です。

        RWDate today = RWDate::now();

次のように、通常の C ロケール規約を使用してこれを表示できます。

        cout << today << endl;

しかし、ロケールが変わるとどうなるでしょうか。フランス語の表記を使用している場合、環境変数 `LANG` は `fr`1 に設定されています。日付を希望の形式で表示するには、RWLocale オブジェクトを作成します。

        RWLocale& here = *new RWLocaleSnapshot("");

クラス RWLocaleSnapshot は、RWLocale によって定義されるインタフェースの主な実装です。これは、`strftime()` や `localeconv()` などの標準 C ライブラリ関数を利用して、オブジェクトの生成中に必要な情報を大域的な環境変数から抽出します。RWLocaleSnapshot を使用する最も直接的な方法は、RWDate のメンバー関数 `asString()`2 にそれを直接渡すことです。

      cout << today.asString('x', here) << endl;

しかし、これよりも簡単な方法があります。`here` を大域的なデフォルトロケールとしてインストールすると、挿入演算子でこれを使用することができます。

        RWLocale::global(&here);
        cout << today << endl;

### 日付

ユーザーがアメリカ人で、日付をドイツ語で表記したい、しかし、ドイツ語はデフォルトにしたくない、という場合を想定します。まず、ドイツ語ロケールを作成します。

        RWLocale& german = *new RWLocaleSnapshot("de");  //脚注 1 を参照

同じ日付を現地言語とドイツ語の両方の形式で表示することができます。

        cout << today << endl
             << today.asString('x', german) << endl;

『Tools.h++ 7.0 クラスライブラリ・リファレンスマニュアル』の RWLocale の項目にある x の定義を参照してください。

次に、ドイツ語表記の日付文字列を読み取ると仮定します。ここでも、直接的な方法は、なんでも明示的に呼び出すことです。

        RWCString str;
        cout << "enter a date in German: " << flush;
        str.readLine(cin);
        today = RWDate(str, german);
        if (today.isValid())
          cout << today << endl;

しかし、抽出演算子 `>>` を使用することもできます。この演算子はドイツ語表記の日付を読み取って、それを構文解析するため、ストリームにドイツ語ロケールを吹き込んでその情報を渡すことができます。

次のコードは、ストリーム `cin` にドイツ語ロケールを吹き込み、ドイツ語表記の文字列を読み取って変換し、それを現地形式で表示します。

        german.imbue(cin);
        cout << "enter a date in German: " << flush;
        cin >> today;  // ドイツ語表記の日付を読み取る
        if (today.isValid())
          cout << today << endl;

ストリームにロケールを吹き込むという方法は、特定のロケールに従って多数の値を挿入または抽出しなければならない場合、あるいはロケール引数を必要なポイントに渡す方法がない場合に便利です。目的のコードの中で静的メンバー関数の `RWLocale::of(ios&)` を使用すると、ストリーム中に吹き込まれたロケールを発見することができます。そのストリームにまだロケールが吹き込まれていない場合は、`of()` が現在の大域的なロケールを返します3。

RWLocale で定義されたインタフェースは日付以外のものも処理します。時間、数字、および価格を文字列に変換したり、その逆の変換も行うことができます。これらの変換には、それぞれに複雑な問題がからんでいます。時間の変換は、入力した人または読む人が属しているタイムゾーンを識別する必要があるため複雑になってきます。夏時間の実施区域が入り組んでいることが、この問題をいっそう厄介なものにしています。数字の表記は、挿入演算子と抽出演算子 (`"<<"` と `">>"`) が `<iostream.h>` ですでに定義されているためいくぶん面倒です。通貨については、価格の標準的な内部表現がないということが最大の問題です。しかし、これらの問題はどれも克服できないものではありません。

### 時間

タイムゾーンの問題を考えてみましょう。まず注意しなければならないことは、タイムゾーンとロケールとの間には単純な対応関係がないということです。スイスは、夏時間 (DST) 規則も含めて全土で単一のタイムゾーンを使用していますが、公用言語は 4 つあります (フランス語、ドイツ語、イタリア語、ロマンシュ語)。一方、ハワイとニューヨークは共通の言語を使用していますが、タイムゾーンは 5 時間ずれていて、ときには 6 時間ずれることもあります (これはハワイでは夏時間が実施されていないためです)。さらに、 タイムゾーンの規定はその文化圏で好んで使用されるフォーマットとはほとんど関係がありません。したがって、タイムゾーンの管理を `RWLocale` に包括せずに、独立したタイムゾーンオブジェクトを使用します。

Tools.h++ では、クラス RWZone がタイムゾーンに関する情報をカプセル化しています。これは抽象クラスで、そのインタフェースはクラス RWZoneSimple の中に実装されています。RWZoneSimple の 3 つのインスタンスが起動時に作成されます。これらは現地時計時間、現地標準時間、およびユニバーサル時間 (UTC: グリニッジ標準時 (GMT) と同じ) を表します。現地時計時間は使用されている夏時間を反映したものです。絶対時間 (クラス `RWTime` における時間) と文字列との間の変換を行う場合は、常に RWZone のインスタンスが関与します。デフォルトでは現地時間が使用されますが、任意の RWZone インスタンスへの参照を渡すことができます。

では、具体例で検討してみましょう。いま、ニューヨークからパリへの旅行計画を立てたと仮定します。ニューヨークを 1993 年 12 月 20 日の午後 11:00 に出発し、1994 年 3 月 30 日に戻ります。パリを発つのは午前 5:00 (パリ時間) です。さてパリに到着したとき、現地の時計は何時を指しているでしょうか。

まず、タイムゾーンおよび出発時刻を構築します。

      RWZoneSimple newYorkZone(RWZone::USEastern, RWZone::NoAm);
      RWZoneSimple parisZone  (RWZone::Europe,    RWZone::WeEu);
      RWTime leaveNewYork(RWDate(20, 12, 1993), 23,00,00, newYorkZone);
      RWTime leaveParis  (RWDate(30,  3, 1994), 05,00,00, parisZone);

フライトには片道約 7 時間かかります。

        RWTime arriveParis  (leaveNewYork + long(7 * 3600));4
        RWTime arriveNewYork(leaveParis   + long(7 * 3600));

ここで、到着時刻と日付を各地域の表記に従って表示します。つまり、パリの場合はフランス語で、ニューヨークの場合は英語で表示します。

        RWLocaleSnapshot french("fr");              // 処理系依存
        cout << "Arrive' au Paris a\Q "
             << arriveParis.asString('c', parisZone, french)
             << ", heure local." << endl;
        cout << "Arrive in New York at "
             << arriveNewYork.asString('c', newYorkZone)
             << ", local time." << endl;

これは、フライトでいくつかのタイムゾーンを通過し、出発した日と違う日に到着するとしても有効です。さらに帰国の日には (翌年になる)、フランスはすでに夏時間に入っていますが、米国はまだです。こうした細目は上記の例コードに現れていません。これらは、RWTime と RWZone によって目に見えないところで処理されるのです。

Tools.h++ に組み込まれている夏時間 (DST) 規則が適用される地域 (今までのところ、北米、西欧、および "noDST" が含まれています)については、これは簡単なことです。他の規則が適用される地域ではどうなるでしょう。たとえばアルゼンチンでは春が 9 月から始まり、夏が 3 月に終ります。RWZoneSimple はテーブル駆動式ですから、規則が十分に単純であればユーザーが独自の (RWDaylightRule 型の) テーブルを作成して、RWZoneSimple の作成時のようにそれを指定することができます。たとえば夏時間が 9 月の最後の日曜の午前 2 時に始まり、3 月の最初の日曜に終わると仮定します。まず次のように、RWDaylightRule の静的インスタンスだけを生成します。

      static RWDaylightRule sudAmerica =
         { 0, 0, TRUE, {8, 4, 0, 120}, {2, 0, 0, 120}};

上記の数字の意味についての詳細は、RWZoneSimple の解説および `<rw/zone.h>` を参照してください。次に RWZone オブジェクトを生成します。

        RWZoneSimple  ciudadSud( RWZone::Atlantic, &sudAmerica );

これで、`ciudadSud` を上記の `paris` や `newYork` と同様に使用することができます。

しかし、夏時間規則が複雑すぎて単純なテーブルで記述できない地域 (たとえば英国) についてはどうしたらよいのでしょうか。英国では夏時間が原則として 4 月の第 3 土曜の次の朝から始まります。ただし、当日が復活祭にあたる場合は、開始日が 1 週間繰り上がるのです。このような区域については、正しくラベル付けした標準時を使用するのが最善と思われます。これがうまくいかない場合は RWZone から派生させ、そのインタフェースを英国専用として実装することができます。この方法は、すべての可能性に対処できるほど汎用性の高いものを作成しようとするよりもずっと簡単で、しかもそのコードは小さくて速いものになります。

残る問題は、ある特定の場所にどのような夏時間規則が適用されているのかを知るための標準的な方法がないということです。この点について、標準 C ライブラリは何もサポートしていません。したがって、アプリケーションが実行されている現地の環境から必要な情報を入手する (ユーザーに尋ねるなどして) 必要があります。

この問題の例として、夏時間が適用されている場合、現地時計時間の RWZone インスタンスが、北米の夏時間規則を使用するように作成されているということです。プログラムのユーザーが北米にいない場合、おそらくデフォルトの現地時間帯によって誤って夏時間変換が実行されてしまうので、それを置き換えなければなりません。たとえば、パリにいるプログラムのユーザーについては、次のようにこの問題を解決することができます。

        RWZone::local(new RWZoneSimple(RWZone::Europe, RWZone::WeEu));

`<rw/locale.h>` をよく見ると、RWDate と RWTime が記述されていないことがわかります。そのかわり、RWLocale は標準 C ライブラリの `struct tm` 型を使用します。RWDate と RWTime は両方ともこの型への変換を実行することができます。場合によっては、その型を直接使用する方が、`RWTime::asString()` を使用するよりも好ましいことがあります。たとえば、時間と分だけからなる時刻文字列 (12:33 など) を記述すると仮定します。`strftime()` に対して定義されて、RWLocale で実装されている標準の書式には、このオプションが含まれていません。しかし、それを模倣することはできます。以下に 1 つの方法を示します。

        RWTime now = RWTime::now();
        cout << now.hour() << ":" << now.minute() << endl;

各種のマニピュレータを使用しなくても、このコードによって `9:5` のような文字列が生成されます。次のような方法もあります。

        RWTime now = RWTime::now();
        cout << now.asString('H') << ":" << now.asString('M') << endl;

これにより、`09:05` が生成されます。

以上の各例において、`now` が構成要素に 2 回変換されています。1 回は時間を抽出するためで、もう 1 回は分を抽出するためです。これは不経済な演算です。時刻または日付の構成要素を数多く処理するつもりなら、時刻を 1 回だけ変換する方が経済的です。

        RWTime now = RWTime::now();
        struct tm tmbuf;
        now.extract(&tmbuf);
        const RWLocale& here = RWLocale::global();         // デフォルトの
                                                         // 大域ロケール
        cout << here.asString(&tmbuf, 'H') << ":"
             << here.asString(&tmbuf, 'M'); << endl;

1901 年より前、または 2037 年より後の時刻を扱う場合は、RWTime を使用することができません。これは、RWTime が必要な範囲を表現できないためです。5 RWLocale による `struct tm` の演算にはそれほど制約がないため、RWLocale を使用すると、任意の時刻や日付に対して変換を実行することができます。

### 数字

RWLocale は、文字列と数字 (整数と浮動小数点値の両方) の間で変換を行うためのインタフェースを提供します。RWLocaleSnapshot は、このインタフェースを実装するもので、標準 C ライブラリの `struct lconv` 型で定義されているすべての機能を提供します。これには、適切な桁区切り子、小数「点」、通貨表記の使用が含まれます。文字列から変換するときは、RWLocaleSnapshot は同じ桁区切り子を許容して検査します。

残念ながら、標準 `iostream` ライブラリが提供する組み込み型の数値の挿入演算子と抽出演算子の定義は無効にできないので、このクラスのストリーム演算は、期待するほど簡潔にはなりません。かわりに、次のように直接 RWCString 関数を使用することができます。

        RWLocaleSnapshot french("fr");
        double f = 1234567.89;
        long i = 987654;
        RWCString fs = french.asString(f, 2);
        RWCString is = french.asString(i);
        if (french.stringToNum(fs, &f) &&
            french.stringToNum(is, &i))            // 変換確認
          cout << "C:\t" << f << "\t" << i << endl
               << "French:\t" << fs << "\t" << is << endl;

フランス語表記では桁区切り子にピリオドを使用し、小数点にカンマを使用するため、このコードは次のように表示されます。

      C:      1.234567e+07     987654
      French: 1.234.567,89     987.654

桁区切り子を付けると数字が読みやすくなります。

### 通貨

コンピュータには価格を表現する標準的な方法がないため、通貨の変換はさらに厄介です。Tools.h++ では、現行の最小通貨単位が整数になるように表現する、という規約を採用しました。たとえば米国では、残高 "$10.00" を表現する場合、次のように記述します。

      double sawbuck = 1000.;

この表現方法の利点は、範囲が広く、正確で、移植性があるということです。範囲が広いというのは、$0.00 から $10,000,000,000,000.00 まで (およびそれ以上) の値を正確に表現できるという意味です。これは国家予算の規模も超える大きさです。正確というのは、価格を小数部なしで表現することによってその算術演算を実行し、結果が等しいか比較することができるという意味です。

      double price = 999.;                                     // $9.99
      double penny = 1.;                                       //  $.01
      assert(price + penny == sawbuck);

これは、例えば "`price = 9.99;`" のように、価格が単純に表現されていれば不可能なことです。

移植性があるというのは、一般の 64 ビット整数や BCD 表現と違って、`double` が基本型であるという意味です。もちろん、そうした表現に対しても価格の計算を実行することはできるのですが、それらと `double` との間の変換が常に可能であるため、`double` がすべてのものをサポートします。将来は、一般に使用されている他の表現についても RWLocale が直接にサポートすることになるでしょう。

次は通貨の変換に関する例です。

      const RWLocale& here = RWLocale::global();
      double sawbuck = 1000.;
      RWCString tenNone  = here.moneyAsString(sawbuck, RWLocale::NONE);
      RWCString tenLocal = here.moneyAsString(sawbuck,RWLocale::LOCAL);
      RWCString tenIntl  = here.moneyAsString(sawbuck, RWLocale::INTL);
      if (here.stringToMoney(tenNone,  &sawbuck) &&
          here.stringToMoney(tenLocal, &sawbuck) &&
          here.stringToMoney(tenIntl,  &sawbuck))  	// 変換を確認
        cout << sawbuck  << "  " << tenNone << "  "
             << tenLocal << "  " << tenIntl << "  " << endl;

米国ロケールでは、次のように表示されます。

      1000.00000  10.00  $10.00  USD 10.00

### 環境変数の設定に関する注意

クラス RWTime に関する節で説明したように、コンパイラとオペレーティングシステム (Windows オペレーティングシステムを含む) によっては、特定の環境変数を設定して、ロケール機能を作動させる必要があります。環境変数を設定しないと、非常に困難な状況になる可能性があります。

Borland、MetaWare、Microsoft、Symantec、Watcom のどれかを使用している場合は、環境変数 TZ を適切なタイムゾーンに設定する必要があります。

      set TZ=PST8PDT

環境変数の設定方法については、使用しているコンパイラやオペレーティングシステムのマニュアルを参照してください。

1 ロケール名に対する既存の標準があるにもかかわらず、多くのベンダーからさまざまな命名方法が提供されています。詳細については、各ベンダーのマニュアルを参照してください。

2 関数 `asString()` の最初の引数は文字です。これは、標準 C ライブラリ関数 `strftime()` がサポートするフォーマットオプションのいずれかにすることができます。

3 静的メンバー関数の `RWLocale::unimbue(ios&)` で、ロケールが吹き込まれていない状態にロケールを戻すことができます。ただしこれは、そのストリームに現在の大域的なロケールを吹き込むことと同じではありません。

4 int 型が 16 ビットの処理系では、long(7L*3600L) と書く必要があります。

5 64 ビットシステムを使用している場合、使用できる日付に関する実用上の制約はありません。