localize

tosijs-ui provides support for localization via the localize method and the <tosi-locale-picker> and <tosi-localized> custom-elements.

Important Note

This module deals with the language used in the user interface. "locale" is not the same thing. The (usually) two-letter codes used designate language and not locale.

E.g. the US locale includes things like measurement systems and date format. Most European locales use commas where we use decimal points in the US.

Similarly, ja is the code for the Japanese language while jp is the locale.

initLocalization(localizationData: string)

Enables localization from TSV string data.

TosiLocalePicker

A selector that lets the user pick from among supported languages.

<h3>Locale Picker</h3>
<tosi-locale-picker></tosi-locale-picker>

<h3>Locale Picker with <code>hide-captions</code></h3>
<tosi-locale-picker hide-caption></tosi-locale-picker>

localize()

If you just want to localize a string with code, use localize(s: string): string.

If the reference string only matches when both are converted to lowercase, the output string will also be lowercase.

E.g. if you have localized Cancel as Annuler, then localize("cancel") will output annuler`.

ellipses

If you end a string with an ellipsis, localize will ignore the ellipsis, localize the string, and then append the ellipsis.

setLocale(language: string)

import { button, p } from 'tosijs'.elements
import { setLocale } from 'tosijs-ui'

preview.append(
  p(
    button(
      {
        onClick() {
          setLocale('en-US')
        }
      },
      'setLocale("en-US")'
    )
  ),
  p(
    button(
      {
        onClick() {
          setLocale('fr')
        }
      },
      'setLocale("fr")'
    )
  ),
  p(
    button(
      {
        onClick() {
          setLocale('qq')
        }
      },
      'setLocale("qq") (see console for error message)'
    )
  ),
)

If you want to directly set locale, just use setLocale().

data-tosi-localized directive

Set the data-tosi-localized attribute on any element with a JSON object mapping attribute names to localization keys. The library applies localize() to each key and writes the result to the corresponding attribute — immediately when the element appears in the DOM, on every locale change, and whenever the data-tosi-localized attribute itself is mutated.

The keys must be reference strings that exist in your localization data — try switching languages with the locale picker above and watch the title and placeholder below update (hover the button to see its tooltip).

<button data-tosi-localized='{"title":"Delete","aria-label":"Delete"}'>
  <tosi-localized>Delete</tosi-localized>
</button>
<input data-tosi-localized='{"placeholder":"Filter…"}'>

You never need to set the underlying attributes (title / aria-label / placeholder) yourself — the data attribute is the source of truth and stays reactive across locale switches. JSON (not a comma-separated mini-format) is deliberate: real translation keys contain commas and colons.

Scope. The library observes the light DOM with a MutationObserver. On locale change, the walk also descends into open shadow roots, so existing elements get re-applied. Insertions inside a shadow root between locale changes are not picked up automatically — call applyLocalized(el) from the component's connectedCallback (or after building the shadow tree) when you add localized elements inside a shadow root. Closed shadow roots are inaccessible by design.

If the JSON is malformed, the directive logs a warning and skips that element — it never throws.

<button
  id="tdl-demo"
  data-tosi-localized='{"title":"Cancel","aria-label":"Cancel"}'
>Hover me</button>
test('data-tosi-localized: insert and runtime mutation both apply', async () => {
  const wait = () => new Promise((r) => setTimeout(r, 0))
  // Combine on-insert and runtime-mutation checks into one test — the
  // page-wide locale-change path is exercised by the unit tests and by
  // localization.pw.ts; changing the locale here would cascade through
  // every localized example on the page.
  await wait(); await wait()
  const btn = preview.querySelector('#tdl-demo')
  expect(btn.getAttribute('title')).toBeTruthy()
  expect(btn.getAttribute('title')).toBe(btn.getAttribute('aria-label'))
  const before = btn.getAttribute('title')
  btn.setAttribute('data-tosi-localized', JSON.stringify({ title: 'Yes' }))
  await wait(); await wait()
  expect(btn.getAttribute('title')).not.toBe(before)
})

TosiLocalized

A span-replacement that automatically localizes its text content. By default the case in the localized data is preserved unless the reference text is all lowercase, in which case the localized text is also lowercased.

While viewing this documentation, all <tosi-localized> elements should display a red underline.

<h3>Localized Widgets</h3>
<button><tosi-localized>Yes</tosi-localized></button>
<button><tosi-localized>No</tosi-localized></button>
<button><tosi-localized>Open…</tosi-localized></button> <i>note the ellipsis</i>

<h3>Lowercase is preserved</h3>
<button><tosi-localized>yes</tosi-localized></button>
<button><tosi-localized>no</tosi-localized></button>
<button><tosi-localized>open…</tosi-localized></button>

<h3>Localized Attribute</h3>
<input>
tosi-localized {
  border-bottom: 2px solid red;
}
import { tosiLocalized, localize } from 'tosijs-ui'

preview.append(tosiLocalized({
  refString: 'localized placeholder',
  localeChanged() {
    this.previousElementSibling.setAttribute('placeholder', localize(this.refString))
  }
}))

<tosi-localized> has a refString attribute (which defaults to its initial textContent) which is the text that it localizes. You can set it directly.

It also has an localeChanged method which defaults to setting the content of the element to the localized reference string, but which you can override, to (for example) set a property or attribute of the parent element.

<tosi-localized> can be used inside the shadowDOM of other custom-elements.

i18n

All of the data can be bound in the i18n proxy (xin.i18n), including the currently selected locale (which will default to navigator.language).

You can take a look at xin.i18n in the console. i18n can be used to access localization data directly, and also to determine which locales are available i18n.locales and set the locale programmatically (e.g. i18n.locale = 'en').

if (i18n.locales.includes('fr')) {
  i18n.locale = 'fr'
}

String Annotations (#)

Sometimes a single term in your reference language needs different translations depending on context. For example, "OK" might translate to both "D'accord" and "Bien" in French depending on usage.

Use # annotations to create context-specific variants:

Example TSV rows:

OK	D'accord	Ok	好的
OK#confirm	"	"	"
OK#accept	Bien	"	"

With the above data:

Ellipsis and case handling work normally with annotations:

Creating Localized String Data

You can create your own localization data using any spreadsheet and exporting TSV.

E.g. you can automatically create localization data using something like my localized Google Sheet which leverages googletranslate to automatically translate reference strings (and which you can manually override as you like).

E.g. in this demo I've replaced the incorrect translation of "Finnish" (googletranslate used the word for Finnish nationality rather than the language).

The format of the input data is a table in TSV format, that looks like this:

en-US fr fi sv zh
English (US) French Finnish Swedish Chinese (Mandarin)
English (US) Français suomi svenska 中文(普通话)
🇺🇸 🇫🇷 🇫🇮 🇸🇪 🇨🇳
Icon Icône Kuvake Ikon 图标
Ok D'accord Ok Ok 好的
Cancel Annuler Peruuttaa Avboka 取消

In the spreadsheet provided, each cell contains a formula that translates the term in the left-most column from the language in that column to the language in the destination column. Once you have an automatic translation, you can hand off the sheet to language experts to vet the translations.

Finally, create a tsv file and then turn that into a Typescript file by wrapping the content thus:

export default `( content of tsv file )`

You use this data using initLocalization().

Leveraging TosiLocalized Automatic Updates

If you want to leverage TosiLocalized's automatic updates you simply need to implement updateLocale and register yourself with TosiLocalized.allInstances (which is a `Set).

Typically, this would look like something like:

class MyLocalizedComponent extends Component {
  ...

  // register yourself as a localized component
  connectecCallback() {
    super.connectedCallback()

    TosiLocalized.allInstances.add(this)
  }

  // avoid leaking!
  disconnectecCallback() {
    super.connectedCallback()

    TosiLocalized.allInstances.delete(this)
  }

  // presumably your render method does the right things
  updateLocale = () =>  {
    this.queueRender()
  }
}