Textos
Un componente no tiene textos propios. Cada etiqueta, cada nombre y cada mensaje que dibuja o dice es de tu producto, dado en el componente, una sola vez para todo el producto o tomado del paquete de textos en inglés que importas.
Un componente no tiene textos propios. Ningún componente tiene un texto predeterminado en ningún idioma: ni «Obligatorio», ni «Cerrar», ni «Aceptar». Un texto que un componente dibuja o dice es uno que dio tu producto: escrito por ti, o tomado del paquete de textos en inglés, que importas tú.
Un valor predeterminado oculta el fallo que provoca. Un producto en japonés que olvida un texto muestra inglés junto a su propio texto, y nada lo notifica. Un producto en inglés también tiene su propia voz («Mandatory», «Done», «Nothing found»). Por eso un componente no tiene ningún texto al que recurrir, y te avisa cuando falta uno. El paquete de textos en inglés no es un recurso así: nada lo aplica salvo tu propia importación.
Un componente mira en tres lugares, en este orden, y en ningún otro.
| Lugar | Qué da | Ejemplo |
|---|---|---|
| El propio componente | Los textos sobre esta única cosa, y cualquier texto que esta diga de otra manera | label, error-text; close-label="Cerrar filtros" |
| Los textos de tu producto | Los textos que dices igual dondequiera que se dibuje la parte, dados una sola vez por idioma, o el paquete de textos en inglés que importas | requiredText, closeLabel, previousMonthLabel |
| La plataforma | Lo que el navegador ya tiene en el idioma del lector | Los nombres de los meses y de los días de la semana, las cifras, «a. m.», el mensaje propio del navegador cuando falta un valor |
Después de estos no hay nada: ningún idioma de reserva, y ningún inglés «por ahora».
Tanrei escribe para ti un conjunto de los textos de tu producto: un texto en inglés llano y neutro para cada clave, como «Required», «Close» y «Previous month». Impórtalo una vez:
import '@tanrei/web/words/en';Da los textos de toda la página, como lo haría una llamada a setWords(). Nada lo importa por ti: ni @tanrei/web, ni @tanrei/react, ni un componente, ni el lang de la página. El texto del paquete para cada clave está en la tabla de claves.
- Tus propios textos siguen ganando. Llama a
setWords()después de la importación, y las claves que des sustituyen a las del paquete; un texto en un componente gana sobre ambos. La importación da todas las claves cuando se ejecuta, así que un texto que diste antes queda sustituido. - Parte de sus textos. El módulo también los exporta:
import { englishWords } from '@tanrei/web/words/en'. - En un producto en otro idioma, una clave que el conjunto de tu idioma no tiene se lee del conjunto de la página, y con el paquete importado eso es inglés. Impórtalo ahí solo donde el inglés sea el texto que quieres para una clave que no escribiste; sin él, el texto que falta se notifica.
- No oculta lo que no debe. Con el paquete importado, todas las claves tienen texto, así que
missingWords()está vacío. Para encontrar los textos que tu producto aún no ha escrito, pregúntalo en una prueba que no importe el paquete.
En React, la importación es la misma línea, en el módulo que se renderiza primero.
En la web, llama a setWords() una sola vez, antes o después de que se carguen los componentes. Los elementos que ya están en la página dibujan los textos de inmediato.
import { setWords } from '@tanrei/web';
setWords({
requiredText: 'Obligatorio',
optionalText: 'Opcional',
closeLabel: 'Cerrar',
clearLabel: 'Borrar',
targetLabel: 'se abre en una pestaña nueva',
});Da solo los textos de los componentes que usas. Una segunda llamada se suma a la primera, clave por clave; una clave dada como cadena vacía se retira. @tanrei/web/words exporta la misma función sin definir ningún elemento, y @tanrei/react también la exporta.
Un atributo en un componente siempre tiene prioridad sobre los textos de tu producto:
El texto en el propio componente: un campo lo dice de otra manera
<tanrei-text-field label="Correo electrónico" required required-text="Obligatorio"></tanrei-text-field>
<tanrei-text-field label="Empresa" required required-text="Necesario para las facturas"></tanrei-text-field>Da un conjunto por idioma. Un componente lee el conjunto del atributo lang más cercano que lo rodea, de modo que una página en japonés, o una parte en japonés de una página en inglés, lee los textos en japonés.
setWords({ requiredText: 'Required', closeLabel: 'Close' }, { lang: 'en' });
setWords({ requiredText: '必須', closeLabel: '閉じる' }, { lang: 'ja' });- Un conjunto para
jasirve también paraja-JP, salvo que des aja-JPun conjunto propio. - Una clave que el conjunto del idioma no tiene se lee del conjunto dado sin
lang. - Los textos son cadenas simples. Si tu producto tiene una biblioteca de localización, toma de ella las cadenas que das:
setWords()no es un sistema de traducción, y no tiene reglas de plural. Unos pocos textos tienen huecos que rellena un componente, y en un texto no se lee nada más.
Algunas frases contienen un valor que solo el componente conoce, en el momento en que las dice: dónde queda una fila después de moverla, o cuántos elementos creó un pegado. Tu producto sigue escribiendo la frase entera. Marca dónde va el valor con un nombre entre llaves, y el componente pone ahí el valor.
setWords({ movedText: '{headline}: {position} of {count}' }, { lang: 'en' });
setWords({ movedText: '{headline}:{count} 件中 {position} 番目' }, { lang: 'ja' });El orden de las palabras es el de tu idioma. El componente rellena los huecos y no le hace nada más a la frase.
- Los nombres los decide el componente. Cada clave tiene sus propios huecos, que figuran en la tabla de claves, y solo esos se rellenan. Puedes escribir un hueco una vez, varias o ninguna.
- Un número se escribe en el idioma del elemento.
{count}y{position}son números enteros, escritos con las cifras y la agrupación dellangmás cercano. - Un texto conserva su propia dirección.
{headline}y{entry}son textos que tiene el componente: el titular de una fila, un elemento que escribió una persona. Se insertan como texto sin formato, nunca como marcado. Donde la frase se dibuja o se anuncia, el texto queda aislado, de modo que un nombre en letras latinas dentro de una frase en árabe conserva su propio orden. - Para escribir una llave, escríbela dos veces.
{{se dibuja como{y}}como}. Un texto sin huecos no se lee en absoluto, así que ahí una llave se escribe una sola vez. - Un hueco es necesario.
entryRemoveLabelda nombre al botón de quitar de cada elemento de un campo de chips. Sin{entry}, todos los botones tienen el mismo nombre.
En desarrollo, la consola notifica un hueco que la clave no tiene ({entri}), una llave suelta y la falta de {entry}. Aun así, la frase se usa exactamente como la escribiste. setWords() los notifica cuando das el texto, y el componente los notifica cuando usa un texto dado en el elemento.
El componente no tiene gramática: ni formas de plural, ni ordinales, ni elección entre dos frases. Tienes tres maneras de tratar un número, y ninguna necesita nada de Tanrei.
- Escribe una frase que sea correcta para cualquier número:
{position} de {count},Añadidos: {count}. - Da el texto en el componente y cámbialo cuando cambie el recuento. Tu producto conoce el recuento, y un texto dado en el componente siempre tiene prioridad. Tus propias reglas de plural eligen la frase.
- Donde es tu producto el que tiene los números, el texto no tiene huecos: el estado de una tabla («1–20 de 132») es una cadena entera que escribes tú.
Un texto que falta es un defecto del producto, igual que un campo de texto sin etiqueta. El componente hace que sea pequeño para la persona y visible para ti.
- Los textos que se dibujarían no se dibujan. Donde una marca dice lo mismo, la marca ocupa su lugar: un asterisco en lugar del tag de obligatoriedad de un campo obligatorio, los iconos de cerrar y de marca de verificación en lugar de «Cancelar» y «Aceptar» en un selector,
+1en lugar de «día siguiente». Donde ninguna marca lo dice, no se dibuja nada y el lugar conserva su tamaño. - Un control que necesita un nombre se dibuja igualmente y sigue funcionando. Los botones de navegación de un calendario, el tirador de cambio de tamaño de un campo de texto y las acciones de un selector siguen funcionando sin nombre. La única excepción son los botones de paso de un campo numérico: uno sin nombre queda oculto para la tecnología de apoyo, porque el botón de número que tiene al lado da los pasos por sí mismo.
- Se notifica. En desarrollo, la consola nombra el atributo, una vez por texto y por página:
<tanrei-field>: required-text is empty and the product's words have no requiredText.
Un control sin nombre accesible falla a las personas que usan un lector de pantalla. La notificación está ahí para que lo corrijas, no para que puedas dejarlo así.
missingWords() devuelve las claves que hicieron falta desde que se cargó la página y que nadie dio. Pregúntale al final de una prueba que recorra tu producto. A una página que importa el paquete de textos en inglés no le falta ninguno.
import { missingWords } from '@tanrei/web';
test('se dan todos los textos que necesita el producto', async () => {
// …renderiza las pantallas del producto…
expect(missingWords()).toEqual([]);
});Un glifo, un asterisco, una raya, una cifra y un separador que da la configuración regional no son textos, y un componente los sigue dibujando. Tampoco lo son los nombres de la API (una propiedad, un valor), ni lo que la consola le dice a quien desarrolla, que está en inglés. Las leyendas que dibuja un atajo de teclado («Ctrl», «Enter», ⌘) son lo que llevan las propias teclas; lo que se lee en voz alta para un atajo es el label que le das.
Todas las claves, las propiedades que las leen, los huecos que el componente rellena en su frase y su texto en el paquete en inglés. La mayoría de las claves tienen el nombre de su propiedad. Tres son distintas: label y failedLabel de un botón de copiar leen copyLabel y copyFailedLabel, y overflowLabel de la ruta de navegación lee breadcrumbsOverflowLabel.
| Clave | Propiedad que la lee | Lugares que rellena | En el paquete de textos en inglés |
|---|---|---|---|
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 |
Los textos que tratan de una sola cosa (el label de un campo, el label de un selector de fecha y hora, el «Quitar a Ada Lovelace» de un botón de quitar, un texto de estado con un recuento) no tienen clave: dalos en el componente.
La regla es la misma en todas las plataformas. Hasta ahora solo está construida la web; SwiftUI, Jetpack Compose, Flutter y React Native recibirán los textos del producto, y el mismo paquete de textos en inglés, con su estilo propio.