• R/O
  • SSH
  • HTTPS

提交

标签
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

PHPのフレームワークです。オートローディング、ルーティング、ORマッパ、フォームバリデータ、その他ユーティリティがセットになっています。


Commit MetaInfo

修订版182 (tree)
时间2022-08-22 10:35:03
作者tantancode

Log Message

多言語ファイルについての対応

更改概述

差异

--- docs/database.sql (revision 181)
+++ docs/database.sql (revision 182)
@@ -61,14 +61,14 @@
6161 -- ソースツリーの中に配置して静的管理するのがふさわしくない、運用次第で動的に追加される翻訳テキストを集中貯蔵する。
6262 -- ソースツリーの中に配置して静的管理するテキストは resources/locale の下にあるファイルで保持されている。
6363 CREATE TABLE text_master (
64- symbol_attr ENUM( -- テキスト種別
65- 'dev.dummy', -- 開発用.ダミー
64+ text_group ENUM( -- テキスト種別
65+ 'dev.dummy' -- 開発用.ダミー
6666 ) NOT NULL, --
67- symbol_id INT NOT NULL, -- テキスト主体のID。symbol_attr に従って何のIDかが決まる。
67+ text_id INT NOT NULL, -- テキストの持ち主のID。text_group に従って何のIDかが決まる。
6868 lang_code ENUM('ja', 'en') NOT NULL, -- 言語コード
6969 text_content VARCHAR(8192) COLLATE utf16_unicode_ci NOT NULL, -- テキスト
70- PRIMARY KEY (symbol_attr, symbol_id, lang_code),
71- INDEX (lang_code, symbol_attr, symbol_id) -- 特定の言語で、特定の属性に対して検索をかける時に使用する。
70+ PRIMARY KEY (text_group, text_id, lang_code),
71+ INDEX (lang_code, text_group, text_id) -- 特定の言語で、特定の属性に対して検索をかける時に使用する。
7272 ) ENGINE=Aria COLLATE=ascii_bin;
7373
7474
--- lib/ArrayUtil.php (revision 181)
+++ lib/ArrayUtil.php (revision 182)
@@ -524,8 +524,7 @@
524524 * // $r は $target['alpha'][1] への参照になる。
525525 *
526526 * 存在しない要素へのパスを指定した場合、その要素が null で作成される。
527- * ただし、第3引数に偽の値を含む変数を指定した場合、要素は作成されず、第3引数に指定した変数に要素の
528- * 有無が真偽値でセットされる。
527+ * ただし、第3引数に偽の値を含む変数を指定した場合、要素は作成されず、第3引数に指定した変数に要素の有無が真偽値でセットされる。
529528 *
530529 * 例)
531530 * $target = array(
--- lib/Localizer.php (revision 181)
+++ lib/Localizer.php (revision 182)
@@ -1,49 +1,82 @@
11 <?php
22
33 /**
4- * テキストシンボルを各言語テキストに置き換えるためのユーティリティ。国際化対応で使う。
4+ * テキストシンボルを各言語テキストに置き換えたり、言語別アセットへのアクセスを提供するためのユーティリティ。多言語対応で使う。
55 *
66 * ●このユーティリティにおける基本
77 *
8- * ・まず言語ファイルを resources/locale/ja/ の下に配置する。ここでは sample.csv という名前で配置して、次のように記述する。
8+ * まずCSV形式の言語ファイルを resources/locale/ja/ の下に配置する。ここでは sample.csv という名前で配置して、次のように記述する。
99 *
10- * これはフー,これはフーです。
10+ * これはフー,これは<フー>です。
1111 * N個あるよ,[num]個ありますね。
1212 * これはバー,これは[keyword]バー[/]です。
1313 *
14- * ・次に処理対象の文字列を準備する。ここでは次のような文字列とし、変数 $s に格納されているものとする。
14+ * 次にビューテンプレートファイルで次のように呼び出す(SmartyRendererの場合)。
1515 *
16- * <div>{(sample:これはフー)}</div>
17- * <div>{(sample:N個あるよ num=`5`)}</div>
18- * <div>{(sample:これはバー keyword=`<span style="color:red">`)}</div>
16+ * <div>{[i18n _='sample:これはフー']}</div>
17+ * <div>{[i18n _='sample:N個あるよ' num=>5]}</div>
18+ * <div>{[i18n _='sample:これはバー' keyword=>'<span style="color:red">'}])</div>
1919 *
20- * ・次のように展開すると...
20+ * すると出力は次のようになる。
2121 *
22- * $localizer = new Localizer();
23- * $r = $localizer->processSymbolicText($s);
24- *
25- * ・$r は次のような文字列になる。
26- *
27- * <div>これはフーです。</div>
22+ * <div>これは&lt;フー&gt;です。</div>
2823 * <div>5個ありますね。</div>
2924 * <div>これは<span style="color:red">バー</span>です。</div>
3025 *
31- * ●ただ...
26+ * ●エンコードとヘルパ関数 i18n()
3227 *
33- * 上記のように母体テキストと融合したシンボルを用いると、(ユーザが入力した文字列などで)意図しない展開を招く場合があるので考慮事項が多くなる。
34- * 実質、使用できるのはそのような心配がない場合に限られるだろう。
28+ * {[i18n ...]} では "encode" パラメータでHTMLエンコードやJSエンコードを行うかを設定できる。次の値のいずれかとなる。
29+ * "html" デフォルト。HTMLエンコード。HTML文字のエスケープとnl2brが行われるが、
30+ * プレースホルダ nl2br が "off" になっているとnl2brだけは抑制される。
31+ * "js" JSエンコード。シングルクォートとダブルクォート、改行のエスケープが行われる。
32+ * "plain" エンコードしない
33+ * HTMLエンコードがデフォルトなので留意されたい。
3534 *
36- * 融合したシンボルを使わず、素直に関数を使って局所的に展開するほうが良いだろう。
35+ * ただ、エンコードされるのは翻訳文中の平テキスト部分のみ。プレースホルダはエンコードされないので注意されたい。
3736 *
38- * <div><?php echo $localizer->expandSymbol('sample:これはフー') ?></div>
39- * <div><?php echo $localizer->expandSymbol('sample:N個あるよ', ['num'=>5]) ?></div>
40- * <div><?php echo $localizer->expandSymbol('sample:これはバー', ['keyword'=>'<span style="color:red">'']) ?></div>
41- * <div><?php echo $user_data ?></div>
37+ * {[i18n _='sample:あなたの名前' name=>'ほ<げ>子']}
38+ * あなたの名前,あなたの<名前>は[name]です。
39+ * ↓
40+ * あなたの&lt;名前&gt;はほ<げ>子です。
4241 *
43- * こうすれば、シンボル展開をする場所としない場所を制御できる。
42+ * これは上述の例のように、プレースホルダにHTMLタグをセットできるようにするためにこうなっている。
4443 *
45- * ●[/]について
44+ * smarty関数 {[i18n ...]} の他にヘルパ関数 i18n() というものがあって、引数定義もほぼ同じなのだが、第三引数の省略時が "plain" になる点が異なる。
45+ * これは、サーバ側のAPIレスポンスなどを実装する時に使用するものであるため。
4646 *
47+ * echo i18n('sample:これはフー'); // これは<フー>です。
48+ *
49+ * ●htdocs/ 下の .js や .html などの、PHPが動かないファイルでは...
50+ *
51+ * たとえば、<script src="/js/foo.js"></script> などとリンクしており、この foo.js の中に翻訳の必要のあるテキストが含まれている場合は...
52+ *
53+ * まず htdocs/js/foo.js は次のように {(...)} というデリミタを使ったシンボルでテキストを記述する。
54+ *
55+ * var message1 = "{(sample:これはフー)}";
56+ * var message2 = "{(sample:N個あるよ num=`5`)}";
57+ * var message3 = "{(sample:これはバー keyword=`<span style=\"color:red\">`)}";
58+ *
59+ * 次に、HTML中に "/js/foo.js" のパスを出力する時に、{[asset_url ...]} を使って次のように記述する。
60+ *
61+ * <script src="{[asset_url path='js/foo.js']}"></script>
62+ *
63+ * すると、次のような内容の .js にリンクすることになる。
64+ *
65+ * var message1 = "これは<フー>です。";
66+ * var message2 = "5個ありますね。";
67+ * var message3 = "これは<span style=\"color:red\">バー</span>です。";
68+ *
69+ * プレースホルダの括りはバッククォートになっているので留意されたい。
70+ * この仕組みが働くのは、htdocs/ の下にある .txt, .js, .html, .htm の拡張子を持つファイルに対して、{[asset_url ...]} (や、それが呼び出す
71+ * UrlUtil::assetUrl() など) を使用した場合のみ。ただし .min.js は除く。
72+ *
73+ * エンコードについては、.js がJSエンコード、.html, .htm がHTMLエンコード、.txt はエンコードなしになる。
74+ *
75+ * 翻訳後のファイルが var/cgm/translate/ の下に作られる。元ファイルの更新日時と比較して自動的に新しくするので普段は意識する必要はないが、
76+ * 言語ファイルを更新した場合はキャッシュに反映できないので、元ファイルを空保存して新しくするか、キャッシュを削除する必要がある。
77+ *
78+ * ●プレースホルダ [/] について
79+ *
4780 * ・[/]は前方に現れたHTMLタグが格納されたプレースホルダを記憶している。
4881 *
4982 * {(sample:フー1 link=`<a ...>`)}
@@ -79,44 +112,38 @@
79112 * ↓
80113 * これは<a ...>リンク<img ... /></a>です。
81114 *
82- * ●エンコード
83- *
84- * シンボルの展開は processSymbolicText() や expandSymbol() メソッドで行うが、このときにHTMLエンコードやJSエンコードを行うかを設定できる。
85- * 次の値のいずれかとなる。
86- * (なし) エンコードしない
87- * html HTMLエンコード。HTML文字のエスケープとnl2brが行われるが、プレースホルダ nl2br が "off" になっているとnl2brだけは抑制される。
88- * js JSエンコード。シングルクォートとダブルクォート、改行のエスケープが行われる。
89- *
90- * ただ、エンコードされるのは翻訳テキスト中の平テキスト部分のみ。プレースホルダはエンコードされないので注意されたい。
91- *
92- * {(sample:あなたの名前 name=`ほ<げ>子`)}
93- * あなたの名前,あなたの<名前>は[name]です。
94- * ↓
95- * あなたの&lt;名前&gt;はほ<げ>子です。
96- *
97- * これは上述の例のように、属性値にHTMLタグをセットできるようにするため。
98- * また、属性の値は普通ビュー変数でセットされる場合がほとんどであるため、二重エンコードを避けるようにする意味もある。
99- *
100115 * ●"!" で始まるシンボル。
101116 *
102117 * ・!include:
103118 * 規約など、かなり長いテキストファイルをインクルードしたい場合は次のようなインクルード命令を書くことが出来る。
104119 *
105- * {(!include.sample)}
120+ * {[i18n _='!include:sample']}
106121 *
107122 * この場合、sample.txt というファイルの内容が読み込まれて翻訳テキストとして展開される。プレースホルダも通常通り使える。
108123 *
109124 * ・!db:
110- * テーブル text_info から翻訳テキストを読み込みたい場合に使用する。
125+ * テーブル text_master, text_info から翻訳テキストを読み込みたい場合に使用する。
126+ * 例えば symbol_group='foo' AND symbol_id=5 のレコードを使いたい場合は次のようになる。
111127 *
112- * {(!db:foo)}
128+ * {[i18n _='!db:foo#5']}
113129 *
114- * text_info から symbol_code='foo' のレコードが読み込まれて、そのテキストが使用される。
130+ * text_master と text_info のどちらを使うのかは symbol_group の値によって自動的に求められる。
115131 *
116- * ●対応する必要のあるファイルと各ファイルにおける対策
132+ * ●画像などのアセット選択
117133 *
134+ * 文字が含まれる画像など、言語によって切り替えなければならないアセットがある。
135+ *
136+ * たとえば、/foo/bar.jpg で多言語対応の必要があるなら、/foo/bar.ja.jpg, /foo/bar.en.jpg, ... などと各国語版をあらかじめ用意して、
137+ * 次のように参照すると...
138+ * <img src="{[asset_url path='foo/bar.jpg']}" />
139+ * 日本語選択状態なら bar.ja.jpg が、英語選択状態なら bar.en.jpg が出力される。
140+ *
141+ * /foo/bar.xxx.jpg などの言語対応版がない場合は /foo/bar.jpg がそのまま使われる。
142+ *
143+ * ●多言語対応のTips的まとめ
144+ *
118145 * .html
119- * 基本的にはタグのようなプログラム要素はそのまま残し、翻訳テキストの部分をシンボルにするのだが...
146+ * 基本的にはHTMLタグのようなプログラム的要素はそのまま残し、翻訳対象テキストの部分をシンボルにするのだが...
120147 *
121148 * <table>
122149 * <tr>
@@ -127,18 +154,17 @@
127154 * ↓
128155 * <table>
129156 * <tr>
130- * <th>{(sample:タイトル)}</th>
131- * <td>{(sample:本文)}</td>
157+ * <th>{[i18n _='sample:タイトル']}</th>
158+ * <td>{[i18n _='sample:本文']}</td>
132159 * </tr>
133160 * </table>
134161 *
135- * 次のようにタグが絡みこんだテキストなどもある。
162+ * 次のようにタグが絡みこんだテキストもあるだろう。
136163 * <div>この<span class="strong">名前</span>の意味を知りたいなら<a href="...">ここ</a>をクリック</div>
137164 *
138165 * これをタグでぶつ切りにしてシンボルにすると翻訳側はやってられないので、次のようにして...
139- * <div>{(sample:この名前の意味 link=`<a href="...">` keyword=`<span class="strong">`)}</div>
140- *
141- * 翻訳テキストの中にある程度潜り込ませる。
166+ * <div>{[i18n _='sample:この名前の意味' link=>'<a href="...">' keyword=>'<span class="strong">']}</div>
167+ * 翻訳テキストの中にプログラム的要素をある程度潜り込ませるしかない。
142168 * この名前の意味,この[keyword]名前[/]の意味を知りたいなら[link]ここ[/]をクリック
143169 *
144170 * .php
@@ -150,30 +176,31 @@
150176 * <div><?php echo EXPLAIN ?></div>
151177 * ↓
152178 * <?php
153- * define('EXPLAIN', '{(sample:エラーメッセージ)}');
179+ * define('EXPLAIN', 'sample:エラーメッセージ');
154180 * ?>
155- * <div><?php echo EXPLAIN ?></div>
181+ * <div>{[i18n _=$smarty.constant.EXPLAIN]}</div>
156182 *
157183 * マスタデータ
158- * 翻訳テキストを locale 以下の .csv ファイルに用意して、マスタにはシンボルを格納するか、あるいは、
159- * マスタデータのテキストを text_info に集約して、主キー値などで参照するか、になるだろう。
184+ * マスタデータにテキストが含まれるなら、そのテキスト列を text_master に正規化するか、あるいは、シンボル名をテキスト列に格納するかの
185+ * いずかになるだろう。
160186 *
187+ * どちらにせよ、出力時にテキストシンボルとして翻訳するように記述することになる。
188+ *
161189 * ユーザデータ
162- * 翻訳不能なので、そのまま出力すれば良い。ただ、"{(" などが含まれている場合にシンボル展開エンジンが作用しないように留意する必要はある。
190+ * そもそも翻訳できるものではないので、言語に関わらずそのまま出力するしか無い。
163191 *
164192 * 画像
165- * 文字が含まれる画像も国際化対応の必要がある。
166- * たとえば、sample.jpg で国際化対応の必要があるなら、sample.ja.jpg, sample.en.jpg, ... などと各国語版をあらかじめ用意して、
167- * translateAsset() などで対応する。
193+ * 上述の通り。
168194 *
169195 * .js
170- * 画像と同じように対応するのだが、「各国語版をあらかじめ用意して...」というのは面倒臭すぎる。
196+ * 静的ファイルという意味では画像と同じように対応するのだが、「各国語版をあらかじめ用意して...」というのは面倒臭すぎる。
171197 * テキスト部分をシンボル化した .js ファイルをひとつだけ用意して、各国語版が必要になったときにシンボル展開したjsファイルを
172- * 自動作成&キャッシュするような仕組みが必要になるだろう。
173- * translateAsset() で行われている。
198+ * 自動作成&キャッシュするような仕組みが必要になるだろう。上記の通り。
174199 *
175- * CSS
176- * contentプロパティにテキストが含まれている可能性があるが、国際化対応する必要がないようにルールを決めたほうが良いだろう。
200+ * .css
201+ * contentプロパティにテキストが含まれている可能性があるが、「使用しない」などのルールで回避できると思われる。
202+ * background-image: などで多言語対応の必要がある画像などを参照する可能性がある。sample.jpg などの固定化されたパスで言語に応じたアセットを
203+ * 返せるような特殊なアクションを用意するか、あるいは、こういった画像に言語的要素を含めないという運用ルールで回避することになる。
177204 *
178205 * 日時出力
179206 * タイムゾーン対応が必要になる…と言っても言語だけではタイムゾーンは絞れないので、その辺どうするかはプロジェクト次第。
@@ -207,7 +234,7 @@
207234 if( is_null($primaryLang) )
208235 $primaryLang = self::decidePrimaryLang();
209236
210- // 指定されていない場合は自動で決定する。
237+ // 言語参照順序を決定する。
211238 $this->langOrder = self::getLangOrder($primaryLang);
212239 }
213240
@@ -248,9 +275,9 @@
248275 *
249276 * 第一引数を省略して、テキストシンボル名を第二引数に含めることも出来る。この場合はキー "0" で指定する。
250277 * 例えばこのような呼び出しなら...
251- * Localizer::expandSymbol('name_of_file:kore_ha_sample', ['keyword'=>'foo']);
278+ * $localizer->expandSymbol('name_of_file:kore_ha_sample', ['keyword'=>'foo']);
252279 * こうすることも出来る。
253- * Localizer::expandSymbol(['name_of_file:kore_ha_sample', 'keyword'=>'foo']);
280+ * $localizer->expandSymbol(['name_of_file:kore_ha_sample', 'keyword'=>'foo']);
254281 */
255282 public function expandSymbol($symbol, $places = [], $encode = null) {
256283
@@ -299,8 +326,12 @@
299326 return $this->getLangFileContent($key);
300327
301328 case '!db':
302- return TextInfo::getLangText($key, $this->langOrder);
303329
330+ // キー文字列を "#" で区切って symbol_group, symbol_id とする。
331+ [$group, $id] = str_snap($key, '#');
332+
333+ return TextMaster::getText($group, $id, $this->langOrder);
334+
304335 // "!" で始まらないシンボルは言語ファイルにある。
305336 default:
306337
@@ -375,8 +406,8 @@
375406 if(0 < strlen($basedir) && !str_end($basedir, '/') && !str_begin($path, '/'))
376407 $basedir .= '/';
377408
378- // 末尾の拡張子部分に ".LANG" という文字列を挿入してパスルールとする。
379- $check = $basedir . preg_replace('/\.\w+$/', ".LANG$0", $path);
409+ // 末尾の拡張子部分に ".:LANG:" という文字列を挿入してパスルールとする。
410+ $check = $basedir . preg_replace('/\.\w+$/', ".:LANG:$0", $path);
380411
381412 // 使用できる言語ファイルを探す。
382413 $find = $this->searchLangPath($check);
@@ -388,10 +419,10 @@
388419 //----------------------------------------------------------------------------------------------------------
389420 /**
390421 * 引数に指定されたパスルールで言語に適合する実在パスを返す。
391- * 文字列中の "LANG" と記述された部分を言語コードに置き換えて、フォールバックしながら実在するかを調べる。
422+ * 文字列中の ":LANG:" と記述された部分を言語コードに置き換えて、フォールバックしながら実在するかを調べる。
392423 *
393- * param 調べたいパス。この文字列中の "LANG" と記述された部分が言語コードに置き換えられてチェックされる。
394- * return "LANG" と記述された部分をフォールバックしながら言語コードに置き換えて、最初に見つけたパス。
424+ * param 調べたいパス。この文字列中の ":LANG:" と記述された部分が言語コードに置き換えられてチェックされる。
425+ * return ":LANG:" と記述された部分をフォールバックしながら言語コードに置き換えて、最初に見つけたパス。
395426 * いずれの言語でも見つからない場合は null。
396427 */
397428 public function searchLangPath($path) {
@@ -400,7 +431,7 @@
400431 foreach($this->langOrder as $lang) {
401432
402433 // その言語の派生パスを取得。
403- $check = str_replace('LANG', $lang, $path);
434+ $check = str_replace(':LANG:', $lang, $path);
404435
405436 // そのパスにファイルがあるならそれ。
406437 if( file_exists($check) )
@@ -424,9 +455,9 @@
424455 $oldpath = $path;
425456 $path = $this->getSpecificFile($path, AppMojo::path('htdocs'));
426457
427- // 言語派生版がなく、求められているパスが js ならば翻訳版を準備する。
428- // ただし、.min.js は多分違うし、Smarty展開すると誤爆する確率が高いので無視。
429- if($path == $oldpath && pathinfo($path, PATHINFO_EXTENSION) == 'js' && !str_end($path, '.min.js'))
458+ // 言語派生版がなく、求められているパスが js, txt, html ならば翻訳版を準備する。
459+ // ただし、.min.js は多分違うし、展開すると誤爆が心配になるので無視。
460+ if($path == $oldpath && in_array(strtolower(path_extension($path)), ['.js', '.txt', '.html', '.htm']) && !str_end($path, '.min.js'))
430461 $path = $this->readyTranslatedScript($path);
431462
432463 return $path;
@@ -499,20 +530,19 @@
499530 */
500531 public static function decidePrimaryLang() {
501532
502- // URLで言語が指定されている場合はそれを使う。ただし、扱っていない場合は無視する。
503- if(@$_GET['lang']) {
504- $lang = self::languageExists($_GET['lang']);
505- if($lang) return $lang;
506- }
533+ // アクションの属性(つまりURL)で言語が指定されている場合はそれを使う。ただし、扱っていない場合は無視する。
534+ $lang = Mojo::getRequestedAction()?->attributes['lang'] ?? null;
535+ $lang = self::languageExists($lang);
536+ if($lang) return $lang;
507537
538+ // ユーザの言語設定などがあるならここで判定することになる。
539+ // ...
540+
508541 // HTTPリクエストヘッダに Accept-Language がある場合はそれを見て自動決定する。
509542 if( isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ) {
510543
511544 // Accept-Language にリストされている言語コードを取得。Q値は…まあいいや。
512- $desires = array_each(explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']), function($v){
513- [$ret] = str_snap($v, ';');
514- return $ret;
515- });
545+ $desires = array_each(explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']), fn($v)=>str_snap($v, ';')[0]);
516546
517547 // リスト先頭からひとつひとつ見て、扱っている言語を見つけたらそれを返す。
518548 foreach($desires as $desire) {
@@ -529,7 +559,7 @@
529559 /**
530560 * フォールバックも考慮して参照する言語コードの序列を返す。
531561 *
532- * param 最優先して参照する言語。
562+ * param 最優先して参照したい言語。
533563 * return 参照する言語コードの序列を表した配列。
534564 */
535565 public static function getLangOrder($primaryLang) {
@@ -546,6 +576,8 @@
546576 */
547577 private static function languageExists($lang) {
548578
579+ if( !$lang ) return null;
580+
549581 // 言語コードのサブタグ("en-US" の "US" の部分)は無視するのだが...
550582 [$lang, $sub] = str_snap($lang, '-');
551583
@@ -564,7 +596,7 @@
564596 */
565597 private static function getLangDir($lang) {
566598
567- return AppMojo::path('resources') . pathf("/locale/%s", $lang);
599+ return AppMojo::path(pathf("resources/locale/%s", $lang));
568600 }
569601
570602 //----------------------------------------------------------------------------------------------------------
--- lib/RsetUtil.php (revision 181)
+++ lib/RsetUtil.php (revision 182)
@@ -73,34 +73,45 @@
7373 * 引数に指定された結果セット形式の配列から、特定のカラムの値をキーとする新しい配列を作成する。
7474 *
7575 * 例)
76- * 次のような配列を第一引数に指定し...
77- * array(
78- * array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
79- * array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
80- * array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
81- * );
76+ * 次のような配列を第一引数に指定し...
77+ * array(
78+ * array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
79+ * array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
80+ * array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
81+ * );
8282 *
83- * 第二引数に 'alpha' を指定すると、次のような配列を返す。
84- * array(
85- * 5 => array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
86- * 6 => array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
87- * 7 => array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
88- * );
83+ * 第二引数に 'alpha' を指定すると、次のような配列を返す。
84+ * array(
85+ * 5 => array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
86+ * 6 => array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
87+ * 7 => array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
88+ * );
8989 *
90- * また、第二引数に配列 array('beta', 'alpha') を指定すると、次のような二次元配列を返す。
91- * array(
92- * 10 => array(
93- * 5 => array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
94- * 6 => array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
95- * ),
96- * 11 => array(
97- * 7 => array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
98- * ),
99- * )
90+ * また、第二引数に配列 array('beta', 'alpha') を指定すると、次のような二次元配列を返す。
91+ * array(
92+ * 10 => array(
93+ * 5 => array('alpha' => 5, 'beta' => 10, 'gamma' => 21),
94+ * 6 => array('alpha' => 6, 'beta' => 10, 'gamma' => 22),
95+ * ),
96+ * 11 => array(
97+ * 7 => array('alpha' => 7, 'beta' => 11, 'gamma' => 23),
98+ * ),
99+ * )
100100 *
101- * 第三引数にtrueを渡すと、キーに変換した列を削除するようになる。
101+ * 第三引数にコールバック関数を渡すと、最終的に格納する値を制御できる。
102+ * $ret = RsetUtil::keyShift($source, array('beta', 'alpha'), fn($rec)=>$rec['gamma']);
103+ * 次のようになる。
104+ * array(
105+ * 10 => array(
106+ * 5 => 21,
107+ * 6 => 22,
108+ * ),
109+ * 11 => array(
110+ * 7 => 23,
111+ * ),
112+ * )
102113 */
103- public static function keyShift($source, $keys, $unsetShift = false) {
114+ public static function keyShift($source, $keys, $processor=null) {
104115
105116 // キー変換列を正規化。
106117 $keys = (array)$keys;
@@ -111,28 +122,14 @@
111122 // 結果セットに含まれるレコードを一つずつ戻り値の該当箇所に振り分けていく。
112123 foreach($source as $row) {
113124
114- // レコードがオブジェクトになっている場合は unset する前にクローンしておく。
115- if($unsetShift && $row instanceof ArrayAccess)
116- $row = clone $row;
125+ // キー変換列の値を取り出す。
126+ $path = ArrayUtil::pick($row, $keys);
117127
118- // レコードを格納するべき、戻り値の該当箇所を変数 $corsor で指し示すようにする。
119- $cursor = &$result;
120- foreach($keys as $key) {
128+ // 戻り値の該当箇所を取得。
129+ $cell = &ArrayUtil::dig($result, $path);
121130
122- // 列の値を取得。
123- // ついでに、キー変換列を削除するならここで削除しておく。
124- $value = $row[$key];
125- if($unsetShift) unset($row[$key]);
126-
127- // 戻り値に該当箇所がないなら作成しておく。
128- if( !array_key_exists($value, $cursor) )
129- $cursor[$value] = array();
130-
131- $cursor = &$cursor[$value];
132- }
133-
134- // 格納するべき場所にレコードを格納。
135- $cursor = $row;
131+ // そこにレコードを格納するのだが、第3引数が指定されているならコールバックして戻り地を格納する。
132+ $cell = $processor ? $processor($row) : $row;
136133 }
137134
138135 // リターン。
--- lib/UrlUtil.php (revision 181)
+++ lib/UrlUtil.php (revision 182)
@@ -221,7 +221,7 @@
221221 public static function versionStamp($path) {
222222
223223 // 静的ファイルの更新時刻を取得。ファイルがなくてもエラーにならないようにする。
224- $mtime = (int)@filemtime( static::assetPhys($path) );
224+ $mtime = (int)@filemtime( Mojo::path("htdocs/{$path}") );
225225
226226 // "?"に続けて更新日時をつなげてリターン。
227227 return $mtime ? '?cache='.$mtime : '';
--- lib/tables/TextMaster.php (nonexistent)
+++ lib/tables/TextMaster.php (revision 182)
@@ -0,0 +1,68 @@
1+<?php
2+
3+class TextMaster extends MojoRecord {
4+
5+ // // text_group の列挙。
6+ // const GROUPS = array(
7+ // );
8+
9+ //----------------------------------------------------------------------------------------------------------
10+ /**
11+ * 引数で指定されたテキストを、指定された言語順で探す。
12+ *
13+ * param グループコード。text_group の値。
14+ * param テキストID。text_id の値。
15+ * param 参照する言語順。
16+ * return 指定されたテキストの内容。見つからない場合は null。
17+ */
18+ public static function getText($group, $id, $langOrder) {
19+
20+ // このクラスで定義されていないグループ名の場合は TextInfo のほうにあるかも。
21+ // if( !array_key_exists($group, static::GROUPS) && static::class != 'TextInfo' )
22+ // return TextInfo::getText($group, $id, $langOrder);
23+
24+ // 使用言語順に従って取得。
25+ foreach($langOrder as $lang) {
26+ $record = static::getSolid($group, $id, $lang);
27+ if($record) return $record['text_content'];
28+ }
29+
30+ return null;
31+ }
32+
33+ //----------------------------------------------------------------------------------------------------------
34+ /**
35+ * 引数で指定されたテキストのグループを、指定された言語順で探す。
36+ *
37+ * param グループコード。text_group の値。
38+ * param 参照する言語順。
39+ * return テキストID => テキスト内容 の配列。優先言語でテキストIDが一部欠損している場合は、第2言語以降のテキストがマージされる。
40+ */
41+ public static function getTexts($group, $langOrder) {
42+
43+ // このクラスで定義されていないグループ名の場合は TextInfo のほうにあるかも。
44+ // if( !array_key_exists($group, static::GROUPS) && static::class != 'TextInfo' )
45+ // return TextInfo::getTexts($group, $langOrder);
46+
47+ // 引数で指定されたグループのテキストを全て取得。
48+ $records = static::recordsWhere(['text_group'=>$group]);
49+
50+ // 言語 => テキストID の二次元配列として、テキストの内容を取得する。
51+ $texts = RsetUtil::keyShift($records, array('lang_code', 'text_id'), fn($rec)=>$rec['text_content']);
52+
53+ // 使用言語順に従って テキストID => テキスト内容 の配列を得るのだが、優先言語でテキストIDが一部欠損している場合にマージされるようにする。
54+ $result = array();
55+ foreach($langOrder as $lang)
56+ $result += $texts[$lang] ?? array();
57+
58+ // テキストIDで並べ替えてリターン。
59+ ksort($result, SORT_NUMERIC);
60+ return $result;
61+ }
62+
63+
64+ // 基底メンバの上書き。
65+ //==========================================================================================================
66+
67+ protected static $primaryColumn = array('text_group', 'text_id', 'lang_code');
68+}
--- resources/initialize.php (revision 181)
+++ resources/initialize.php (revision 182)
@@ -832,3 +832,9 @@
832832 if( !$condition )
833833 throw new IrregularAccess( sprintf($errstr, ...$vals) );
834834 }
835+
836+// 多言語対応のヘルパ関数。Localizer::expandSymbol() のショートカット。
837+function i18n($symbol, $places = [], $encode = null) {
838+
839+ return Localizer::getLocalizer()->expandSymbol($symbol, $places, $encode);
840+}