ロケール

ルールのtitle/description、検出箇所のsummary/hintはすべて、キーによる辞書引きでローカライズされます。言語ごとにハードコードしているわけではありません。このページで扱うのは、エンジンが結果やレポートに出力するメッセージの言語です。このドキュメントサイトの日本語訳とは別のものです。エンジンには日本語(ja)の辞書も同梱されており、WCAGの用語はWAICによるWCAG 2.2の日本語訳に従っています。

現在のロケール対応状況

ロケールファイルキー数未翻訳
en (English)src/i18n/en.json1334—(基準・フォールバック用のセット)
de (German)src/i18n/de.json13340
es (Spanish)src/i18n/es.json13340
fr (French)src/i18n/fr.json13340
ja (Japanese)src/i18n/ja.json13340

「未翻訳」は、英語のテキストと1文字残らず一致する値の数です。ただし、フランス語の「Orientation」「Interruptions」「Occurrences」、ドイツ語の「Frame」「Frames」「Element」、スペイン語の「Selector」のように、その言語でも同じ語であるのが正しいものは除きます。こうした語は、照合した英語のテキストとともに一覧に登録され、翻訳済みとして数えられます。英語が変わるとその登録は当てはまらなくなり、誰かが改めて確認するまで、その文字列は未翻訳として数えられます。訳文の中に英単語が残っていても、この数には表れません。それはテキストを読んで見つけるもので、上のロケールには1つも残っていません。また、訳が正確か、その言語で通常使われるアクセシビリティの用語を使っているかも測りません。それには、その言語を読める人が必要です。

パッケージからカバー率を読み取る

同じ数値は、インストールしたパッケージに含まれる辞書から実行時に取得できます。ツールやドキュメントサイトは、リポジトリをチェックアウトしなくても表示できます。

const { getLocaleCoverage } = require('@surea11y/core');
 
getLocaleCoverage();
// {
// sourceLocale: 'en',
// totalKeys: 1334,
// locales: [
// { locale: 'de', total: 1334, translated: 1334, missing: 0, orphaned: [], percent: 100 },
// { locale: 'es', total: 1334, translated: 1334, missing: 0, orphaned: [], percent: 100 },
// ...
// ]
// }

ロケールごとに、totalは比較の基準となる英語のキーの数、translatedは英語と異なる値、または同じであるのが正しいと登録された値の数、missingは欠けているキーの数(英語で表示されます)、orphanedは英語にはないのにそのロケールにあるキーです。total - translatedが上の「未翻訳」の列で、欠けているキーも含みます。プロファイルが意図してロケールから外したキーは数えません。リポジトリ自体のカバー率レポートとこの関数は1つの実装を共有しているため、結果は常に一致します。レポートはcoreの辞書と各プロファイルの辞書を別々に数え、この関数はスキャンで使われる統合済みの辞書を数えます。サポート対象のエクスポートです(API_STABILITY.mdの拡張ポイント)。

ロケールファイルはただのJSONで、キーから翻訳済み文字列へのフラットなマップをen.jsonと同じキー順で並べたものです。それ以外は何も入っていないので、言語を追加するのに必要なのはテキストの編集だけで、コードに触れることはありません。

どのロケールもen.jsonと同じキーをすべて持っています。この状態を保つのがnpm run i18n:syncの役目です。en.jsonを変更したら実行してください。英語以外のロケールファイルをすべて書き換えて一致させます。新しいキーは追加し、en.jsonからなくなったキーは削除し、既存の翻訳はそのまま残します。追加されたキーには英語のテキストが仮に入り、誰かが置き換えるまでは未翻訳として扱われます。

ロケールの指定

runDomRulesInPage(url, null, { locale: 'fr' }, null);

省略した場合のデフォルトは'en'です。どんな文字列でも受け付けます。認識できないロケールがエラーになることはありません。

ロケールの決まり方

処理は2段階で、この順に行われます。この2つを混同するのがよくある混乱のもとなので、分けて説明します。

ステップ1:辞書を選ぶ(スキャンごとに1回)

  1. 指定したコードに一致する辞書を使います。deならde.jsonが見つかります。大文字・小文字は区別しません。pt-br、pt-BR、PT-BRのいずれでもpt-BR.jsonが見つかります。
  2. 見つからなければ、最初の-以降を取り除いて探します。de-DEでもde-ATでもde.jsonが見つかります。
  3. それでもなければ英語を使います。

つまりde-DE.jsonが必要になるのは、オーストリアとドイツでドイツ語の表現を実際に変えたい場合だけです。de.jsonを用意すれば、ドイツ語のバリエーションはすべてカバーされます。

ステップ2:文字列ごとに解決する(文字列単位)

選ばれた辞書の中で、文字列は1つずつ個別に引かれます。

  1. 選ばれた辞書からそのキーの値を取ります。
  2. そのキーがなければ、英語の値を取ります。
  3. 英語にもない場合(組み込みのキーでは起こらないはずですが、独自に作ったキーでは起こりえます)は、ルール自体が持っているテキストをそのまま使います。

結果が空欄やundefinedになったり、エラーが投げられたりすることはありません。翻訳が途中でも、訳がある部分は指定した言語で、それ以外はすべて英語で表示されます。

実際にどのロケールになったかを確認する

エラーにせず英語にフォールバックするこの仕組みには、1つ欠点があります。ビルドに含まれていない言語を指定すると、自然な英語が返ってきて、文字列を見てもそのことはわかりません。そのため、どの結果にも解決の結果が先頭に1度だけ記録されます。

"engine": {
"tag": "a11ycore",
"schemaVersion": "1.0.0",
"locale": { "requested": "ko", "resolved": "en", "reason": "unknown-locale" }
}

requestedは指定した値(前後の空白を除去したもの。何も渡さなかった場合や文字列以外を渡した場合はen)、resolvedは実際に使われた辞書、reasonは次のいずれかです。

reason意味
ok指定したとおりの辞書が使われ、その辞書にすべてのキーがそろっています。
primary-subtag指定したコードのサブタグに対応する辞書がなかったため、その基本言語が使われました。de-DEを指定してdeになった場合がこれです。通常の想定どおりの動作で、対処は不要です。大文字・小文字の違いだけならこれには当たらず、DEはokを返します。
dictionary-not-loadedプロジェクトとしてはその言語を提供していますが、このエンジンには含まれておらず、別途渡されてもいません。実際には、ロケール用の別ファイルを読み込まずにスタンドアロンのブラウザー用バンドルを使った場合です。
unknown-localeプロジェクトにその言語の翻訳がまったくないため、英語が使われました。現時点ではkoもpt-BRもこれに該当します。
partial-dictionary辞書は使われましたが一部のキーが欠けているため、その文字列だけ英語にフォールバックしました。

この一覧は今後増えるものとして扱ってください。後のリリースで値が追加されることがあるので、必要な値だけを照合し、それ以外はデフォルトの分岐に流すようにしてください。

特定の言語が必要なら、CIで確認すべき条件はrequested !== resolvedです。ただし、これは問題のないprimary-subtagの場合にも真になります。基本言語での一致で十分なら、reason === 'unknown-locale' || reason === 'dictionary-not-loaded'で判定してください。結果の中でのこのフィールドの位置は出力スキーマを、何が保証されているかはAPI_STABILITY.mdを参照してください。

辞書の置き場所

使える言語は、エンジンの読み込み方によって決まります。

読み込み方使える言語
require('@surea11y/core')すべてのロケールが組み込み済みです。設定は不要です。
バインディング(Playwright、Cypressなど)すべてのロケールが組み込み済みです。設定は不要です。
surea11y.browser.js英語のみです。それ以外の言語は、このファイルの後にsurea11y.i18n.<locale>.jsを読み込んでください。

バンドルを分けているのは、それを使うすべてのページにネットワーク経由で配信されるうえ、すべての言語を必要とするページはないからです。英語だけを内蔵し、残りを任意にしたことで、ほかの言語はダウンロードに含まれず、言語を追加しても増えなくなりました。各別ファイルsurea11y.i18n.<locale>.jsの大きさは、置き換える英語のテキストとほぼ同じで(現在200〜250 KB)、ページが読み込むのは使うものだけです。それ以外は何も変わりません。Nodeパッケージとバインディングには影響しません。

<script src="surea11y.browser.js"></script>
<script src="surea11y.i18n.ja.js"></script>
<script>
const result = a11ycore.runa11yCoreInPage(location.href, null, { locale: 'ja' }, null);
</script>

ファイルを読み込んでいない言語を指定すると英語になり、engine.locale.reasonはdictionary-not-loadedになります。これは、プロジェクトにその言語の翻訳がまったくないことを示すunknown-localeとは別のものです。

Nodeでは、同じ別ファイルがモジュールになります。require('@surea11y/core/i18n/de')は{ locale, messages }を返し(localeは'de'、messagesはキーからテキストへのフラットなマップ)、バンドルは必要ありません。

パックのメッセージはパック自身の辞書にあり、パックがロケールごとに列挙します。パックに辞書がないロケールでは、そのメッセージは英語で表示されます。

確認手順の辞書

判定できなかった検出結果に人がどう答えるかを示す手順(howToCheck。出力スキーマを参照)は、同じ5つのロケールについて、専用の辞書にあります。手順がバンドルやページに入らないよう、エンジンの辞書とは分けてあり、スキャンの後にNodeで、またはそれをインポートするコードで追加されます。core 1.14.0以降です。

読むもの方法
runDomRulesInPage, addHowToCheck, howToCheckFor, renderHtmlReportインストールしたパッケージから読み込みます。ロケールを小文字にして探し(fr-ca、次にfr)、最後は英語です。
ブラウザーで動くコード(Cypressのspec)@surea11y/core/how-to-checkのapplyHowToCheck(result, { en, fr })を使い、辞書は@surea11y/core/review/<locale>としてインポートします。
a11ycore.openReport(result)(ブラウザーバンドル)バンドルの隣から取得するか、{ reviewDictionaries }として渡します。

ロケールにない手順は、メッセージと同じく英語で表示されます。パックのルール独自の手順は、どのロケールでも英語です。getLocaleCoverage()はこれらの辞書を数えません。手順はキーを持たないので、別のロケールで表示するには、もう一度追加します。

const { addHowToCheck } = require('@surea11y/core');
 
const inFrench = addHowToCheck(result, { locale: 'fr' });

辞書を自分で渡す

engineOptions.messagesは{ [locale]: { key: value } }を受け取り、組み込みの辞書や別ファイルから読み込んだ辞書よりも優先されます。一部の文字列だけを上書きしたい場合や、社内で独自に管理している言語を使う場合に便利です。

runDomRulesInPage(url, null, {
locale: 'de',
messages: { de: { img_altPresent_title: 'Eigener Text' } }
}, null);

渡さなかったキーは通常どおりフォールバックするので、一部だけの上書きでも問題ありません。

言語ごとの照合

5つのルールは既知の語句でテキストを判断するため、メッセージに加えて語のリストを持っています。link-name-quality(「click here」「こちら」)、heading-quality(「Untitled」「見出し」)、form-control-label-quality(「Label」「入力欄」)、page-title-patterns(「Home」「トップページ」)、media-alternative-transcript-evidence(「transcript」「文字起こし」)です。各リストはen、de、es、fr、jaに対応しています。skip-linkルールも、同じ5つの言語でスキップリンクの表現を認識します。

英語は常に確認します。それに加えて、最も近いlang属性(ページタイトルの場合は<html>要素のもの)から得た要素自身の言語のリストを確認します。リンクの上の木構造のどこにもlangが宣言されていない場合、link-name-qualityはすべての言語のリストを確認します。リンクがどの言語でもありうるためです。すべてのリストをどこでも確認すると、ある言語では一般的な語でも別の言語では実際の名前になる語、例えば英語のページの「Suite」や「Plus」を検出してしまうためです。文字起こしを表す語は例外です。どの言語でも紛れがなく、ページが別の言語の文字起こしにリンクすることもあるため、常にすべて確認します。テキストは最初にNFKC正規化するので、「見出し2」のような全角の形も一致します。

page-title-patternsは、タイトルが短すぎるかを判断するとき、中国語、日本語、韓国語の文字を1文字につき2文字として数えます。「お問い合わせ」は6文字で完結したタイトルだからです。

キーが使われる場所

複数のキー名前空間があり、どれも同じ方法で解決されます。

  • ルール単位:meta.i18n.titleKey/meta.i18n.descriptionKey。checksResults[]の各エントリーで、ルールのtitle/descriptionを解決します。
  • HTMLレポート:report_*キーは、@surea11y/core/reportが生成するページのラベルです。見出し、表の列、判定結果と重大度の名前、見出し文、ページ送りが該当します。レポートはスキャンで解決されたロケールでこれらを読むため、辞書を翻訳すればレポートも翻訳されます。
  • 複合ルール単位:rulesResults[]の各集約のtitle/descriptionを解決します。達成基準の名前には、各言語で公開されている名称を使います。
  • 検出箇所単位:i18n.summaryKey/i18n.hintKey。{{placeholder}}の埋め込みにはi18n.paramsを使います。検出箇所のsummary/hintを解決します。傾きのi18n.whyKey/i18n.cautionKeyも同じ方法で、そのwhyとcautionを解決します。詳しくは出力スキーマを参照してください。
  • 確認手順:確認手順の辞書の<prefix>_review_<CODE>_step<n>、_pass、_fail。判定できなかった検出箇所のhowToCheckです。

ルール、複合ルール、検出箇所のキーは解決済みのテキストと一緒に結果に含まれるので、保存した結果から、再スキャンせずに別のロケールで表示し直せます。