Internationalization (i18n) and localization (l10n)

Table of contents

Specify available languages

  • The <AppBaseAdvanced> blockly component has a field languages.
  • It takes a comma-separated list of language codes.
    The first code is the default language.
    Example: de,en,fr
  • The language codes should follow the standard for language features to work (like the browser’s built-in hyphenation, or translation extensions)
  • Language is stored per user, so you will also need e.g. an <AnonymousLogin>. There you can override the default language.

blockly view

The language switcher component

  • <LangSwitch> in blockly
  • can be placed anywhere
  • fields
    • label — a descriptive label, optionally blank; can be translated
    • reloadAfterSwitch — some complex components are not fully reactive w.r.t. localization. Set to yes to force a blunt document.location.reload after the user switches their language.

blockly view

frontend view

Internationalization

“i18n”. Make the app translatable.

i18n: static blockly component text (strings)

Any text used in a Svelte component that is not provided by a field (see below).

import { t, translations, lang } from '../i18n.js'
// translations and lang are stores

const foo1 = t('$foo')
// or in longform, for more control, but no auto-fallback:
const foo2 = get(translations)?.[get(lang)]?.['$foo']

<span>{foo1}: {t('$bar')}</span>
  • t(id) is the shortcut function. It will give you the translation for an id in the active language (lang).
  • It depends on a network connection. If it fails, or page load is slow, you might see instead of the proper value.
  • You can specify the fallback string: t('$foo', 'loading…')
  • For access to other languages’ values, use the translations store.
  • See Implementation overview for other values that i18n.js provides.
  • Since lang is a store which is initialized from user data, it might be not available in the non-reactive JS part. E.g., in a component,
    export let someParam = t('$someparam') will likely not work,
    but providing a defaultValue for the param in its yaml will.
    You might be able to work around this with $: ….

i18n: simple blockly component fields (strings)

E.g. a <Span>’s text.

Start the string with a $ sigil. This string will be reactively replaced with its translation.
If no translation is provided, it will default to the “identifier” (the string with the leading $ stripped).

blockly view

i18n: dynamic blockly components

Something using Database sheets, e.g. a <DataLoaderMulti> with a <DataList> of <DataCell>s.

  • Each column you want to translate, has to be multiplied into and suffixed with all languages, e.g.
    namename$de + name$en
  • The sheet also needs an extra, meta column with suffix $lang, e.g.
    name$lang
    The contents of this column might be used as a fallback.
  • $lang acts as a “template variable suffix”. lang will be replace with the current language code. This makes the column name reactively dynamic, e.g.
    column = Cats/name$lang →(turns reactively into)→ Orte/name$de
  • In Blockly, you select the …$lang column.

admin view blockly view

i18n: incompatible components

Some components might not be properly reactive, e.g.:

  • the legacy <Subsection>’s title is propagated via svelte context. The <SubsectionsNav> component builds a breadcrumb-y history array of copies of subsection titles. These can not reactively-retroactively change language.

We can use the language switchers reloadAfterSwitch functionality to work around this.

Localization

“l10n”. Translate the contents.

The translation sheet

  • A Database sheet with key translation (name is not enough) which holds translations.
  • needs a column with key _id (again, name is not enough) – corresponds to the sigiled $foo strings, here we use it without the $.
  • multiple columns with keys that match the language codes to hold the actual translations

translation sheet

Heads up: make sure to set the correct keys before you enter any data. If you change the keys later, the data will be lost (the entries are connected to the keys and the connection is not updated on key change)

sheet keys renaming

Default strings

E.g. warnings and hints related to network connectivity.

Please see interkit/i18n_messages.js for predefined strings. Shortened excerpt:

const messages = {
  de: {
    '$init_loading': 'lade....',
    '$appbase_noconnection_network': 'Keine Verbindung (Offline)'
  },
  en: {
    '$init_loading': 'loading....',
    '$appbase_noconnection_network': 'No connection (offline)'
  }
}

The language switcher’s language labels

Translate via the translation sheet, using the internal ID _LangSwitchLanguageOptionLabel.
(See image above).

Chat

The onMessage and onArrive handlers…

  • receive another argument, idiomatically named t – a helper function that takes several strings and returns the one matching the current language. The strings can be provided in three ways:
    1. pipe-separated string
    2. array
    3. object/hash
  • their api argument carries a userLang (code string) and userLangIndex (zero-index) member

Caveat:

  • delayed messages might not reflect current language changes

Examples from the cheatsheet:

// access current language
api.sendText('your language: ' + api.userLang)
api.sendText('your language, index: ' + api.userLangIndex)

// use current language
if (api.userLang === 'en') ...
if (api.userLangIndex === 1) ...
let text1 = ['Deutsch', 'Englisch'][api.userLangIndex]
let text2 = {de: 'Deutsch', en: 'Englisch'}[api.userLang]

// use text localized to current user language
export const onMessage = async (msg, api, t) => {
  api.sendChoice({ a: t('Ja|Yes'), b: t('Nein|No') })
}

// the t helper function takes pipe-separated strings, arrays or objets:
api.sendText(t('Ja|Yes'))
api.sendText(t(['Ja', 'Yes']))
api.sendText(t({ de: 'Ja', en: 'Yes' })) // order-independant

// use sendTextT & sendChoiceT shortcuts, equivalently
api.sendTextT('Tschüß|Bye')
api.sendChoiceT({ a: 'Ja|Yes', b: ['Nein', 'No'] })

Implementation overview

  • the current language is stored in a user’s userProjectData
    • userLang has the language code
    • userLangIndex has a zero-based index matching the order in <AppBaseAdvanced>.languages
  • both are exported by interkit/i18n.js as stores. Use $lang / $langIndex
  • also notably exported:
    • $translations — an object store constructed from the sheet with all translations
    • $langs — array from <AppBaseAdvanced>.languages
    • setUserLang — update user’s language
  • reactive blockly/component fields
    • are injected via interkit-blockly/blockly/initCodeGenerator.js
    • the referenced stores are imported by way of svelte-admin/src/BlocklyEditor.svelte

TODOs / possible improvements

  • guess language from browser/navigator language?
  • set lang HTML attributes?
  • contexts for translations strings/IDs
  • other gettext-like functionalities (pluralization, dates)