複数のロケール環境で動作するコードを書いたことがあるプログラマであれば分かるだろうが、 プログラムで扱うメッセージを十分に抽象化して ユーザーに表示するメッセージから分離することは、 プログラミング言語のサポートがなければ簡単なことではない。 実際、ほとんどのモダンなプログラミング言語のライブラリでは、 この抽象化を行うためのメカニズムを用意している。
ある程度の規模のソフトウェア プロジェクトでは、 プログラムに何の変更も行わなくても 異なる国や言語において動作するような、 移植性の高い API を設計することが必ず必要になってくる。 プログラム ロジックと翻訳されたメッセージを分離するインターフェイスがあるとよい。
この SRFI では、そのような機能をもつインターフェイスを定義する。 翻訳されたメッセージにアクセスするためのデータ構造は、 その実装にとって効率のよいものを選択すればよい。 また、翻訳されたメッセージを格納している外部システムに アクセスするための標準化された関数を定義する。
この SRFI で定義するインターフェイスは、 ローカライズというもののすべての側面をカバーしてはいない。 たとえば、非ラテン語系文字のサポートや、 数値や日付の書式といった事柄は扱っていない。 そのような機能は、 (この SRFI を拡張した形で定義されるであろう) 将来の SRFI で定義されるだろう。
メッセージ バンドル (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] 番目のオプション引数を直接参照するようにする。 参照された値は消費されない。この拡張により、オプション値をその位置によって参照することができ、 言語により語の位置が変化するようなメッセージに対しても、 メッセージ テンプレートを適切に作成することができる。
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-bundle と load-bundle! は実装しなければならず、 引数に関わらず常に #f を返すようにしなければならない。 この SRFI を使用するプログラマは、 ローカライズされたバンドルを外部システムから取得したり格納することができなくても、 それは致命的なエラーではないと認識すべきである。
localized-template package-name
message-template-name -> string or #f
パッケージ名とメッセージ テンプレート名 (両方ともシンボル) を指定して、ローカライズされたメッセージ テンプレートを取得する。 指定されたテンプレートが見つからない場合、 #f が返る。テンプレートを取得したら、 その文字列に format を適用することで、 ユーザーに表示する文字列を作成できる。
(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-language と current-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)))))))
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.