Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
@lion/localize
Advanced tools
The localization system helps to manage localization data split into locales and automate its loading
🛠 Status: Pilot Phase
Lion Web Components are still in an early alpha stage; they should not be considered production ready yet.
The goal of our pilot phase is to gather feedback from a private group of users. Therefore, during this phase, we kindly ask you to:
- not publicly promote or link us yet: (no tweets, blog posts or other forms of communication about Lion Web Components)
- not publicly promote or link products derived from/based on Lion Web Components
As soon as Pilot Phase ends we will let you know (feel free to subscribe to this issue https://github.com/ing-bank/lion/issues/1)
The localization system helps to manage localization data split into locales and automate its loading. The loading of data tries to be as unobtrusive as possible for a typical workflow while providing a flexible and controllable mechanism for non-trivial use cases. The formatting of data containing numbers and dates takes current locale into account by using Intl MessageFormat specification.
The core of the system is a LocalizeManager
instance which is responsible for data loading and working with these data.
It is exposed as a localize
singleton instance.
This ensures that the data can be cached in the single place and reused across different components and same component instances.
import { localize } from '@lion/localize';
// localize is the instance of LocalizeManager
The system is designed for two different groups of developers: component developers and application developers. Component developers will have a unified way to integrate with localization system while application developers will be able to customize the loading behavior.
As a component developer you get:
Data are split into locales.
Typically the locale is an ES module which is by convention put into the /translations
directory of your project.
But there is also a possibility to serve data from an API using JSON format.
Localization data modules for my-hello-component
might look like these:
/path/to/my-hello-component/translations/en-GB.js
export default {
greeting: 'Hello {name}!',
};
/path/to/my-hello-component/translations/nl-NL.js
export default {
greeting: 'Hallo {name}!',
};
The approach with ES modules is great because it allows to simply reuse basic locale data and override only the needed parts for more specific locales.
/path/to/my-family-component/translations/en.js
export default {
havePartnerQuestion: 'Do you have a partner?',
haveChildrenQuestion: 'Do you have children?',
};
/path/to/my-family-component/translations/en-GB.js
import en from './en.js';
export default en;
/path/to/my-family-component/translations/en-US.js
import en from './en.js';
export default {
...en,
haveChildrenQuestion: 'Do you have kids?',
};
Async method loadNamespace()
can be used to load these data.
localize.loadNamespace(namespace).then(() => {
// do smth when data are loaded
});
The namespace
can be one of two types: an object with an explicit loader function and just a simple string for which the loader has been preconfigured.
Let's look at both cases in depth.
// use the dynamic import to load static assets
localize.loadNamespace({
'my-hello-component': locale => {
// resolves to a module with the module.default `{ greeting: 'Hallo {name}!' }`
return import(`./translations/${locale}.js`);
},
});
Usage of dynamic imports is recommended if you want to be able to create smart bundles later on for a certain locale.
The module must have a default
export as shown above to be handled properly.
But in fact you are not limited in the way how exactly the data are loaded. If you want to fetch it from some API this is also possible.
// fetch from an API
localize.loadNamespace({
'my-hello-component': async locale => {
const response = await fetch(
`https://api.example.com/?namespace=my-hello-component&locale=${locale}`,
);
return response.json(); // resolves to the JSON object `{ greeting: 'Hallo {name}!' }`
},
});
But it does not make much sense to have such a loader function for each of your namespaces. And this is there the second option comes in handy.
setupNamespaceLoader()
:// using the regexp to match all component names staring with 'my-'
localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
const response = await fetch(`https://api.example.com/?namespace=${namespace}&locale=${locale}`);
return response.json();
});
await Promise.all([
localize.loadNamespace('my-hello-component');
localize.loadNamespace('my-goodbye-component');
]);
Thus there is a loader function for all components having a certain prefix in a name.
The single source of truth for page locale is <html lang="my-LOCALE">
.
Accessing it via localize.locale
getter/setter is preferable.
External tools like Google Chrome translate or a demo plugin to change a page locale (e.g. in the Storybook) can directly change the attribute on the <html>
tag.
If <html lang>
is empty the default locale will be set to en-GB
.
Changing the locale is simple yet powerful way to change the language on the page without reloading it:
localize.addEventListener('localeChanged', () => {
// do smth when data are loaded for a new locale
});
// change locale (syncs to `<html lang="es-ES">` and fires the event above)
localize.locale = 'es-ES';
If the locale is changed when a few namespaces have been already loaded for the previous one, all the data will be requested for existing namespaces for a new locale and only after that the event listeners will be called. This ensures that all data necessary for localization is loaded prior to rendering. If a certain namespace for a certain locale has been loaded previously, it will never be fetched again until the tab is reloaded in the browser.
Due to the need to develop application code with not everything translated (yet) to all languages, it is good to have a fallback.
By default the fallback is en-GB
, meaning that if some namespace does not have data for the current page locale, the en-GB
will be loaded, making it an important foundation for all other locales data.
In addition to that the fallback is a necessary mechanism to allow some features of the browsers like Google Chrome translate to work and use the same original data for translations into all not officially supported languages.
We highly discourage using fallback locales as a user, because it is bug-prone with things like date and number formatting. Please use full locales whenever possible.
When all necessary data are loaded and you want to show localized content on the page you need to format the data.
localize.msg
comes into play here.
It expects a key in the format of namespace:name
and can also receive variables as a second argument.
_onNameChanged() {
// inserts 'Hello John!' into the element with id="name"
const greeting = localize.msg('my-hello-component:greeting', { name: 'John' });
this.shadowRoot.querySelector('#greeting').innerText = greeting;
}
localize.msg
uses Intl MessageFormat implementation under the hood, so you can use all of its powerful features like placing a little bit different content based on number ranges or format a date according to the current locale.
This mixin was created to significantly simplify integration with LitElement. It provides many capabilities:
waitForLocalizeNamespaces
;msgLit
for localize.msg
;class MyHelloComponent extends LocalizeMixin(LitElement) {
static get localizeNamespaces() {
// using an explicit loader function
return [
{ 'my-hello-component': locale => import(`./translations/${locale}.js`) }
...super.localizeNamespaces,
];
// using a preconfigured loader function
return ['my-hello-component', ...super.localizeNamespaces];
}
static get waitForLocalizeNamespaces() {
// return false to unblock the rendering till the data are loaded
return true;
}
render() {
return html`
<!-- use this.msgLit() here to inject data, e.g.: -->
<span>${this.msgLit('my-hello-component:greeting')}</span>
`;
}
onLocaleReady() {
super.onLocaleReady();
// life-cycle callback - when data are loaded for initial locale
// (reaction to loaded namespaces defined in `localizeNamespaces`)
}
onLocaleChanged() {
super.onLocaleChanged();
// life-cycle callback - when data are loaded for new locale
// (reaction to `localize.locale` change and namespaces loaded for it)
}
onLocaleUpdated() {
super.onLocaleUpdated();
// life-cycle callback - when localized content needs to be updated
// (literally after `onLocaleReady` or `onLocaleChanged`)
// most DOM updates should be done here with the help of `this.msgLit()` and cached id selectors
}
async inYourOwnMethod() {
// before data are loaded or reloaded
await this.localizeNamespacesLoaded;
// after data are loaded or reloaded
}
}
In the majority of cases defining localizeNamespaces
and using msgLit
in the render
is enough to have a fully working localized component.
As an application developer you get:
If you want to optimize the page rendering and you can inline some of your localization data upfront then there is a simple way to do it:
// my-inlined-data.js
import { localize } from 'lion-localize/localize.js';
localize.addData('en-GB', 'my-namespace', {
/* data */
});
localize.addData('nl-NL', 'my-namespace', {
/* data */
});
// my-app.js
import './my-inlined-data.js'; // must be on top to be executed before any other code using the data
This code must come before any other code which might potentially render before the data are added. You can inline as much locales as you support or sniff request headers on the server side and inline only the needed one.
By convention most components will keep their localization data in ES modules at /translations/%locale%.js
.
But as we have already covered in the documentation for component developers there is a way to change the loading for certain namespaces.
The configuration is done via setupNamespaceLoader()
.
This is sort of a router for the data and is typically needed to fetch it from an API.
// for one specific component
localize.setupNamespaceLoader('my-hello-component', async locale => {
const response = await fetch(
`https://api.example.com/?namespace=my-hello-component&locale=${locale}`,
);
return response.json();
});
// for all components which have a prefix in their names
localize.setupNamespaceLoader(/my-.+/, async (locale, namespace) => {
const response = await fetch(`https://api.example.com/?namespace=${namespace}&locale=${locale}`);
return response.json();
});
Typically in the application you have a prefix in all of your application specific components. Having a way to load their corresponding localization data in a unified way is handy in such cases. But you need to make sure this configuration happens before you run any other code using these namespaces.
FAQs
The localization system helps to manage localization data split into locales and automate its loading
We found that @lion/localize demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.