Skip to content
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Getting startedWebReactSwiftUIJetpack ComposeFlutterReact NativePresetsYour own design systemUsing Tanrei with AI agentsBrandDesign tokensDesign systems and customizationTheme StudioColorTypographyIconsShapeElevationMotionInteraction statesBehaviourLayoutAccessibilityWordsColor tokensTypescale tokensShape tokensElevation tokensMotion tokensState tokensSpace and measure tokensComponentsButtonMain action buttonSegmented buttonButton groupButton with menuToolbarCopy buttonCheckboxRadio buttonSwitchChipMenuSliderSlider fieldCalendarDateTimePickerField groupRadio groupCheckbox groupSelectSelect fieldMenubarFile uploadFile picker AdvancedDrop zoneText input AdvancedText fieldChip fieldFieldCombobox AdvancedCombobox fieldNumber input AdvancedNumber fieldColor input AdvancedColor fieldOne-time-code input AdvancedOne-time-code fieldEmbedded text input AdvancedChat composerCardDividerModal layer AdvancedDialogListList rowBottom sheetSide sheetCarouselCode blockTablePopoverStepsAccordionDescription listFigureApp shellPage headerTree viewTimelineImage previewHandleResizable frameScroll areaBadgeProgress indicatorMeterNumber tickerLoading indicatorSnackbarTooltipKeyboard shortcutTagInline codeCalloutAvatarEmpty stateTag groupSkeletonFile itemSwatchMarquee textTabsTab viewApp barNavigation barNavigation drawerSearchLinkPaginationPrevious and next linksStepperBreadcrumbsTable of contentsLink cardFAB menuStatusHow Tanrei is builtContributingLicense and attribution
HomeFoundationsWords

Words

A component has no words of its own. Every label, name and message it draws or says is your product's — given on the component, once for the whole product, or taken from the English word pack you import.

content_copyView as Markdown

A component has no words of its own. No component has a default text in any language: not “Required”, not “Close”, not “OK”. A word a component draws or says is one your product gave: written by you, or taken from the English word pack, which you import.

A default hides the failure it causes. A product in Japanese that forgets one word shows English beside its own text, and nothing reports it. A product in English has its own voice too (“Mandatory”, “Done”, “Nothing found”). So a component has no word to fall back on, and it tells you when one is missing. The English word pack is not such a fallback: nothing applies it but your own import.

Where a word comes from

A component looks in three places, in this order, and nowhere else.

Place What it gives Example
The component itself Words about this one thing, and any word this one says differently label, error-text; close-label="Close filters"
Your product’s words Words you say the same way wherever the part is drawn, given once per language, or the English word pack you import requiredText, closeLabel, previousMonthLabel
The platform What the browser already has in the reader’s language Month and weekday names, digits, “AM”, the browser’s own message for a missing value

After these there is nothing: no fallback language, and no English “for now”.

The English word pack

Tanrei writes one set of your product’s words for you: a word in plain, neutral English for every key, such as “Required”, “Close” and “Previous month”. Import it once:

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

It gives the words of the whole page, as one call of setWords() would. Nothing imports it for you: not @tanrei/web, not @tanrei/react, not a component, and not the lang of the page. The pack’s word for each key is in the table of keys.

  • Your own words still win. Call setWords() after the import, and the keys you give replace the pack’s; a word on a component wins over both. The import sets every key when it runs, so a word you gave before it is replaced.
  • Start from its words. The module exports them too: import { englishWords } from '@tanrei/web/words/en'.
  • In a product in another language, a key that your language’s set does not have is read from the page’s set — with the pack imported, that is English. Import it there only where English is the word you want for a key you did not write; without it, the missing word is reported instead.
  • It hides nothing it should not. With the pack imported, every key has a word, so missingWords() is empty. To find the words your product has not written itself, ask it in a test that does not import the pack.

In React, the import is the same line, in the module that renders first.

Give your product’s words once

On the web, call setWords() once, before or after the components load. Elements already on the page draw the words at once.

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

setWords({
  requiredText: 'Required',
  optionalText: 'Optional',
  closeLabel: 'Close',
  clearLabel: 'Clear',
  targetLabel: 'opens in a new tab',
});

Give only the words of the components you use. A second call adds to the first, key by key; a key given as an empty string is taken away. @tanrei/web/words exports the same function without defining any element, and @tanrei/react exports it too.

An attribute on one component always wins over your product’s words:

The word on the component itself: one field says it differently

<tanrei-text-field label="Email" required required-text="Required"></tanrei-text-field>
<tanrei-text-field label="Company" required required-text="Needed for invoices"></tanrei-text-field>

More than one language

Give one set per language. A component reads the set of the nearest lang attribute around it, so a page in Japanese, or a Japanese part of an English page, reads the Japanese words.

setWords({ requiredText: 'Required', closeLabel: 'Close' }, { lang: 'en' });
setWords({ requiredText: '必須', closeLabel: '閉じる' }, { lang: 'ja' });
  • A set for ja also answers ja-JP, unless you give ja-JP a set of its own.
  • A key that the language’s set does not have is read from the set given without lang.
  • The words are plain strings. If your product has a localization library, hand it the strings from there: setWords() is not a translation system, and has no plural rules. A few words have places a component fills, and nothing else is read in a word.

A word with places

Some sentences hold a value that only the component knows, at the moment it says them: where a row stands after a move, or how many entries a paste made. Your product still writes the whole sentence. It marks where the value goes with a name between braces, and the component puts the value there.

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

The order of the words is your language’s. The component fills the places and does nothing else to the sentence.

  • The component decides the names. Each key has its own places, listed in the table of keys, and only those are filled. You may write a place once, several times, or not at all.
  • A number is written in the language of the element. {count} and {position} are whole numbers, written with the digits and the grouping of the nearest lang.
  • A text keeps its own direction. {headline} and {entry} are texts the component holds: a row’s headline, an entry a person typed. They are put in as plain text, never as markup. Where the sentence is drawn or announced, the text is isolated, so a Latin name inside an Arabic sentence stays in its own order.
  • To write a brace, write it twice. {{ is drawn as { and }} as }. A word without places is not read at all, so there a brace is written once.
  • One place is needed. entryRemoveLabel names the remove button of each entry of a chip field. Without {entry} every button has the same name.

In development the console reports a place the key does not have ({entri}), a single brace and the missing {entry}. The sentence is still used exactly as you wrote it. setWords() reports them when you give the word, and the component reports them when it uses a word given on the element.

Plurals

The component has no grammar: no plural forms, no ordinals and no choice between two sentences. You have three ways to deal with a number, and none needs anything from Tanrei.

  1. Write a sentence that is right for every number: {position} of {count}, Added: {count}.
  2. Give the word on the component, and change it when the count changes. Your product knows the count, and a word on the component always wins. Your own plural rules choose the sentence.
  3. Where your product holds the numbers itself, the word has no places: a table’s status (“1–20 of 132”) is one whole string that you write.

Without a word

A missing word is a fault of the product, as a text field without a label is. The component makes it small for the person and visible to you.

  • Words that would be drawn are not drawn. Where a mark says the same thing, the mark stands in their place: an asterisk for the requirement tag of a required field, the close and check icons for a picker’s Cancel and OK, +1 for “next day”. Where no mark does, nothing is drawn and the place keeps its size.
  • A control that needs a name is still drawn and still works. A calendar’s navigation buttons, a text field’s resize handle and a picker’s actions keep working without a name. The one exception is a number field’s steppers: an unnamed one is hidden from assistive technology, because the spin button beside it steps by itself.
  • It is reported. In development the console names the attribute, once per word and page: <tanrei-field>: required-text is empty and the product's words have no requiredText.

A control without an accessible name fails people who use a screen reader. The report is there so that you fix it, not so that you may leave it.

Fail a test on a missing word

missingWords() returns the keys that were needed since the page loaded and that nobody gave. Ask it at the end of a test that walks through your product. A page that imports the English word pack misses none.

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

test('every word the product needs is given', async () => {
  // …render the screens of the product…
  expect(missingWords()).toEqual([]);
});

What is not a word

A glyph, an asterisk, a dash, a digit and a separator the locale gives are not words, and a component still draws them. Nor are the names in the API (a prop, a value), or what the console tells a developer, which is English. The legends a keyboard shortcut draws (“Ctrl”, “Enter”, ⌘) are what the keys themselves carry; what is read aloud for a shortcut is the label you give it.

The keys

Every key, the props that read it, the places the component fills in its sentence, and its word in the English word pack. Most keys have the name of their prop. Three differ: a copy button’s label and failedLabel read copyLabel and copyFailedLabel, and the breadcrumbs’ overflowLabel reads breadcrumbsOverflowLabel.

Key Read by Places it fills In the English pack
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

Words that are about one thing — a field’s label, a date-time picker’s label, a remove button’s “Remove Ada Lovelace”, a status text with a count — have no key: give them on the component.

Other platforms

The rule is the same on every platform. Only the web is built so far; SwiftUI, Jetpack Compose, Flutter and React Native will take the product’s words, and the same English word pack, in their own idiom.