翻訳選択に関する最近の変更点

本稿は「Some recent changes to choice of translation」の抄訳です。
 

Qt 6.7以降、ローカライゼーション(地域対応)およびインターナショナリゼーション(国際化)に関して、Qtがどのように適切な言語設定を選択し、またユーザーがそれを選択できるよう支援するかについて、多くの改良が加えられてきました。この変更ラッシュも、Qt 6.9では一段落したようなので、ここで一度、何が変わったのか、そしてその背景についてまとめておきたいと思います。

この話の発端は、実はQt 6.4にさかのぼります。QTBUG-102796 の修正により、 uiLanguages() で返されるエントリの順序が、システムロケールと Unicode Consortium の Common Locale Data Repository(CLDR)から得られるデータに基づくロケールとの間で一貫性を持つようになりました。

まずは、その前提から見ていきましょう…

背景

アプリケーションが、ユーザーのニーズに合わせて動作を調整するために複数のリソースの中から選択できる場合、その選択肢の一つのカテゴリが「ローカライズおよび国際化(localisation and internationalisation)」です。これは、L10n と I18n とも略されます。英語話者の間でも綴りが一定しないため、多くの言語で前者は12文字、後者は20文字の単語であり、それぞれ先頭と末尾は一致しているという特徴があります。

この考え方は、ユーザーが理解できる言語、読めるスクリプト、金額や日付表記といった様々な記述方法に関する慣習に(必要に応じて)適応するというものです。最初の2つは言語と言語スクリプトとして明示されますが、残りの要素は通常、それらと言語使用地域(ほとんどの場合は国)に依存すると見なされます。ただし、世界の事情は複雑なため、一筋縄ではいきません。

この言語、スクリプト、地域の組み合わせは「ロケール(locale)」と呼ばれます。Qt においては、L10n と I18n は主に QLocaleQTranslator によって処理されますが、他にも関与するコンポーネントがあります。

QLocale は、ユーザーの L10n 設定をオペレーティングシステムから取得する方法を知っています。また、アプリケーションがダイアログなどで状況ごとの L10n 設定を提供する場合に、利用可能な選択肢の一覧を取得するためにも使われます。いずれにしても、QLocale インスタンスは、アプリケーションの他の機能に対して適切な L10n と I18n を選ぶ手助けとなる情報を保持しています。特に重要な要素のひとつが、Qt では I18n に分類される「プログラマーが書いたテキストをユーザーが読むための翻訳処理」です。これは QTranslator によって処理され、Qt Linguist などの関連ツールと連携して行われます。

QTranslator は、アプリケーションにインストールされた利用可能な翻訳の中から、ユーザーにとって適切なものを選びます。そのための基準となるのが QLocale::uiLanguages() であり、厳密には「言語」ではなく「ロケール識別子」のリストを返すため、uiLocales() と呼ぶべきかもしれません。QTranslator は、このリストの中で最も早く一致するエントリと一致する翻訳を選ぶ仕組みです。

最近の一連の変更は、こうした多様なユーザー設定に対応するために、より堅牢で信頼性の高い仕組みにしようとする取り組みから生まれたものです。将来的には、これをより洗練された形で処理できる QLocaleSelectorQTBUG-112765 参照)を実装することが目標ですが、現時点では uiLanguages() の返す値と、それを QTranslator がどう使うかの改良という形で進めています。

Qt 内には他にも uiLanguages() を類似の用途で使用する部分があり、今後それらの扱いも見直す可能性がありますが、それらは翻訳とは異なる優先事項を持っていることもあります。たとえば、テキスト読み上げ機能では、ロケールに合った音声を選ぶ必要がありますが、その際にスクリプト(文字体系)は重要でないかもしれません。

ロケール識別子

uiLanguages() が返す文字列のリストに含まれる各エントリは、ロケールを識別するものであり、いわゆる「サブタグ」がセパレータ(ここではハイフンを使用するが、アンダースコアも一般的)で結合されたものです。各サブタグは、言語・スクリプト・地域のいずれかを表しており、通常この順序で記述されます(一般的にサブタグは他の情報を表すこともあるが、Qt が認識するのはこの3つのみです)。例えば、en-Latn-US は、アメリカで使われるラテン文字による英語を表します(ここで言うラテン文字とは、ヨーロッパ諸言語の多くで使用され、US-ASCII の文字セットの基礎となっているアクセント記号のない文字群のことです)。同じ種類のサブタグを2つ含む識別子は存在できませんが、スクリプトおよび/または地域を省略することは可能です。言語が指定されていない場合には、特殊な言語 und(未定義)が使われます。したがって、en は英語の汎用ロケールであり、und-AU はオーストラリアの汎用ロケールです。

システムロケールを除き、QLocale はローカリゼーションに関するすべてのデータを Unicode Consortium の Common Locale Data Repository(CLDR)から取得します。CLDR には、ロケールが不完全に指定されている場合に不足部分を補完するための likely subtag ルールが用意されています。2つのロケール識別子がこれらのルールに従って補完された際に同じ完全な形式になる場合、QLocale はそれらを等価とみなします。このような等価性を、ここでは「likely-equivalence(類似等価性)」と呼びます。

たとえば、da ⇒ da-Latn-DK というルールは、「ユーザーの好みがデンマーク語であることしか分からない場合には、ラテン文字を使い、デンマークで一般的に使われているデンマーク語の形式を使うのが最も適切である」と示しています。多くの場合、言語に暗黙的に対応する地域はその言語の名前の由来地域ですが、2つの例外があります:英語とポルトガル語です。これらはそれぞれイングランドやポルトガルには対応せず、en ⇒ en-Latn-USpt ⇒ pt-Latn-BR によってアメリカ合衆国やブラジルに対応します。これは、それぞれの言語を話す人々が発祥の地よりも旧植民地に多く存在するためです。ただし、実際には en ⇒ en-Latn-US というルールは存在していません――この等価性は以下のルールによって暗黙的に示されます。

CLDR v46.1 において、790個のルールの最後に位置する likely subtag のフォールバックは und ⇒ en-Latn-US です。これは「他に何も分からなければ、アメリカ英語のラテン文字表記を使うべきだ」と示しています。完全に一致するルールがない不完全なロケール記述に対しては、適用すべき likely subtag ルールを選ぶ方法に関するルールもあります。たとえば、ユーザーがオーストラリア人であることだけが分かっており、und-AU として表現される場合、それに対するルールは存在しないため、既知の AU を一時的に取り除き、上記の und のルールを適用した後、元の情報である AU を復元し、en-Latn-US の地域部分 USAU に置き換えて en-Latn-AU を得ます。同様に、und-Latn-AU の場合も、同じルールにより en-Latn-AU となります。これと同じように、プレーンな en は、und ルールから残りのサブタグを補って en-Latn-US と等価になります。これは、先ほど述べた明示的なルールが存在しないにもかかわらず、そうなるということです。

このように、ルールにより不完全なロケール(例:en-AU)から省略された部分を補完できます。逆に、完全なロケールが与えられた場合、どの部分を省略しても同じ意味になるかを判断できます。たとえば、en ⇒ en-Latn-US であり、これは en-AU とは異なるため、en-AUen に短縮することはできません。一方で、AU だけから始めた場合(上記参照)、en-Latn-AU を意味することになりますが、それは en-AU からの短縮ではなく、und-AU としての開始であるため、en-AU は最小形とみなされます。

Qt におけるこの仕組みの使われ方

Qt 6.9以前では、QLocale::uiLanguages() は与えられたロケールの識別子、またはシステムロケールの場合には「ユーザーが理解できる」と明示した識別子のシーケンスを最初に使用し、それぞれのエントリに対して推定同等(likely-equivalent)な形式を追加して展開していました。Qt 5.14(および LTS である 5.12.6以前では、この展開は CLDR 由来のエントリにのみ適用されており、システムロケールが返すリストはそのまま使用されていました。当初、システムロケールに対しても likely-equivalent な拡張を行うために、システムから渡された文字列それぞれを元に QLocale インスタンスを作成していましたが、この方法では、ユーザーが指定したロケールを、Qt がCLDRデータを持つ最も近い一致へと「強制的に」変換してしまうことに気付いていませんでした。

Qt 6.4までは、システムロケールに対する並び順が、CLDR由来のロケールのそれと一致していないという問題がありました(QTBUG-102796)。それ以降は(最初のエントリが早めに現れるという特殊なケースを除いて)、単一のエントリから展開された結果は、「サブタグが多く具体的なもの」ほど先に並び、「サブタグが少ない汎用的なもの」ほど後ろに並ぶようになっています。Qt 6.5では、上記の「QLocale による変換ミス」を修正し、システムクエリの結果にシステムロケール自身の識別子(または同等と見なされるもの)が含まれていなかった場合には、それをリストに追加する処理も加えました。

Qt 6.7では、QMimeTypeuiLanguages() を使用する際の複雑な処理(主に、ファイル拡張子などで識別されるファイル種別をユーザーにわかりやすく表示する方法の選択)を整理しました。そして、その処理を少しでも簡潔にするため、uiLanguages() にセパレータ(区切り文字)を指定できるパラメータを追加しました(ただし、その後にその実装ミスを修正する必要がありました)。さらにこの後に説明する内容を受けて、最近では QMimeType のコードをさらにシンプルにすることができました

変化の芽生え

類似エントリのみを含める場合の問題の一つに、たとえば en-AU のリストには en-Latn-AU は含まれるが en は含まれないという点があります。アプリケーションが en-AUen-Latn-AU の翻訳を持っていなくても en の翻訳を持っている場合、その en を選ぶのは理にかなっています。英語の場合、これは問題なく機能します(もっと複雑なロケールではそう簡単にはいかないのですが、それは後述します)。このため QTranslator は、uiLanguages() から得られた各エントリに対して、まずそのエントリに一致するリソースを検索し、その後、一致しなかった場合はそのエントリの末尾のサブタグを一つずつ削っていく形で短縮形を試しながら一致を探す、という処理を行っていました。そうすることで、en-AU の短縮形として en を見つけることができ、問題なく動作していました。しかしバージョン 6.4 で順序の一貫性を保つように変更した結果、システムロケールが en-AU よりも先に en-Latn-AU を返すようになり、状況が変わったのです。

その結果、QTranslatoren-Latn-AUen-Latn、次に en と順に短縮していく過程で、en-AU にたどり着く前に en を選択してしまいました。そのため、en-AU を設定していたユーザーにもかかわらず、en の翻訳が適用されてしまい、より適切であるはずの en-AU の翻訳が無視されるという事態になったのです。そして、ここから私の話が始まります。

  • QTBUG-121418:QTranslator が zh_TW の翻訳ではなく zh を読み込んでしまう問題
  • QTBUG-124898:上記の en-AU のケース(少し見えにくい形で発生)

技術的には、この問題は以前から存在していましたが、6.4 の変更によってより表面化したのです。たとえば、ユーザーがシステム言語に en-AUen-GB を設定していた場合、これまでは内部的に en-AU, en-Latn-AU, en-GB, en-Latn-GB という順に展開されていました。そして、en-GBen のみの翻訳が存在する場合、本来一致していた en-GB よりも先に、実際にはどの指定とも一致しない en(しかも en-Latn-US に相当する)を選んでしまっていたのです。

しかしこのバグが見えるようになった今、ようやく修正に取りかかることができました。

最近の出来事

QTBUG-124898 を修正しようとした最初の試みでは、QTranslatoruiLanguages() の各エントリを処理する際にその都度短縮していくのではなく、最初に短縮形すべてを含む展開済みのリストを構築し、そのリストを「詳細度(specificity)」に基づいてソートするという方法が取られました。

しかしこのアプローチは、uiLanguages() が複数のエントリから始まり、それぞれに「類似エントリ(likely-equivalent companions)」を展開する場合に何が起こるかを十分に考慮していませんでした。前段で取り上げたケースを例に見てみましょう。

  • uiLanguages() が en-AU, en-GB から始まると、それは次のように展開されます:en-Latn-AU, en-AU, en-Latn-GB, en-GB
  • その後、QTranslator は短縮形を追加します:en-Latn-AU, en-Latn, en, en-AU, en, en-Latn-GB, en-Latn, en, en-GB, en
  • これらを合わせて、重複を省いて並べ替えると次のようになります(※明確にするために重複は除いています):en-Latn-AU, en-Latn-GB, en-Latn, en-AU, en-GB, en

ご覧のとおり、この並べ替えにより、元の uiLanguages() リストで en-AU より後にあった en-Latn-GB が、逆に en-AU より前に来てしまっています。これは、異なる言語が混在している場合に特に問題を悪化させます。

たとえば、QTBUG-129434 のケースでは、英語と繁体字中国語(zh-Hant)が混在していました。zh(プレーンな中国語)は zh-Hans(簡体字中国語)と類似であると見なされるため、zh-Hant の翻訳ファイルはより特定的な形式のファイル名になっていました。一方で、アプリ側の英語翻訳はさまざまな英語変種を区別する必要がなかったため、単に en として用意されていました。

しかし、並べ替え後のリストでは en が後ろの方に回されてしまっていたため、システム構成で英語が zh-Hant より前に指定されていたにもかかわらず、en の翻訳ファイルが見つからず、代わりに zh-Hant が適用されてしまったのです。

幸いなことに、この問題はリリース前に発見され、速やかなロールバックにより修正されました。

この時点で私は問題を詳しく調査し、根本的な原因は QTranslator が「likely-equivalence(類似言語としての等価性)」を理解していないことにあると結論づけました。つまり、QTranslator 自体は uiLanguages() のエントリの順序が何を意味しているのかを理解する立場にないのです。短縮によって得られる言語コードの一部は類似言語として扱えるものの、すべてがそうとは限りません。たとえば、en-AU を短縮して得られる en は、見かけ上は似ていても en-AU と同等ではありません(en は実際には en-Latn-US とみなされるため)。したがって、短縮処理は「類似言語」を理解しているコンポーネント、すなわち QLocale に任せるべきだという結論に至りました。また、ある言語エントリに対応するすべての likely-equivalent を、適切にリストに含める必要があることも明らかになりました。これまでの実装では、リストには「likely subtags を補完した結果」と「最小形の likely-equivalent」のみが含まれていました。たとえば、ユーザーが単に en を指定した場合、それは最小形と見なされるため、追加で en-Latn-US は含まれましたが、en-USen-Latn などは含まれていませんでした。今回の見直しによりこれらも適切に追加されるようになりました。

さらに、今回の修正作業を通じてもう一つ明らかになったことがあります。従来の修正アプローチでは、uiLanguages() にまったく異なる言語(たとえば英語と中国語など)が混在する可能性があるという事実を想定していなかったのです。この前提の欠如が、以前の修正案がうまく機能しなかった理由でした。この点についてはドキュメントにも明記されていなかったため、Volker がその旨の段落を追記しました。そして我々は、短縮語を正しい場所に挿入できるよう、再設計に着手しました。

スクリプトの一致問題

uiLanguages() が最初から短縮形を含めていないのはなぜかと、もっともな疑問を抱くかもしれません。実際、多くの言語においては、最小形だけが翻訳者によって用意され、それでその言語の大多数のユーザーには十分に機能しています。しかし、既に見たとおり、それほど単純ではないケースもあります。たとえば、zh-Hant は広く使われていますが、zhzh-Hans という関係のため、短縮しても likely-equivalent にはなりません。簡体字と繁体字の相互理解度や、繁体字の読者がどの程度簡体字に対応できるかは不明ですが、ここで問題となるのは、言語が複数のスクリプトで存在しうるという点です。これは、テキスト読み上げのために音声を選ぶコードであれば問題ありませんが、書き言葉の翻訳においては重要な違いとなります。

私は、ある言語が異なる集団によって異なるスクリプトで書かれる例を網羅的に把握しているわけではなく、それらのケースのうちどれが実際に相互理解の問題を引き起こすのかについても、十分な知識を持っているわけではありません。しかし、そうしたケースで共通するテーマの一つは、同じ言語を異なるスクリプトで使っている集団が、政治的または文化的な対立関係にあることが少なくないという点です。そのため、ユーザーに「相手側のスクリプト」で書かれた翻訳を表示してしまうと、不快感や侮辱を与えるリスクがあるだけでなく、場合によっては「敵側の文章を読んでいる」として問題になる可能性すらあります(たとえば、理解のない上司に見つかるなど)。ましてや、単に読めないという実用的な問題もあるのです。人々は、自分と最も似ているがゆえに対立している相手に対して、特に強い感情を抱きがちです。したがって、回避可能な問題については極力避けるべき理由が、ここにもあるのです。

したがって、現在は uiLanguages() に非等価な短縮形も含めることにしましたが、ユーザーが設定した内容と等価なすべての合理的な選択肢が、非等価な短縮形より先に試されるように注意する必要があります。いくつかの実験と、関連する問題を報告してくれたユーザーからのフィードバックを経て、私は妥協案として、短縮形が元のエントリと同じスクリプトを使用しているが等価ではない場合には、それをそれに短縮される likely-equivalent の最後のブロックの直後に挿入することにしました。

このルールが少し分かりにくい場合、次のような例を考えてみましょう:

ユーザーが en-GB, en-NL, nl-NL を設定している(オランダに住むイギリス人を想定)。likely-equivalent を追加すると、次のように展開されます:en-Latn-GB, en-GB, en-Latn-NL, en-NL, nl-Latn-NL, nl-NL, nl-Latn, nl; nl-Latnnlnl-NL と likely-equivalent ですが、enen-Latnen-GBen-NL とは likely-equivalent ではありません。もしすべての非等価な短縮形を末尾に置いた場合、ennl の後になり、ユーザーの UI はオランダ語になってしまいます。ユーザーは en(慣れたスクリプトで書かれている)を問題なく読めるにもかかわらずです。このルールでは、これらの英語の短縮形を英語のエントリ群の最後のブロックの後に配置するため、結果として en-Latn-GB, en-GB, en-Latn-NL, en-NL, en-Latn, en, nl-Latn-NL, nl-NL, nl-Latn, nl となり、利用可能な場合に en の翻訳が nl より優先されるようになります。

対照的に、パンジャーブ語はパキスタンではアラビア文字で書かれ、インドではグルムキー文字で書かれます。アラビア文字でパンジャーブ語を読み書きする人は、グルムキー文字を全く知らないかもしれません(逆も同様です)。そのようなユーザーがイギリスに住んでいる場合、システムの言語設定が pa-PK, en-GB となっている可能性があります。likely-equivalent を追加すると、次のように展開されます:pa-Arab-PK, pa-PK, pa-Arab, en-Latn-GB, en-GBpa-Arabpa-PK と likely-equivalent なので含まれますが、likely subtag のルールにより papa-Guru-IN となるため、pa は異なるものと見なされ、除外されます。さらに、pa が暗黙的に示すスクリプトは Guru であり、pa-PK が示す Arab とは一致しないため、短縮形を追加する際にはリストの末尾に回されます。一方で、これまでと同様に、en-Latnenen-GB と likely-equivalent ではありませんが、同じスクリプト(Latn)を使用しているため、en-GB ブロックの直後に追加されます。結果として、リストは以下の順になります:

pa-Arab-PK, pa-PK, pa-Arab, en-Latn-GB, en-GB, en-Latn, en, pa

もし pa-PK やその等価な翻訳が存在せず、paen の翻訳が存在する場合、この順序ではユーザーには en が選択されます。これは、en がユーザーの求めたスクリプトと一致しているため、たとえ pa が第一言語であっても、読めない可能性のあるスクリプトで書かれた pa より en を優先する、という判断です。

正しくするために

何を目指すべきかが分かったので、あとは実際にコードをそれに合わせて調整するだけでした。これはかなり厄介でしたが、慎重にテストケースを書くことで最終的な動作する解決策にたどり着くことができました。すべての複雑さを考慮しつつも、できる限りシンプルなコードに仕上げることができました。(実際、この記事を書いている途中で思い出したケースを確認したところバグを見つけ、その場で修正しました。)どのみち、最初のいくつかの変更がどのように機能するかを確認し、他の人と動作について議論するまで、ここで述べたすべての細かい点には気づいていませんでした。

主な変更点は、uiLanguages()短縮形エントリを追加したことでした。これにより結果を操作し、癖や例外ケースを発見し、対応策を検討できるようになりました。この変更は 6.9 に入り、QTranslator の処理を簡素化し、一方で 6.8 では同様の処理を行うよう QTranslator のコードを再設計しました。その後、(現在は)6.9 となっているバージョンで細かい部分を整理しました。

短縮形エントリの追加により uiLanguages() がやや複雑になったため、少し分かりやすくなるように再設計しました。さらにいくつかのテストケースを追加することで、ようやく QTBUG-121418 をクローズできました。

この時点で、等価なエントリの追加をより体系的に行う必要があると認識しました。その結果、順序の理解が進み、各スクリプトが同一の短縮形を、それを生み出した等価エントリ群の最後に配置するよう順序を調整しました(ただし、先に述べたミスがあり、それは現在修正が統合されつつあります)。その後、等価エントリの挿入をより体系化する方法が見えてきました。

では、現時点でどうなっているのでしょうか?

願わくば、Qt ではいつも通り「そのままでうまく動く(Just Work)」状態になっているはずです――これまでと同様に、あるいは少し良くなっているかもしれません。ただし、もしこれまでこうした複雑さに対処するために何らかの処理を行っていた場合は、QLocale::uiLanguages() を使ってコードを簡素化し、高速かつ適切に動作させられる可能性があります。

  • これまで QTranslator が行っていたように uiLanguages() のエントリを短縮していたコードがある場合、6.9 以降(おおよそ 6.8.3 以降も)では、その処理は不要になります。
  • uiLanguages() をチェックしてリソースの一致を探す際に、ロケール名と一致するものを個別に探していた場合は、6.5 以降では uiLanguages() の中にそのロケール名も(likely-equivalent や短縮形と一緒に)含まれているはずなので、個別に探す必要はなくなっています。
  • また、uiLanguages() が確実に「正しいこと」をしてくれなかったために、変則的なコードを書いていた方は、その変則処理を削除して、今はうまく動くかどうかをぜひ試してみてください。もしそういった事例があれば、うまくいった場合でも、まだ工夫が必要だった場合でも、ぜひ教えていただけるとうれしいです。

これまで問題点を教えてくださった皆さま、本当にありがとうございました。今回の変更でそれらが完全に解消されていることを願っています。そして、今後も Qt の動作に疑問を感じたり、改善案があれば、いつでもお気軽にフィードバックをお寄せください。Qt をより良くしていくために、皆さまの声をお待ちしております。


Blog Topics:

Comments