It's i18n

COVERAGE: 91.46% TESTS: 93 / 93 LICENSE: MIT

Introducion

"It's i18n" is a web component that intenationalizes html content

Background

This project started when I was developing an application to generate bootstraps for future projects, and internationalization is one of the features that would be added, so I did not find a simple i18n library that the only thing you had to worry is to put an i18n file and one component in the HTML document and voilá, the page is internationalized.

Another point that I found strange is in the translation files. Parameterization/templating the keys is just complex in HTML. It is easy to do in programming languages like javascript as shown in the next example

import i18n from "example-i18n-library"
// ...
i18n("key with param", param1, param2)

How would you do in HTML? there are ways to translate the information using only HTML

<!-- the pattern is generally key, end of key char (in this case the ;, or be in parentesis), list of params-->
<button data-i18n="hello mouse; param1, param2"></button>

Which is why most translations are done using javascript in the browser, the idea of this component is to be able to apply i18n with HTML only.

Getting started

CDN

To use a CDN all you need is to add the following code in the HTML page:

<script type="module" src="https://cdn.jsdelivr.net/gh/OmarCastro/its-i18n@0.3.1/dist/i18n.element.min.js?named=i18n-container"></script>

As noted, there is a query string named to set the component name, if it is not defined then the component will not be registered and you would need to import it and register manually, like with the following code:

import element from 'https://cdn.jsdelivr.net/gh/OmarCastro/its-i18n@0.3.1/dist/i18n.element.min.js'

customElements.define('i18n-container', element)

Nodejs

If you wish to import from nodejs and use a bundler, you can install the its-i18n package

npm install its-i18n

Not all bundlers support query strings, it is recommended to import and register the component, like the following code:

import element from 'its-i18n'

customElements.define('i18n-container', element)

Features

This component has the following features:

Reactive

Everytime a data-i18n attribute, and attibutes where its name starts with data-i18n- is added or changed inside the component, its respective content will be automatically updated to reflect the content.

Removing it just disables it, leaving for the developer to remove the translated content if he wishes

Lazy loading

Only the necessary content is loaded to apply the internationalization.

Template keys

I18n key can match multiple translations, because of that, conflict may arise, to handle it. Prioritization rules are made to choose which of the translation key are to be used

1 to 1 mapping

Due to template keys. Not only simplifies the translation file, it makes them consistent

Javascript API

You can use JS api to work outside elements, or on element with programmatic content (e.g. canvas):

import { translate } from 'its-i18n'

await translate("hello world")

Examples

This section will show some examples on how to use its-i18n library

How to read the examples

The examples will normally show 3 or 4 sections.

  1. The result section that shows the final result of the other sections;
  2. The tabbed i18n section that shows the content of i18n files, every change in the JSON will reset the view. JSON files are expected to be static so there is no problem with that;
  3. The html section that shows the whole document or a subset to show the usage, the editable part changes will only update the current element without resetting the view;
  4. The javascript section, only shown on canvas examples, since you need to use JS to draw it. This is static and only serves to show how to use the features on canvas;

Hello world

The very first example shows how to use the component, it shows a button with translated "hello world" text and "hello mouse" tooltip that will be shown on mouse cursor above the button

{
  "hello world": "hola mundo",
  "hello mouse": "hola ratón"
}
{
  "hello world": "hello world",
  "hello mouse": "hello mouse"
}
{
  "hello world": "olá mundo",
  "hello mouse": "olá rato"
}
<html>
    <head>
        <link rel="i18n-translation-map" href="es.json" lang="es">
        <link rel="i18n-translation-map" href="en.json" lang="en">
        <link rel="i18n-translation-map" href="pt.json" lang="pt">
        <script type="module" src="https://cdn.jsdelivr.net/gh/OmarCastro/its-i18n@0.3.1/dist/i18n.element.min.js?named=i18n-container"></script>
    </head>
    <body>
        <i18n-container lang="es">
            <button data-i18n--title="hello mouse" data-i18n="hello world">hover me</button>
        </i18n-container>
    </body>
</html>

I18n definition import

The very first example shows how to use the component, it shows a button with translated "hello world" text and "hello mouse" tooltip that will be shown on mouse cursor above the button

hover me
{
    "en": { "import": "en.json" },
    "es": { "import": "es.json" },
    "pt": { "import": "pt.json" },
}
{
  "hello world": "hola mundo",
  "hello mouse": "hola ratón"
}
{
  "hello world": "hello world",
  "hello mouse": "hello mouse"
}
{
  "hello world": "olá mundo",
  "hello mouse": "olá rato"
}
<html>
    <head>
        <link rel="i18n-locale-map" href="definition.json">
        <script type="module" src="https://cdn.jsdelivr.net/gh/OmarCastro/its-i18n@0.3.1/dist/i18n.element.min.js?named=i18n-container"></script>
    </head>
    <body>
        <i18n-container lang="es">
            <span data-i18n--title="hello mouse" data-i18n="hello world">hover me</span>
        </i18n-container>
    </body>
</html>

Multiple languages

The language used is based on the lang attribute. It will find the lang on the element and it's ancestors until the document root, the <html> element. If not found it will choose the language defined in the browser using navigator.language

{
  "hello mouse": "hola ratón",
  "I am portuguese": "soy portugués",
  "I am spanish": "soy español",
  "I am english": "soy inglés"
}
{
  "hello mouse": "hello mouse",
  "I am portuguese": "I am portuguese",
  "I am spanish": "I am spanish",
  "I am english": "I am english"
}
{
  "hello mouse": "olá rato",
  "I am portuguese": "sou português",
  "I am spanish": "sou espanhol",
  "I am english": "sou inglês"
}
<i18n-container>
    <div class="portuguese" lang="pt">
        <span data-i18n--title="hello mouse" data-i18n="I am portuguese"></span>
    </div>
    <div class="spanish" lang="es">
        <span data-i18n--title="hello mouse" data-i18n="I am spanish"></span>
    </div>
    <div class="english" lang="en">
        <span data-i18n--title="hello mouse" data-i18n="I am english"></span>
    </div>
</i18n-container>

Pluralization & Specificity

There are use cases when appliyng singular and plural nouns based of the amount used to identify it.

Due to the possibility of template keys, it is easy to apply pluralization, simply add the key match with higher specificity that will be chosen in case of conflicting key match

Specificity

Due to template keys, one i18n key can match multiple translations, making it possible to have conflicts. A conflict happens when one translation matches multiple keys. To solve conflicts, prioritization rules are made to choose which of the translation key are to be used one of them is calculating the specificity of the key, the more specific the key is, the higher the priority.

The key specificity is calculated in the following 2 variables

  1. amount of capture expressions
  2. specificty of each capture expressions

A capture expressions, is the text between curly brackets ({}), as long as opening bracket is not escaped. So, when comparing 2 i18n keys, the key with less capture expressions will have higher priority, indepenently of the specificty of each one. When the amount of capture expressions are equal, it will verify the specificty value of each capture expression. Each on has a defined specificity value defined based on the specificity, e.g. {number} has a higher value than {}

In the unlikely case that both calculated specificity has the same value, the i18n key defined later wins

The next example show the use of specificity to apply i18n pluraliztion.

{
  "I bought 0 carrots": "Não comprei cenouras",
  "I bought 1 carrots": "Comprei 1 cenoura",
  "I bought {number} carrots": "Comprei {0} cenouras"
}
{
  "I bought 0 carrots": "I did not buy carrots",
  "I bought 1 carrots": "I bought 1 carrot",
  "I bought {number} carrots": "I bought {0} carrots"
}
<i18n-container lang="pt">
    
     <span data-i18n="I bought 2 carrots"></span>
</i18n-container>

Time

Its-i18n also support time on i18n keys. The match groups that get the time are 3:

There are time variants by adding "past" and "future" on the prefix the the following examples

{
    "date using timestamp - {unix timestamp}": "date using timestamp: {0}",
    "date using ISO 8601 - {iso 8601}": "date using ISO 8601: {0}",
    "or both ways - {date}": "or both ways: {0}"
}
{
    "date using timestamp - {unix timestamp}": "data usando timestamp: {0}",
    "date using ISO 8601 - {iso 8601}": "data usando ISO 8601: {0}",
    "or both ways - {date}": "ou das duas formas: {0}"
}
 <i18n-container lang="en-GB">

     <p class="unix-timestamp" data-i18n="date using timestamp - 1686862473"></p>

     <p class="iso-8601" data-i18n="date using ISO 8601 - 2023-06-15T20:55:37.313Z"></p>

     <p class="date" data-i18n="or both ways - 0"></p>
 </i18n-container>

Relative Time

There are use cases that is desirable to show the relative time instead of the formatted time, i18n support relative time by specifyying the formatter to use.

Each capture expression has its default formatter, e.g. {date} show the dated formatted to locale, {number} show the number based on locale (some locale use comma as numeric decimal separator), etc.

You can override the formatter by adding the vertical bar after the positional arguments ({ 1 | formatter to use })

The next example shows how is the relative formatter is used by overriding the capture expression default formater

{
    "time since unix": "Unix Epoch aconteceu {'0' | relative time}",
    "relative time to unix: {unix timestamp}": "tempo relativo em unix: {0}, {0 | relative time}",
    "relative time to iso: {iso 8601}": "tempo relativo em IS0 8601: {0}, {0 | relative time}",
    "relative time to both: {date}": "tempo relativo em ambos: {0}, {0 | relative time}"
}
{
    "time since unix": "Unix Epoch happened {'0' | relative time}",
    "relative time to unix: {unix timestamp}": "relative time to unix: {0}, {0 | relative time}",
    "relative time to iso: {iso 8601}": "relative time to IS0 8601: {0}, {0 | relative time}",
    "relative time to both {date}": "relative time to both: {0}, {0 | relative time}"
}
 <i18n-container lang="pt-BR">

     <p class="unix-timestamp" data-i18n="time since unix"></p>
     
     <p class="unix-timestamp" data-i18n="relative time to unix: 1686862473"></p>

     <p class="iso-8601" data-i18n="relative time to iso: 2023-06-15T20:55:37.313Z"></p>

     <p class="date" data-i18n="relative time to both: 2024-01-01T00:00:00.000Z"></p>
 </i18n-container>

Relative Time Duration

While relative time is usefull for most use cases. There are 2 use cases it doesn't solve

  1. Showing only the duration (e.g. "3 days" instead of "in 3 days")
  2. Show the duration in parts (e.g. "3 days and 3 hours" instead of "3 days")

That's where relative time duration formatter comes in, by default it shows only the realtive time duration. It is possible to define the highest or lowest time unit to use when defining the duration

{
    "time since unix": "Unix Epoch aconteceu há {'0' | relative time duration} atrás",
    "relative time to unix: {unix timestamp}": "tempo relativo em unix: {0}, {0 | relative time duration}",
    "relative time to iso: {iso 8601}": "tempo relativo em IS0 8601: {0}, {0 | relative time duration}",
    "relative time to both: {date}": "tempo relativo em ambos: {0}, {0 | relative time duration}",
    "time in days: {date}": "tempo em dias: {0}, {0 | relative time duration in days}",
    "time to days: {date}": "tempo até das: {0}, {0 | relative time duration to days}"
}
{
    "time since unix": "Unix Epoch happened {'0' | relative time duration} ago",
    "relative time to unix: {unix timestamp}": "relative time to unix: {0}, {0 | relative time duration}",
    "relative time to iso: {iso 8601}": "relative time to IS0 8601: {0}, {0 | relative time duration}",
    "relative time to both {date}": "relative time to both: {0}, {0 | relative time duration}",
    "time in days: {date}": "time in days: {0}, {0 | relative time duration in days}",
    "time to days: {date}": "time to days: {0}, {0 | relative time duration to days}"
}
 <i18n-container lang="pt-BR">

     <p class="unix-timestamp" data-i18n="time since unix"></p>
     
     <p class="unix-timestamp" data-i18n="relative time to unix: 1686862473"></p>

     <p class="iso-8601" data-i18n="relative time to iso: 2023-06-15T20:55:37.313Z"></p>

     <p class="date" data-i18n="relative time to both: 2024-01-01T00:00:00.000Z"></p>

     <p class="date" data-i18n="time in days: 2024-01-01T00:00:00.000Z"></p>

     <p class="date" data-i18n="time to days: 2024-01-01T00:00:00.000Z"></p>
     </i18n-container>

Tick Time

You can set a relative time to seconds, but showing seconds without updating it regularly does not help, that's where data-i18n-tick-time comes from, it updates the i18n content when outdated

The next example shows the relative formatter updating every second

{
    "time since unix": "Unix Epoch aconteceu {'0' | relative time duration to seconds}",
    "relative time to unix: {unix timestamp}": "tempo relativo em unix: {0}, {0 | relative time duration to seconds}",
    "relative time to iso: {iso 8601}": "tempo relativo em IS0 8601: {0}, {0 | relative time duration to seconds}",
    "relative time to both: {date}": "tempo relativo em ambos: {0}, {0 | relative time duration to seconds}"
}
{
    "time since unix": "Unix Epoch happened {'0' | relative time duration to seconds}",
    "relative time to unix: {unix timestamp}": "relative time to unix: {0}, {0 | relative time duration to seconds}",
    "relative time to iso: {iso 8601}": "relative time to IS0 8601: {0}, {0 | relative time duration to seconds}",
    "relative time to both {date}": "relative time to both: {0}, {0 | relative time duration to seconds}"
}
 <i18n-container lang="pt-BR">

     <p class="unix-timestamp" data-i18n="time since unix" data-i18n-tick-time></p>
     
     <p class="unix-timestamp" data-i18n="relative time to unix: 1686862473" data-i18n-tick-time></p>

     <p class="iso-8601" data-i18n="relative time to iso: 2023-06-15T20:55:37.313Z" data-i18n-tick-time></p>

     <p class="date" data-i18n="relative time to both: 2024-01-01T00:00:00.000Z" data-i18n-tick-time></p>
 </i18n-container>

Canvas

Its-i18n is meant to be applied for all HTML elements, and canvas is an HTML element, but you need Javascript to paint on the canvas.

For that reason Its-i18n provides a JS API to apply i18n inside canvas.

The next example shows the usage of the JS api that reacts to canvas language change

{
  "hello world": "olá mundo",
  "hello mouse": "olá rato"
}
{
  "hello world": "hola mundo",
  "hello mouse": "hola ratón"
}
{
  "hello world": "hello world",
  "hello mouse": "hello mouse"
}
<i18n-container lang="pt-MZ">
    <canvas class="canvas-example" data-i18n--title="hello mouse"></canvas>
</i18n-container>
import { translate, ElementLangObserver } from 'its-i18n'

const canvas = document.querySelector('canvas.canvas-example')

async function paintHelloWorldOnCanvas (canvas) {
    const context = canvas.getContext('2d')
    context.clearRect(0, 0, canvas.width, canvas.height)
    const text = await translate('hello world', { element: canvas })
    context.font = '30px Arial'
    context.fillStyle = 'green'
    context.fillText(text, 10, 50)
}
  
const observer = new ElementLangObserver((records) => {
    for (const record of records) {
        if (record.target instanceof HTMLCanvasElement) {
            paintHelloWorldOnCanvas(record.target)
        }
    }
})
  
paintHelloWorldOnCanvas(canvas)
observer.observe(canvas)

Canvas only

By using the JS API, the canvas element do not necessarily need to be inside the component.

The next example shows the canvas working without being wrapped in the component

{
  "hello world": "hola mundo"
}
{
  "hello world": "hello world"
}
{
  "hello world": "olá mundo"
}
<canvas class="canvas-example" lang="es"></canvas>
import { translate, ElementLangObserver } from 'its-i18n'

const canvas = document.querySelector('canvas.canvas-example')

async function paintHelloWorldOnCanvas (canvas) {
    const context = canvas.getContext('2d')
    context.clearRect(0, 0, canvas.width, canvas.height)
    const text = await translate('hello world', { element: canvas })
    context.font = '30px Arial'
    context.fillStyle = 'green'
    context.fillText(text, 10, 50)
}
  
const observer = new ElementLangObserver((records) => {
    for (const record of records) {
        if (record.target instanceof HTMLCanvasElement) {
            paintHelloWorldOnCanvas(record.target)
        }
    }
})
  
paintHelloWorldOnCanvas(canvas)
observer.observe(canvas)