メインコンテンツへスキップ
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
はじめにWebReactSwiftUIJetpack ComposeFlutterReact Nativeプリセット自分のデザインシステムを作るAI エージェントと Tanrei を使うブランドデザイントークンデザインシステムとカスタマイズTheme Studioカラータイポグラフィアイコン形状エレベーションモーションインタラクション状態振る舞いレイアウトアクセシビリティ言葉カラートークンタイプスケールトークン形状トークンエレベーショントークンモーショントークン状態トークンスペースと寸法のトークンコンポーネントボタンメインアクションボタンセグメントボタンボタングループメニュー付きボタンツールバーコピーボタンチェックボックスラジオボタンスイッチチップメニュースライダースライダーフィールドカレンダーDateTimePickerフィールドグループラジオグループチェックボックスグループセレクトセレクトフィールドメニューバーファイルアップロードファイルピッカー 上級者向けドロップゾーンテキスト入力欄 上級者向けテキストフィールドチップフィールドフィールドコンボボックス 上級者向けコンボボックスフィールド数値入力欄 上級者向け数値フィールドカラー入力欄 上級者向けカラーフィールドワンタイムコード入力欄 上級者向けワンタイムコードフィールド埋め込みテキスト入力欄 上級者向けチャットコンポーザーカード区切り線モーダルレイヤー 上級者向けダイアログリストリスト行ボトムシートサイドシートカルーセルコードブロックテーブルポップオーバーステップアコーディオン説明リスト図アプリシェルページヘッダーツリービュータイムライン画像プレビューハンドルサイズ変更できる枠スクロールエリアバッジプログレスインジケーターメーターナンバーティッカーローディングインジケータースナックバーツールチップキーボードショートカットタグインラインコードコールアウトアバター空の状態タググループスケルトンファイル項目色見本マーキーテキストタブタブビューアプリバーナビゲーションバーナビゲーションドロワー検索リンクページネーション前後のリンクステッパーパンくずリスト目次リンクカードFAB メニューステータスTanrei の作り方コントリビューションライセンスと帰属
ホーム基礎言葉

言葉

コンポーネントは自分の言葉を持ちません。コンポーネントが描いたり読み上げたりするラベル、名前、メッセージは、すべてプロダクトのものです。コンポーネントに書くか、プロダクト全体に一度だけ渡すか、import した英語の言葉のパックから取ります。

content_copyMarkdown で表示

コンポーネントは自分の言葉を持ちません。 どのコンポーネントも、どの言語でも、既定のテキストを持ちません。「必須」も「閉じる」も「OK」もありません。コンポーネントが描いたり読み上げたりする言葉は、プロダクトが渡したものです。自分で書いたものか、自分で import した英語の言葉のパックから取ったものです。

既定値は、それが引き起こす失敗を隠します。日本語のプロダクトが言葉を 1 つ書き忘れると、自分のテキストの横に英語が出ますが、そのことは誰にも報告されません。英語のプロダクトにも、自分の言い回しがあります(「Mandatory」「Done」「Nothing found」)。そこで、コンポーネントは代わりに使う言葉を持たず、足りない言葉があればそれを知らせます。英語の言葉のパックは、そのような代わりの言葉ではありません。プロダクト自身が import しない限り、使われません。

言葉はどこから来るか

コンポーネントは、次の 3 か所をこの順に見ます。ほかの場所は見ません。

場所 渡すもの 例
コンポーネント自身 その 1 つだけについての言葉と、その 1 つだけ言い方を変えたい言葉 label、error-text、close-label="絞り込みを閉じる"
プロダクトの言葉 そのパーツがどこに描かれても同じ言い方をする言葉。言語ごとに一度だけ渡すか、import した英語の言葉のパックから取ります requiredText、closeLabel、previousMonthLabel
プラットフォーム ブラウザーが読み手の言語ですでに持っているもの 月名、曜日名、数字、「午前」、値がないときのブラウザー自身のメッセージ

この 3 つのあとには何もありません。代わりの言語はなく、「とりあえず英語」もありません。

英語の言葉のパック

Tanrei は、プロダクトの言葉を 1 組だけ用意しています。すべてのキーに、平易で癖のない英語の言葉を 1 つずつ(「Required」「Close」「Previous month」など)。一度だけ import します。

import '@tanrei/web/words/en';

これで、setWords() を 1 回呼んだときと同じように、ページ全体の言葉が渡ります。プロダクトの代わりにこれを import するものはありません。@tanrei/web も @tanrei/react も、コンポーネントも、ページの lang も import しません。キーごとのパックの言葉は、キーの一覧にあります。

  • 自分の言葉が優先します。 import のあとに setWords() を呼ぶと、渡したキーはパックの言葉と置き換わります。コンポーネントに書いた言葉は、その両方に優先します。import は実行されたときにすべてのキーを渡すので、それより前に渡した言葉は置き換えられます。
  • パックの言葉から始められます。 モジュールは同じ言葉を値としても出しています。import { englishWords } from '@tanrei/web/words/en'
  • 英語以外のプロダクトでは、その言語の組にないキーはページの組から読まれます。パックを import していれば、それは英語です。自分で書いていないキーに英語を出してよいところでだけ import してください。import しなければ、足りない言葉は報告されます。
  • 隠すべきでないものは隠しません。 パックを import すると、すべてのキーに言葉があるので、missingWords() は空になります。プロダクトが自分で書いていない言葉を探すには、パックを import しないテストで確かめます。

React でも、import は同じ 1 行です。最初に描画するモジュールに書きます。

プロダクトの言葉を一度だけ渡す

Web では、setWords() を一度呼びます。コンポーネントを読み込む前でも後でもかまいません。すでにページにある要素は、すぐにその言葉を描きます。

import { setWords } from '@tanrei/web';

setWords({
  requiredText: '必須',
  optionalText: '任意',
  closeLabel: '閉じる',
  clearLabel: '消去',
  targetLabel: '新しいタブで開きます',
});

渡すのは、使うコンポーネントの言葉だけでかまいません。2 回目の呼び出しは、1 回目にキーごとに加わります。空の文字列を渡したキーは取り除かれます。@tanrei/web/words は、要素を定義せずに同じ関数だけを提供します。@tanrei/react からも使えます。

コンポーネントに書いた属性は、プロダクトの言葉より常に優先されます。

コンポーネント自身に書いた言葉:1 つのフィールドだけ言い方を変える

<tanrei-text-field label="メールアドレス" required required-text="必須"></tanrei-text-field>
<tanrei-text-field label="会社名" required required-text="請求書に必要"></tanrei-text-field>

複数の言語

言語ごとに 1 組ずつ渡します。コンポーネントは、自分を囲むいちばん近い lang 属性の組を読みます。日本語のページでも、英語のページの中の日本語の部分でも、日本語の言葉が読まれます。

setWords({ requiredText: 'Required', closeLabel: 'Close' }, { lang: 'en' });
setWords({ requiredText: '必須', closeLabel: '閉じる' }, { lang: 'ja' });
  • ja の組は ja-JP にも使われます。ja-JP に専用の組を渡した場合は、そちらが使われます。
  • その言語の組にないキーは、lang なしで渡した組から読みます。
  • 言葉はただの文字列です。プロダクトにローカライズのライブラリがあるなら、そこから文字列を渡します。setWords() は翻訳の仕組みではありません。複数形の規則はありません。いくつかの言葉には、コンポーネントが埋める場所があります。言葉の中で読まれるのは、それだけです。

場所のある言葉

文の中には、コンポーネントだけが、その文を言う瞬間に知っている値を含むものがあります。移動のあとに行が何番目になったか、貼り付けでいくつの項目ができたか、などです。その場合も、文全体を書くのはプロダクトです。値の入る場所に、波かっこで囲んだ名前で印を付けます。コンポーネントは、そこに値を入れます。

setWords({ movedText: '{headline}: {position} of {count}' }, { lang: 'en' });
setWords({ movedText: '{headline}:{count} 件中 {position} 番目' }, { lang: 'ja' });

語順は、プロダクトの言語のものです。コンポーネントは場所を埋めるだけで、文にはほかに何もしません。

  • 名前を決めるのはコンポーネントです。 キーごとに場所が決まっていて、キーの一覧に載っています。埋められるのは、その場所だけです。1 つの場所は、1 回書いても、何回書いても、書かなくてもかまいません。
  • 数は、要素の言語の書き方で書きます。 {count} と {position} は整数です。いちばん近い lang の数字と桁区切りで書きます。
  • テキストは、自分の方向を保ちます。 {headline} と {entry} は、コンポーネントが持っているテキストです。行の見出しや、利用者が入力した項目です。マークアップとしては扱わず、そのままのテキストとして入れます。文を描くときと読み上げるときには、そのテキストを周りから切り離します。アラビア語の文に入ったラテン文字の名前は、元の並びのままです。
  • 波かっこそのものは、2 つ続けて書きます。 {{ は {、}} は } として描きます。場所のない言葉は読まれないので、そこでは波かっこを 1 つだけ書きます。
  • 必要な場所が 1 つあります。 entryRemoveLabel は、チップフィールドの項目ごとの削除ボタンの名前です。{entry} がないと、すべてのボタンが同じ名前になります。

開発時には、そのキーにない場所({entri})、対になっていない波かっこ、足りない {entry} を、コンソールに報告します。その場合も、文は書かれたとおりに使います。setWords() は言葉を渡したときに報告します。コンポーネントは、要素に書いた言葉を使うときに報告します。

複数形

コンポーネントには文法がありません。複数形も、序数も、2 つの文からの選び分けもありません。数の扱い方は 3 つあります。どれも、Tanrei には何も求めません。

  1. どの数でも正しい文を書きます。{count} 件中 {position} 番目、追加:{count} 件 などです。
  2. 言葉をコンポーネントに書き、件数が変わったら書き換えます。件数はプロダクトが知っていて、コンポーネントに書いた言葉が常に優先されます。どの文にするかは、プロダクト自身の複数形の規則で決めます。
  3. 数をプロダクト自身が持っている場合、その言葉に場所はありません。テーブルのステータス(「132 件中 1〜20 件」)は、プロダクトが書く 1 つの文字列です。

言葉がないとき

言葉が足りないのは、ラベルのないテキストフィールドと同じく、プロダクトの不具合です。コンポーネントは、利用者への影響を小さくし、開発者にはそれが見えるようにします。

  • 描くはずだった言葉は描きません。 同じことを表せる印があれば、その印を代わりに置きます。必須のタグにはアスタリスク、ピッカーのキャンセルと OK には閉じるアイコンとチェックのアイコン、「翌日」には +1 です。そのような印がなければ何も描かず、その場所の大きさは保ちます。
  • 名前の必要なコントロールは、名前がなくても描かれ、動作します。 カレンダーのナビゲーションボタン、テキストフィールドのサイズ変更のハンドル、ピッカーのアクションは、名前がなくても動作します。例外は数値フィールドのステッパーだけです。名前のないステッパーは支援技術から隠します。隣のスピンボタンが自分で増減するからです。
  • 報告します。 開発時には、コンソールに属性の名前が出ます。1 つの言葉につき、ページごとに 1 回です。<tanrei-field>: required-text is empty and the product's words have no requiredText

アクセシブルネームのないコントロールは、スクリーンリーダーの利用者には使えません。報告は直すためのもので、そのままにしてよいという意味ではありません。

言葉が足りなければテストを失敗させる

missingWords() は、ページを読み込んでから必要になり、誰も渡さなかったキーを返します。プロダクトの画面をひととおり表示するテストの最後に確かめます。英語の言葉のパックを import したページでは、足りない言葉はありません。

import { missingWords } from '@tanrei/web';

test('プロダクトに必要な言葉がすべて渡されている', async () => {
  // …プロダクトの画面を描画する…
  expect(missingWords()).toEqual([]);
});

言葉ではないもの

グリフ、アスタリスク、ダッシュ、数字、ロケールが決める区切りは言葉ではないので、コンポーネントはそのまま描きます。API の名前(プロパティや値)と、コンソールが開発者に伝える内容も言葉ではありません。後者は英語です。キーボードショートカットが描く文字(「Ctrl」「Enter」「⌘」)は、キーそのものに書かれているものです。ショートカットとして読み上げられるのは、渡した label です。

キーの一覧

すべてのキーと、それを読み取るプロパティ、コンポーネントがその文の中で埋める場所、そして英語の言葉のパックでの言葉です。ほとんどのキーは、プロパティと同じ名前です。違うのは 3 つです。コピーボタンの label と failedLabel は copyLabel と copyFailedLabel を読み、パンくずリストの overflowLabel は breadcrumbsOverflowLabel を読みます。

キー 読み取るプロパティ 埋める場所 英語の言葉のパック
addedLabel code-block.addedLabel Added
attachLabel chat-composer.attachLabel Attach files
avatarGroupOverflowLabel avatar.overflowLabel {count} {count} more
backLabel search.backLabel Back
breadcrumbsOverflowLabel breadcrumbs.overflowLabel Show hidden levels
busyLabel button.busyLabel Loading
cancelLabel date-time-picker.cancelLabel Cancel
cancelledText file-upload.cancelledText, list.cancelledText {label}, {position}, {count} {label} returned to {position} of {count}
characterCountNoLimitText text-input.counterNoLimitText {count} Characters: {count}
characterCountText text-input.counterText {count}, {limit}, {remaining} Characters: {count} of {limit}
clearLabel combobox.clearLabel, navigation-drawer.filterClearLabel, search.clearLabel, tree-view.filterClearLabel Clear
closeLabel bottom-sheet.closeLabel, callout.closeLabel, image-preview.closeLabel, popover.closeLabel, side-sheet.closeLabel, snackbar.closeLabel Close
confirmLabel date-time-picker.confirmLabel OK
copiedLabel code-block.copiedLabel, copy-button.copiedLabel Copied
copyFailedLabel code-block.copyFailedLabel, copy-button.failedLabel Could not copy
copyLabel code-block.copyLabel, copy-button.label Copy
dateLabel date-time-picker.dateLabel Date
decrementLabel number-input.decrementLabel Decrease
dialLabel date-time-picker.dialLabel Dial
dragHandleHint file-upload.dragHandleHint, list.dragHandleHint Press Space to lift, the arrow keys to move, Space to drop, Escape to cancel; Enter opens the moves
dragHandleLabel file-upload.dragHandleLabel, list.dragHandleLabel {label} Reorder {label}
dropZoneRefusingText drop-zone.refusingText These files cannot be added here
dropZoneText drop-zone.dropText Drop files here
duplicateEntryText chip-field.duplicateText This is already in the list.
editLabel tree-view.editLabel Name
emptyText combobox.emptyText No matches
endLabel calendar.endLabel, date-time-picker.endLabel End
entriesAddedText chip-field.addedCountText {count} Added: {count}
entryAddedText chip-field.addedText {entry} {entry} added
entryLimitText chip-field.limitText No more can be added.
entryModeLabel date-time-picker.entryModeLabel Input mode
entryRemoveLabel chip-field.removeLabel {entry} Remove {entry}
entryRemovedText chip-field.removedText {entry} {entry} removed
expandLabel snackbar.expandLabel Show full message
externalLabel link.externalLabel, link-card.externalLabel external link
fileCountErrorText chat-composer.countErrorText, file-upload.countErrorText No more files can be added.
fileSizeErrorText chat-composer.sizeErrorText, file-upload.sizeErrorText This file is too large.
fileTypeErrorText chat-composer.typeErrorText, file-upload.typeErrorText This kind of file cannot be added.
filesDroppedText drop-zone.droppedText {count} Files added: {count}
filesRefusedText drop-zone.refusedText {count} Files not added: {count}
filterCountText navigation-drawer.filterCountText, tree-view.filterCountText {count} Matches: {count}
filterEmptyText navigation-drawer.filterEmptyText, tree-view.filterEmptyText No matches
filterPlaceholder navigation-drawer.filterPlaceholder, tree-view.filterPlaceholder Filter
firstPageLabel pagination.firstLabel First page
handleLabel bottom-sheet.handleLabel Resize sheet
highlightedLabel code-block.highlightedLabel Highlighted
hourLabel date-time-picker.hourLabel Hour
imageCountText image-preview.countText {position}, {count} {position} of {count}
imageErrorText image-preview.errorText This image could not be loaded
incrementLabel number-input.incrementLabel Increase
lastPageLabel pagination.lastLabel Last page
liftedText file-upload.liftedText, list.liftedText {label}, {position}, {count} {label} lifted: {position} of {count}
loadingLabel image-preview.loadingLabel, navigation-drawer.loadingLabel, skeleton.label, tree-view.loadingLabel Loading
marqueePauseLabel marquee-text.pauseLabel Pause scrolling text
marqueePlayLabel marquee-text.playLabel Play scrolling text
menubarOverflowLabel menubar.overflowLabel More
minuteLabel date-time-picker.minuteLabel Minute
moveAfterLabel file-upload.moveAfterLabel, list.moveAfterLabel Move down
moveBeforeLabel file-upload.moveBeforeLabel, list.moveBeforeLabel Move up
moveToEndLabel file-upload.moveToEndLabel, list.moveToEndLabel Move to bottom
moveToStartLabel file-upload.moveToStartLabel, list.moveToStartLabel Move to top
movedText file-upload.movedText, list.movedText {label}, {position}, {count} {label}: {position} of {count}
nextDayLabel date-time-picker.nextDayLabel next day
nextImageLabel image-preview.nextLabel Next image
nextItemLabel carousel.nextLabel Next slide
nextMonthLabel calendar.nextMonthLabel, date-time-picker.nextMonthLabel Next month
nextPageLabel pagination.nextLabel Next page
noFileText file-upload.emptyText No file chosen
noOptionsText select.emptyText No options
optionalText field.optionalText, field-group.optionalText Optional
pageCountText pagination.countText {page}, {count} Page {page} of {count}
pageLabel pagination.pageLabel {page} Page {page}
pauseLabel carousel.pauseLabel Pause slideshow
periodLabel date-time-picker.periodLabel AM/PM
playLabel carousel.playLabel Play slideshow
previousImageLabel image-preview.previousLabel Previous image
previousItemLabel carousel.previousLabel Previous slide
previousMonthLabel calendar.previousMonthLabel, date-time-picker.previousMonthLabel Previous month
previousPageLabel pagination.previousLabel Previous page
rangeErrorText date-time-picker.rangeErrorText End must be after start
rangeLabel date-time-picker.rangeLabel Range
removeLabel file-item.removeLabel {name} Remove {name}
removedLabel code-block.removedLabel Removed
requiredText field.requiredText, field-group.requiredText Required
resizeLabel text-input.resizeLabel Resize
retryLabel file-item.retryLabel {name} Retry {name}
segmentEmptyText text-input.segmentEmptyText Empty
selectAllLabel table.selectAllLabel Select all rows
sendLabel chat-composer.sendLabel Send
sortedAscendingLabel table.sortedAscendingLabel sorted ascending
sortedDescendingLabel table.sortedDescendingLabel sorted descending
startLabel calendar.startLabel, date-time-picker.startLabel Start
stepCompletedText stepper.completedText Completed
stepErrorText stepper.errorText Has an error
stepPositionText stepper.positionText {position}, {count} Step {position} of {count}
stopLabel chat-composer.stopLabel Stop
targetLabel button.targetLabel, card.targetLabel, chip.targetLabel, link.targetLabel, link-card.targetLabel, list/list-item.targetLabel, menu/item.targetLabel, menubar/item.targetLabel, navigation-bar/destination.targetLabel, navigation-drawer/destination.targetLabel, search/result.targetLabel, search/result-group.targetLabel opens in a new tab
todayLabel calendar.todayLabel, date-time-picker.todayLabel Today
toolbarOverflowLabel toolbar.overflowLabel More actions
wheelsLabel date-time-picker.wheelsLabel Wheels
yearViewLabel calendar.yearViewLabel, date-time-picker.yearViewLabel Choose a year
zoomInLabel image-preview.zoomInLabel Zoom in
zoomOutLabel image-preview.zoomOutLabel Zoom out

1 つのものだけについての言葉には、キーがありません。フィールドの label、日付と時刻のピッカーの label、削除ボタンの「山田 花子を削除」、件数を含むステータステキストなどです。これらはコンポーネントに書きます。

ほかのプラットフォーム

この規則は、どのプラットフォームでも同じです。いま実装されているのは Web だけです。SwiftUI、Jetpack Compose、Flutter、React Native は、それぞれの流儀でプロダクトの言葉と、同じ英語の言葉のパックを受け取るようになります。