组件没有自己的文字。 没有哪个组件带有任何语言的默认文本:没有“必填”,没有“关闭”,也没有“确定”。组件绘制或朗读的文字,都是你的产品给出的:要么是你自己写的,要么取自你导入的英语文字包。
默认值会掩盖它自己造成的失败。一个日语产品漏写了一个词,自己的文字旁边就会出现英语,而且没有任何东西报告这一点。英语产品同样有自己的说法(“Mandatory”“Done”“Nothing found”)。所以组件没有可以退而使用的文字,缺了哪一个,它会告诉你。英语文字包不是这样的后备:除了你自己的导入,没有任何东西会应用它。
组件按下面的顺序查看三个地方,不看别处。
| 地方 |
它给出什么 |
示例 |
| 组件自身 |
只与这一个组件有关的文字,以及这一个组件说法不同的文字 |
label、error-text;close-label="关闭筛选条件" |
| 你的产品的文字 |
这个部件无论画在哪里,你的说法都一样的文字,每种语言只给一次,或者取自你导入的英语文字包 |
requiredText、closeLabel、previousMonthLabel |
| 平台 |
浏览器已经用读者的语言备好的东西 |
月份和星期的名称、数字、“上午”、缺少值时浏览器自己的提示 |
这三处之后就什么都没有了:没有后备语言,也没有“暂时先用英语”。
Tanrei 为你写好了一组产品的文字:每个键都有一个平实、中性的英语文字,例如“Required”“Close”和“Previous month”。导入一次即可:
import '@tanrei/web/words/en';
它给出整个页面的文字,就像调用一次 setWords() 一样。不会有任何东西替你导入它:@tanrei/web 不会,@tanrei/react 不会,组件不会,页面的 lang 也不会。每个键在包中的文字,列在键的表格里。
- 你自己的文字仍然优先。 在导入之后调用
setWords(),你给出的键会替换包中的文字;写在组件上的文字又优先于这两者。导入在运行时会给出每一个键,所以在它之前给出的文字会被替换。
- 从它的文字开始。 这个模块也把这些文字作为值导出:
import { englishWords } from '@tanrei/web/words/en'。
- 在其他语言的产品里,你那种语言的文字组里没有的键,会从页面的文字组里读取——导入了这个包,那就是英语。只有在你没写的键用英语也合适的地方,才导入它;不导入的话,缺少的文字会被报告。
- 它不会掩盖不该掩盖的东西。 导入这个包之后,每个键都有文字,所以
missingWords() 为空。要找出你的产品自己还没写的文字,请在不导入这个包的测试里调用它。
在 React 中,导入也是同样的一行,写在最先渲染的模块里。
在 Web 上,调用一次 setWords(),在组件加载之前或之后都可以。已经在页面上的元素会立即绘制这些文字。
import { setWords } from '@tanrei/web';
setWords({
requiredText: '必填',
optionalText: '选填',
closeLabel: '关闭',
clearLabel: '清除',
targetLabel: '在新标签页中打开',
});
只需给出你用到的组件的文字。第二次调用会按键逐个加到第一次之上;给出空字符串的键会被去掉。@tanrei/web/words 导出同一个函数,但不定义任何元素;@tanrei/react 也导出它。
组件上的属性总是优先于产品的文字:
format_textdirection_r_to_lcode
<tanrei-text-field label="邮箱" required required-text="必填"></tanrei-text-field>
<tanrei-text-field label="公司" required required-text="开具发票需要"></tanrei-text-field>
每种语言给一组。组件读取离它最近的 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' });
词序是你的语言的词序。组件只填入占位标记,不对句子做别的任何事。
- 名称由组件决定。 每个键有它自己的占位标记,列在键的表格里,只有这些会被填入。一个占位标记可以写一次、写好几次,也可以不写。
- 数字按元素所用的语言书写。
{count} 和 {position} 是整数,用最近的 lang 所对应的数字和分组方式书写。
- 文本保持它自己的方向。
{headline} 和 {entry} 是组件持有的文本:一行的标题、用户键入的一个条目。它们作为纯文本填入,绝不当作标记(markup)。在绘制或朗读这个句子的地方,这段文本会被隔离开,所以阿拉伯语句子里的拉丁字母名称保持它自己的顺序。
- 要写花括号本身,就连写两个。
{{ 绘制成 {,}} 绘制成 }。不带占位标记的文字根本不会被解读,所以在那里花括号只写一个。
- 有一个占位标记是必需的。
entryRemoveLabel 为纸片字段每个条目的移除按钮命名。没有 {entry},每个按钮的名称就都一样。
在开发时,控制台会报告这个键没有的占位标记({entri})、单独一个花括号,以及缺少的 {entry}。句子仍然完全按你写的样子使用。setWords() 在你给出文字时报告它们;写在元素上的文字,则由组件在用到它时报告。
组件不懂语法:没有复数形式,没有序数词,也不会在两个句子之间做选择。处理数字有三种办法,哪一种都不需要 Tanrei 做什么。
- 写一个对任何数字都成立的句子:
第 {position} 项,共 {count} 项、已添加 {count} 项。
- 把文字写在组件上,数量变化时更改它。你的产品知道数量,而组件上的文字总是优先。用哪个句子,由你自己的复数规则来选。
- 数字由你的产品自己持有的地方,文字不带占位标记:表格的状态(“第 1–20 件,共 132 件”)是由你写的一整个字符串。
缺少文字是产品的缺陷,就像文本字段没有标签一样。组件让它对用户的影响尽量小,并让你看得见它。
- 本该绘制的文字不绘制。 有标记能表达同样的意思时,标记就站在文字的位置上:必填小标签用星号,选择器的“取消”和“确定”用关闭和对勾的图标,“次日”用
+1。没有这样的标记时,什么都不绘制,那个位置保持原来的大小。
- 需要名称的控件仍然会绘制,也仍然可以使用。 日历的导航按钮、文本字段的调整手柄和选择器的操作,没有名称也照常工作。唯一的例外是数字字段的步进按钮:没有名称的步进按钮对辅助技术隐藏,因为它旁边的微调按钮自己就能步进。
- 它会被报告。 在开发时,控制台会指出是哪个属性,每个文字在每个页面上报告一次:
<tanrei-field>: required-text is empty and the product's words have no requiredText。
没有无障碍名称的控件,使用屏幕阅读器的人用不了。报告是为了让你去修正,而不是让你可以置之不理。
missingWords() 返回自页面加载以来被需要、却没有人给出的键。在一个把你的产品走一遍的测试的末尾调用它。导入了英语文字包的页面不会缺少任何文字。
import { missingWords } from '@tanrei/web';
test('产品需要的每个文字都已给出', async () => {
// …渲染产品的各个界面…
expect(missingWords()).toEqual([]);
});
字形、星号、短横线、数字,以及由区域设置给出的分隔符,都不是文字,组件仍然会绘制它们。API 里的名称(属性、取值)也不是,控制台告诉开发者的内容也不是,后者是英语。键盘快捷键画出的字符(“Ctrl”“Enter”、⌘)是按键本身带有的;为快捷键朗读的,是你给它的 label。
所有的键、读取它们的属性、组件在句子里填入的占位标记,以及它在英语文字包中的文字。大多数键与它的属性同名。有三个不同:复制按钮的 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 |
|---|
只与某一样东西有关的文字——字段的 label、日期时间选择器的 label、移除按钮的“移除李华”、带有数量的状态文本——没有键:请在组件上给出它们。
这条规则在每个平台上都相同。目前只有 Web 已经实现;SwiftUI、Jetpack Compose、Flutter 和 React Native 将按各自的惯用方式接受产品的文字,以及同一个英语文字包。