表題

ローカライズ

著者

Scott G. Miller

状態

この SRFI は現在「確定」の状態である。SRFI の各状態の説明については ここ を参照せよ。 この SRFI に関する議論については メーリングリストのアーカイブ を参照せよ。

概要

この SRFI では、ロケール依存のメッセージを表示するためのインターフェイスを定義する。 Scheme プログラムは、テンプレート化されたメッセージの翻訳を登録し、 Scheme システムのロケール環境に適したメッセージを透過的に取得できる。

論拠

複数のロケール環境で動作するコードを書いたことがあるプログラマであれば分かるだろうが、 プログラムで扱うメッセージを十分に抽象化して ユーザーに表示するメッセージから分離することは、 プログラミング言語のサポートがなければ簡単なことではない。 実際、ほとんどのモダンなプログラミング言語のライブラリでは、 この抽象化を行うためのメカニズムを用意している。

ある程度の規模のソフトウェア プロジェクトでは、 プログラムに何の変更も行わなくても 異なる国や言語において動作するような、 移植性の高い API を設計することが必ず必要になってくる。 プログラム ロジックと翻訳されたメッセージを分離するインターフェイスがあるとよい。

この SRFI では、そのような機能をもつインターフェイスを定義する。 翻訳されたメッセージにアクセスするためのデータ構造は、 その実装にとって効率のよいものを選択すればよい。 また、翻訳されたメッセージを格納している外部システムに アクセスするための標準化された関数を定義する。

この SRFI で定義するインターフェイスは、 ローカライズというもののすべての側面をカバーしてはいない。 たとえば、非ラテン語系文字のサポートや、 数値や日付の書式といった事柄は扱っていない。 そのような機能は、 (この SRFI を拡張した形で定義されるであろう) 将来の SRFI で定義されるだろう。

他の仕様への依存

SRFI-29 に準拠した実装は、 SRFI-28 「基本的な書式文字列」も実装しなければならない。 メッセージ テンプレートとなる文字列は、 SRFI-28 で定義される format 関数で処理できなければならないからである。

仕様

メッセージ バンドル

メッセージ バンドル (message bundle) とは、 メッセージ テンプレートとそれを識別するためのキーのペアの集合のことである。 各バンドルは、キーと値のペアを 1 つ以上含んでいる。 バンドルそれ自体は、 バンドルを一意に識別するための バンドル指定子に関連付けられる。

バンドル指定子

バンドル指定子は Scheme のリストであり、 メッセージ バンドルのパッケージとロケールを指定する。 ほとんどの場合、ロケール指定子は 1 個~ 3 個の要素をもつ。 1 つめの要素は、そのバンドルの適用対象となるパッケージを表すシンボルである。 2 つめ、および、3 つめのの要素はロケールを表す。 2 つめの要素 (ロケールの最初の要素) を指定する場合、 バンドルの言語を表す ISO 639-1 言語コードを 2 文字で指定する。 3 つめの要素を指定する場合、 ISO 3166-1 国コードを 2 文字で指定する。 ときには、 4 つめの要素として、 バンドルで使われている文字エンコードを指定することもある。 バンドル指定子を構成するこれらの要素は、すべて Scheme シンボルで指定する。

もし、たった 1 つの翻訳しか提供されていない場合、 バンドル指定子としてパッケージ名だけを与えるべきである。 たとえば、(mathlib) のように指定する。 この翻訳のことをデフォルト翻訳と呼ぶ。

バンドルの検索

バンドルからメッセージ テンプレートを取得するとき、 Scheme システムの現在のロケールに基づいてテンプレートが検索される。 テンプレートを取得するときには、パッケージ名を指定する。 Scheme システムは、指定されたパッケージ名と現在のロケールから バンドル指定子を作成する。 たとえば、mathlib パッケージ用の、 フランス系カナダ人のためのメッセージを取得する場合、 バンドル指定子 '(mathlib fr ca)' を使用する。 プログラムは、引数なしの手続きを呼び出すことで、 現在のロケールの要素を取得できる。

current-language -> symbol
current-language symbol -> undefined

引数を指定しない場合、現在の ISO 639-1 言語コードをシンボルとして返す。 引数を指定する場合、現在実行されている Scheme スレッドの言語コードを変更する。 (スレッドごとの言語設定ができないのであれば、 Scheme システム全体の言語を変更する。)

current-country -> symbol
current-country symbol -> undefined

引数を指定しない場合、現在の ISO 3166-1 国コードをシンボルとして返す。 引数を指定した場合、現在実行されているスレッドの国コードを変更する。 (スレッドごとの国コード設定ができないのであれば、 Scheme システム全体の国コードを変更する。)

current-locale-details -> list of symbols
current-locale-details list-of-symbols -> undefined

引数を指定しない場合、ロケールの詳細を、シンボルのリストとして返す。 このリストには、文字エンコードなどの情報が含まれていることがある。 引数を指定した場合、 現在実行されているスレッドのロケール情報を変更する。 (スレッドごとのロケール情報が設定できないのであれば、 Scheme システム全体のロケール情報を変更する。)

Scheme システムは、まず最初に、 指定された名前に正確に一致するバンドルを検索すべきである。 そのようなバンドルが見つからない場合、 バンドル指定子のリストの最後の要素を除去して、 その名前に一致するバンドルを検索する。 それでも見つからない場合、 最後の要素を除去して検索することを繰り返す。 バンドル指定子が空リストになるまでバンドルが見つからない場合、 エラーを発生させるべきである。

このような手順で検索する理由は、 指定されたロケールにもっとも近いテンプレートを選択することを可能にし、 さらに、 もしそのロケールに対する翻訳が提供されていない場合、 より一般的なテンプレートを選択するようにするためである。

メッセージ テンプレート

メッセージ テンプレート (message template) とは、 ローカライズされたメッセージのことで、 書式コードが含まれてもよい。 メッセージ テンプレートは Scheme の文字列であるが、 多くの Scheme システムや SRFI-28 (基本的な書式文字列) で定義されている format 手続きで処理できる形式でなければならない。

この SRFI では、SRFI-28 を拡張して format のエスケープ コードを追加する:

~[n]@* - このコードの直後に続く、値を必要とするエスケープ コードが、 次の消費されない値を参照するのではなく、 format 関数の [n] 番目のオプション引数を直接参照するようにする。 参照された値は消費されない
この拡張により、オプション値をその位置によって参照することができ、 言語により語の位置が変化するようなメッセージに対しても、 メッセージ テンプレートを適切に作成することができる。

バンドルの準備

Scheme システムがバンドルを使用して ローカライズされたメッセージ テンプレートを取得するには、 Scheme システムがバンドルを認識しなくてはならない。 この SRFI では、移植性が高い方法でバンドルを定義する方法を規定する。 また、 何らかの外部システムにバンドルを格納したり、 そこからバンドルを取り出す方法を規定する。

declare-bundle! bundle-specifier association-list -> undefined

バンドル指定子で指定された名前をもつ新しいバンドルを宣言する。 バンドルの中身は連想リストで指定する。 この連想リストは、シンボルとメッセージ テンプレート (文字列) を関連付けたものである。 指定された名前のバンドルがすでに存在する場合、 ここで新しく宣言されたバンドルにより上書きされる。
store-bundle bundle-specifier -> boolean
declare-bundle!load-bundle! により使用可能となったバンドルを、 何らかの外部システムに格納する。 その外部システムでは、Scheme システムが再起動してもバンドルが永続的に保持される。 成功すると真値が返る。 失敗すると #f が返る。
load-bundle! bundle-specifier -> boolean
バンドルを格納するための何らかの外部システムから、 バンドルを取得する。 バンドルの取得に成功した場合、 この関数は真値を返し、 取得したバンドルはただちに Scheme システムから使用可能となる。 バンドルが見つからなかったりロードに失敗した場合、 この関数は #f を返し、 Scheme システムにおけるバンドルの登録状態は変化しない。
本 SRFI に準拠する Scheme システムは、 バンドルを格納するための外部システムをサポートしなくてもよい。 その場合でも store-bundleload-bundle! は実装しなければならず、 引数に関わらず常に #f を返すようにしなければならない。 この SRFI を使用するプログラマは、 ローカライズされたバンドルを外部システムから取得したり格納することができなくても、 それは致命的なエラーではないと認識すべきである。

ローカライズされたメッセージ テンプレートの取得

localized-template package-name message-template-name -> string or #f

パッケージ名とメッセージ テンプレート名 (両方ともシンボル) を指定して、ローカライズされたメッセージ テンプレートを取得する。 指定されたテンプレートが見つからない場合、 #f が返る。

テンプレートを取得したら、 その文字列に format を適用することで、 ユーザーに表示する文字列を作成できる。

使用例

以下の使用例では、SRFI-29 を利用してローカライズされた簡単なメッセージを表示している。 また、バンドルを定義して外部システムに格納し、 可能であれば、外部システムからバンドルを取得している。
(let ((translations
       '(((en) . ((time . "Its ~a, ~a.")
                (goodbye . "Goodbye, ~a.")))
         ((fr) . ((time . "~1@*~a, c'est ~a.")
                (goodbye . "Au revoir, ~a."))))))
  (for-each (lambda (translation)
              (let ((bundle-name (cons 'hello-program (car translation))))
                (if (not (load-bundle! bundle-name))
                    (begin
                     (declare-bundle! bundle-name (cdr translation))
                     (store-bundle! bundle-name)))))
             translations))

(define localized-message
  (lambda (message-name . args)
    (apply format (cons (localized-template 'hello-program
                                            message-name)
                        args))))

(let ((myname "Fred"))
  (display (localized-message 'time "12:00" myname))
  (display #\newline)

  (display (localized-message 'goodbye myname))
  (display #\newline))

;; Displays (English):
;; Its 12:00, Fred.
;; Goodbye, Fred.
;;
;; French:
;; Fred, c'est 12:00.
;; Au revoir, Fred.

実装

本 SRFI に準拠する実装では、 current-languagecurrent-country は、Scheme セッションで使用可能なロケールを区別できなければならないが、 以下の参照実装ではこの区別ができない。 この関数を参照実装に含めているのは、 単に以下のコードが任意の R4RS Scheme システムで動作するようにするためである。

以下で実装している format は本 SRFI に準拠してはいるが、 SRFI-6 (基本的な文字列ポート) と SRFI-23 (エラー報告) を使用している。

;; バンドルを格納するための連想リスト
(define *localization-bundles* '())

;; ここで実装している current-language と current-country は、
;; Scheme セッションの実際のロケールをデフォルトとするように
;; 書き直さなければならない。
(define current-language
  (let ((current-language-value 'en))
    (lambda args
      (if (null? args)
          current-language-value
          (set! current-language-value (car args))))))

(define current-country
  (let ((current-country-value 'us))
    (lambda args
      (if (null? args)
          current-country-value
          (set! current-country-value (car args))))))

;; この参照実装では、load-bundle! と store-bundle! はともに #f を返す。
;; このような実装でも、本 SRFI に準拠している。
(define load-bundle!
  (lambda (bundle-specifier)
    #f))

(define store-bundle!
  (lambda (bundle-specifier)
    #f))

;; 指定されたバンドル指定子のバンドルを宣言する。
(define declare-bundle!
  (letrec ((remove-old-bundle
            (lambda (specifier bundle)
              (cond ((null? bundle) '())
                    ((equal? (caar bundle) specifier)
                     (cdr bundle))
                    (else (cons (car bundle)
                                (remove-old-bundle specifier
                                                   (cdr bundle))))))))
    (lambda (bundle-specifier bundle-assoc-list)
      (set! *localization-bundles*
            (cons (cons bundle-specifier bundle-assoc-list)
                  (remove-old-bundle bundle-specifier
                                     *localization-bundles*))))))

;; パッケージ名とテンプレート名を与えて、
;; ローカライズされたテンプレートを取得する。
(define localized-template
  (letrec ((rdc
            (lambda (ls)
              (if (null? (cdr ls))
                  '()
                  (cons (car ls) (rdc (cdr ls))))))
           (find-bundle
            (lambda (specifier template-name)
              (cond ((assoc specifier *localization-bundles*) =>
                     (lambda (bundle) bundle))
                    ((null? specifier) #f)
                    (else (find-bundle (rdc specifier)
                                       template-name))))))
    (lambda (package-name template-name)
      (let loop ((specifier (cons package-name
                                  (list (current-language)
                                        (current-country)))))
        (and (not (null? specifier))
             (let ((bundle (find-bundle specifier template-name)))
               (and bundle
                    (cond ((assq template-name bundle) => cdr)
                          ((null? (cdr specifier)) #f)
                          (else (loop (rdc specifier)))))))))))

;; SRFI-28 および SRFI-29 に準拠した format 手続き。
;; エラー報告のために SRFI-23 を使用している。
(define format
  (lambda (format-string . objects)
    (let ((buffer (open-output-string)))
      (let loop ((format-list (string->list format-string))
                 (objects objects)
                 (object-override #f))
        (cond ((null? format-list) (get-output-string buffer))
              ((char=? (car format-list) #\~)
               (cond ((null? (cdr format-list))
                      (error 'format "Incomplete escape sequence"))
                     ((char-numeric? (cadr format-list))
                      (let posloop ((fl (cddr format-list))
                                    (pos (string->number
                                          (string (cadr format-list)))))
                        (cond ((null? fl)
                               (error 'format "Incomplete escape sequence"))
                              ((and (eq? (car fl) '#\@)
                                    (null? (cdr fl)))
                                    (error 'format "Incomplete escape sequence"))
                              ((and (eq? (car fl) '#\@)
                                    (eq? (cadr fl) '#\*))
                               (loop (cddr fl) objects (list-ref objects pos)))
                              (else
                                (posloop (cdr fl)
                                         (+ (* 10 pos)
                                            (string->number
                                             (string (car fl)))))))))
                     (else
                       (case (cadr format-list)
                         ((#\a)
                          (cond (object-override
                                 (begin
                                   (display object-override buffer)
                                   (loop (cddr format-list) objects #f)))
                                ((null? objects)
                                 (error 'format "No value for escape sequence"))
                                (else
                                  (begin
                                    (display (car objects) buffer)
                                    (loop (cddr format-list)
                                          (cdr objects) #f)))))
                         ((#\s)
                          (cond (object-override
                                 (begin
                                   (display object-override buffer)
                                   (loop (cddr format-list) objects #f)))
                                ((null? objects)
                                 (error 'format "No value for escape sequence"))
                                (else
                                  (begin
                                    (write (car objects) buffer)
                                    (loop (cddr format-list)
                                          (cdr objects) #f)))))
                         ((#\%)
                          (if object-override
                              (error 'format "Escape sequence following positional override does not require a value"))
                          (display #\newline buffer)
                          (loop (cddr format-list) objects #f))
                        ((#\~)
                          (if object-override
                              (error 'format "Escape sequence following positional override does not require a value"))
                          (display #\~ buffer)
                          (loop (cddr format-list) objects #f))
                         (else
                           (error 'format "Unrecognized escape sequence"))))))
              (else (display (car format-list) buffer)
                    (loop (cdr format-list) objects #f)))))))

著作権

Copyright (C) Scott G. Miller (2002). All Rights Reserved.

This document and translations of it may be copied and furnished to others, and derivative works that comment on or otherwise explain it or assist in its implementation may be prepared, copied, published and distributed, in whole or in part, without restriction of any kind, provided that the above copyright notice and this paragraph are included on all such copies and derivative works. However, this document itself may not be modified in any way, such as by removing the copyright notice or references to the Scheme Request For Implementation process or editors, except as needed for the purpose of developing SRFIs in which case the procedures for copyrights defined in the SRFI process must be followed, or as required to translate it into languages other than English.

The limited permissions granted above are perpetual and will not be revoked by the authors or their successors or assigns.

This document and the information contained herein is provided on an "AS IS" basis and THE AUTHOR AND THE SRFI EDITORS DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.


編集者: David Rush
著者: Scott G. Miller
最終更新日時: Mon Jun 17 12:00:08 Pacific Daylight Time 2002