xml:langによる言語切替

DITAではたいていどの要素にもxml:langがつけられます.例えばbookmap/@xml:langでドキュメント全体の言語を指定します.今まではたいていこのレベルで済んでいました.文書ごとに言語が切り替えら得れば良いのです.
 
でもあるときにtopic/@xml:langで言語を切り替えられないかという要望がお客様から寄せられました.このときはtopicレベルで@xml:langを取得し切り替えるようにしました.これは主にヨーロッパ向けでひとつの文書の中に複数の言語で安全性に関する注意書きが書かれているものでした.ですから、例えばnote要素のようなもののアイコンも各言語毎に変えて出力しなければなりません.しかしこれはまだtopicレベル限定なので簡単なものです.
 
もう少し難しい例が考えられます.例えばDITAでは次のような記述が英語の文書でも可能です.文書によってはないとは言えないでしょう.(英語の文書でbookmap/@xml:lang="en"とします.)
 
<p>"Hello!" is written "<ph xml:lang="ja">こんにちは</ph>" in Japanese and "<ph xml:lang="zh-CN">你好</ph>" in Simplified Chinese.</p>
 
しかし、たぶん今世界中のどのDITA→XSL-FOを処理するスタイルシートでもこのようなインラインレベルでの言語切り替えには対応していないのではないかと思います.インラインレベルでの言語切り替えは、いわば本当の意味での一文書での多言語処理です.これがXHTML出力だったらxml:langをそのままspan要素の属性につけてやるだけで済むでしょう.何故かといえば、ブラウザがちゃんと当てるフォントをxml:langによりコントロールしてくれるからです.
 
一方PDF出力では明示的に<fo:inline font-family="MS-Mincho">こんにちは<fo:inline>と<fo:inline font-family="SimSun">你好</fo:inline>とやらなければならないでしょう.元が英語の文書でこれをPDFにしようとすれば、フォントが英語用のTimes New Romanでは、日本語や中国語のインラインの文字に対するグリフがないためエラーとなってしまうからです.
もちろん以下のように変換すればこのようなMissing glyphのエラーも防げるかもしれません.
 
<fo:block font-family="Times New Roman,MS-Mincho,SimSun">"Hello!" is written <fo:inline xml:lang="ja">こんにちは</fo:inline> in Japanese and <fo:inline xml:lang="zh-CN">你好</fo:inline> in Simplified Chinese.</fo:block>
 
これはPDFを作るFormatterにxml:langの情報を与えてフォント選択をまかせるものです.この方法でちょっと危ういのは、font-family="Times New Roman,SimSun,MS-Mincho"とすると"こんにちは"にSimSunが選択されてしまう可能性があることです.中国語フォントは日本語のかなスクリプトにも対応しているからです.ですので明示的なフォント指定がPDF出力では欠かせないと思います.
 
しかしここで考え込んでしまいました.
 
それはph要素には一般に固定的なフォントを割り当てることは決してしないからです.ph要素はDITAでは主に条件処理に使われます.基本的には上位のフォント指定を継承させるのが普通だからです.
 
例えば地の文をserifフォントでレイアウトし、タイトル(title要素)にsans-serifを割り当てるようなデザインを取るとき、ph要素はこのどちらにも現われ得ます.つまり<ph xml:lang="ja">こんにちは</ph>は、<fo:inline font-family="MS-Mincho">こんにちは<fo:inline>ではなく、<fo:inline font-family="MS-Gothic">こんにちは<fo:inline>が正解なのかもしれないのです.
 
結論としてph要素のテンプレートでは、それ自体ではフォントファミリーを選択し得ないというのが現実でした.でもなんとか指定された言語に対するfont-familyを求めなければなりません.
 
これに対する答えは何があるでしょうか?しばし考え込んでしまいました.やっと出てきた答えは次のようなものです.
 
まずカレントのコンテキストがphのとき、./ancestor-or-self::*の要素を取得し、更にその要素に対して適用すべきスタイルを取得します.このスタイルの中からphに指定されているxml:langに対するfont-familyの指定を探し出し、ph要素にもっとも近いものを適用すれば良いのです.
 
例えば、最初の例の<ph xml:lang="ja">こんにちは</ph>に対するancestor-or-self::*を列挙するとたぶんDITAの中間ファイルだと次のような階層になるでしょう.
 
ph
p
tbody
topic
bookmap
 
少なくともpとbookmapのいずれかにxml:lang="ja"に対するfont-familyが定義されていれば、<fo:inline font-family="MS-Mincho">こんにちは<fo:inline>を生成可能です.p要素は段落で地の文のスタイルを適用しますからたいていfont-familyの指定はあるでしょう.またbookmapはたいていfo:rootに設定するスタイルが対応します.p要素にfont-familyを対応付けなければ、fo:rootにはたいてい文書全体に適用すべきfont-familyを記述しますからこのいずれかから取得可能と考えられるからです.
 
このようにph要素の祖先の要素にさかのぼって適用すべきfont-familyを求めるという考え方は、通常のxsl:attribute-setを使ったスタイルシートではまず不可能だと思います.スタイル定義をXSLTスタイルシートから切り離して、完全に外付けのXMLファイルにして自由に読み取りができるようにしなければなりません.
 
例えばp要素に対するスタイル定義は次のように書きます.本当のXSLTスタイルシートのxsl:attribute-setにはxml:langなんていう区別はつけられないですが、自分で外部に作るXMLファイルだったら何でも勝手に作れます.
 
[en_style.xml]
<attribute-set name="atsP" xml:lang="en">
  <attribute name="font-family">Times New Roman</attribute>
</attribute-set>
 
[ja_style.xml]
<attribute-set name="atsP" xml:lang="ja">
  <attribute name="font-family">MS-Mincho</attribute>
</attribute-set>
 
[zh-CN_style.xml]
<attribute-set name="atsP" xml:lang="zh-CN">
  <attribute name="font-family">SimSun</attribute>
</attribute-set>
 
さてこのようにインラインレベルでの言語選択が理論的には可能だとすると、DITAの文書を読み込んで最初にやらなければいけないのは、その文書に含まれるxml:langの一覧を取り出し、それに対するスタイル定義を一括して取得することです.例えば20ヶ国語のスタイル定義を作ったとして毎回20ヶ国語のスタイル定義を読み込むのは意味がありません.文書中に指定されているxml:langに対するものだけを読み込めばよいのです.
こんなことは出来るのでしょうか?まだテストしていませんが、次のようなことをやればユニークなxml:langを列挙できると思います.distinct-values()を使います.(DITA-OTの中間ファイルを想定しています.)
 
<xsl:variable name="root" select="/*[1]" as="element()"/>
<xsl:variable name="map" select="$root/*[contains(@class,' map/map ')][1]" as="element()"/>
<xsl:variable name="uniqueXmlLang" as="xs:string*">
  <xsl:variable name="xmlLang" as="xs:string*">
    <xsl:for-each select="$map/@xml:lang | $map//*[contains(@class,' topic/topic ')]/descendant-or-self::*/@xml:lang">
      <xsl:sequence select="string(.)"/>
    </xsl:for-each>
  </xsl:variable>
  <xsl:for-each select="distinct-values($xmlLang)">
      <xsl:sequence select="string(.)"/>
  </xsl:for-each>
</xsl:variable>
 
最初の例だとこれで
 
en
ja
zh-CN
 
が取得できるでしょう.あとはこれを基にして英語、日本語、中国語(簡体字)のスタイル定義を読み込めばよいはずです.
 
またxml:lang属性はbookmapのテンプレートからトンネルパラメータで下位のテンプレートに伝達します.下位の要素でxml:langが指定されていたらそこでトンネルパラメータの値を更新して更に下位のテンプレートに伝達します.各要素では伝達されたxml:langまたはその要素に指定されたxml:langがあればそれを優先して対応するスタイルを外部のスタイル定義から取得します.
 
bookmapレベルを超えた言語切り替えというのはずっと課題でした.クリスマスの休みで家でゴロゴロしていたのですが、なんとか実現できるようなアイディアを考えることができました.