Skip to content

Translating Texts (i18n) ​

Atscript ships no translation runtime. Your .as files are written in one source language, and every text in them is a message id: a translator (your map, i18next, FormatJS, …) looks the text up and returns the translation.

atscript
export interface Signup {
  @meta.label 'Email'
  @expect.pattern '^\S+@\S+$', '', 'Enter a valid email'
  email: string

  @meta.label 'Password'
  @expect.minLength 8
  password: string
}

Here 'Email', 'Password' and 'Enter a valid email' are message ids. The @expect.minLength error has no text of its own: its id is the code MIN_LENGTH.

The two kinds of ids ​

The text comes fromIts idExample
An annotation (@meta.label, @meta.description, a constraint's message argument, @expect.message, …)the text itself, as written'Enter a valid email'
The validator's built-in messagethe error code (template with {params})MIN_LENGTH → Mindestens {min} Zeichen

Error codes, their English templates and localizeErrors() are covered in Validation Reference — Error Codes.

Which annotations hold text is declared by their specs (userText). Core flags @meta.label, @meta.description, @meta.required and the message of every @expect.* constraint; plugins flag their own, see User-facing Text.

Serving a translated type ​

A server that sends types to a client (Serialization) translates them per request with translateServed():

typescript
import {
  serializeAnnotatedType,
  translateServed,
  VALIDATOR_MESSAGES,
} from '@atscript/typescript/utils'
import { Signup } from './signup.as'

const json = serializeAnnotatedType(Signup) // once, at startup

// t: your translator for the request's locale, returning undefined when it has no translation
function servedSignup(t: (text: string) => string | undefined) {
  const { value, texts } = translateServed(json, t)
  const templates: Record<string, string> = {}
  for (const code of Object.keys(VALIDATOR_MESSAGES)) {
    const template = t(code) // e.g. 'Mindestens {min} Zeichen'
    if (template !== undefined) templates[code] = template
  }
  return { ...value, $i18n: { ...templates, ...texts } }
}
  • t(text) returns the translation, or undefined to keep the text.
  • Every annotation holding text is translated, at any depth: props, array items, repeated types, reference targets, every item of a multiple annotation, the message field of a constraint. The exception is a primitive's built-in format message (string.email's pattern message, …): it stays in English, because the validator reports it by its code (EMAIL), which the client translates through $i18n.
  • The input is never changed, and is returned itself when nothing was translated — cache the result per type and locale.
  • translateServed() accepts any JSON that carries serialized types, such as an envelope with the type inside it.

$i18n: texts the client needs ​

Put the texts the client translates itself into the served type's $i18n field (a flat { [id]: text } map, the templates first and texts over them):

  • Validator templates by code, translated, keeping their {name} placeholders (MIN_LENGTH: 'Mindestens {min} Zeichen').
  • texts returned by translateServed(): the translated string literals of expressions (see below).

deserializeAnnotatedType() ignores $i18n.

Using moost?

setTranslator() from moost picks each request's locale and gives your code one translator, useTranslate(). See the moost i18n guide.

Translating on the client ​

The served type's labels and messages are already translated. Validation errors the client produces use the built-in templates, so translate them with $i18n:

typescript
import {
  deserializeAnnotatedType,
  localizeErrors,
  messagesTranslator,
} from '@atscript/typescript/utils'

const served = await fetch('/signup/meta').then(r => r.json())
const type = deserializeAnnotatedType(served)
const validator = type.validator()

if (!validator.validate(data, true)) {
  const errors = localizeErrors(validator.errors, messagesTranslator(served.$i18n ?? {}))
}

Expressions: texts computed on the client ​

Some annotations hold the source of a function the client runs, such as a computed label or a custom validator returning its message. A server cannot translate the result, which depends on the data. Their specs flag them userText: 'expression' instead:

atscript
@ui.form.validate '(v) => v.password === v.confirm || "Passwords must match"'

translateServed() leaves the source as written, translates each string literal in it ('…', "…", and templates without ${…}), and returns the translations in texts, keyed by the literal. The client looks a computed result up in $i18n by exact match: "Passwords must match" is translated, while a result built by concatenation ("Hello " + name) is not.

DOs and DON'Ts ​

  • DO write each text once, in the source language, and use it as the id in your translations.
  • DO serve validator templates by code in $i18n so client-side validation speaks the user's language.
  • DON'T branch on message in clients: it changes with the locale. Use code and params.
  • DON'T build texts from pieces in expressions ('Add ' + label); exact-match translation only finds whole literals.
  • DON'T expect derived texts to be translated: a label that is not written in an annotation (taken from the field name, say) has no id.

Next Steps ​

Released under the MIT License.