lit-translate
lit-translate copied to clipboard
Lazy-loading translations
I'd like to lazy-load strings on a per-feature basis, like
new-feature/
├── app-new-feature.css
├── app-new-feature.graphql
├── app-new-feature.js
├── app-new-feature.spec.js
├── app-new-feature.fr.json
└── app-new-feature.en.json
Currently, it looks from the docs that strings have to be loaded statically as a monolith, either that or deep internals of lit-translate have to be modified. It would be great if I could do something like this:
import { get, lazyLoadStrings } from '@appnest/lit-translate';
import { LitElement, html, customElement } from 'lit-element';
import en from './app-new-feature.en.json';
import fr from './app-new-feature.fr.json';
lazyLoadStrings({ en, fr });
@customElement('app-new-feature')
class AppNewFeature extends LitElement { /* ... */ }
Great idea! I definitely think that lazy-loading strings on a per-feature basis should be encouraged. Currently it is actually possible to lazy-load strings, you can see an example on how you can achieve it here.
https://github.com/andreasbm/lit-translate/blob/7db7188186cfd71b7055ccafd84f375b18517650/src/demo/pages/demo-page.ts#L42-L55
I think it would be a great addition if lit-translate could export a function to allow for easy lazy-loading. I will take a look at it in the near future 😊
Is there a sync API for this as well? In the example above, we work assuming that the entire component module is synchronously loaded, which helps avoid FOUC.
I think both APIs would be useful, one which registers strings synchronously when the module is loaded, and one which loads strings async at runtime
Here is a tentative approach to load locale resources per component (litElement) and not globally for the application. It is a bit hacky as it injects new objects into translateConfig, keyed by component.
What would be nice as an ~enhancement is a cleaner path to achieve the same thing ; )
The mixin below allows:
import { LitElement, html, css } from 'lit-element';
import locale from './myLocale.js';
/*
// Note(cg): myLocal.js example
export default {
completed: {
online: {
title: 'Submit the form',
info: 'Once submitted, <strong>this form can’t be changed</strong>. Please review the information you have provided before submitting.'
}
};
*/
import Translate from '../../util/translate-mixin.js';
class PapFormSectionSubmit extends Translate(LitElement, locale) {
render() {
// this.translate will add component name to the translation key
return html `
<h2>${this.translate('completed.online.title')}</h2>
<p>${this.translateUnsageHTML('completed.online.info')}</p>`;
}
}
// Register the new element with the browser.
customElements.define('pap-form-section-submit', PapFormSectionSubmit);
Mixin (connect stuff connects language to a redux store with https://github.com/albertopumar/lit-element-redux):
import connect from './connect-store.js';
import { registerTranslateConfig, use, translate, translateUnsafeHTML } from 'lit-translate';
const LANG = 'en';
const mapStateToProps = state => {
return {
language: state.language,
};
};
const mapDispatchToProps = dispatch => {
return {
set_language: ([lan]) => dispatch({ type: 'SET_LANGUAGE', language: lan }),
};
};
let translateConfig;
registerTranslateConfig({
// Note(cg): loader is injecting component-keyed additional objects
loader: async (lang, config) => {
translateConfig = config;
// loading per component
const strings = config.strings || {};
config.strings = strings;
config.loaders = config.loaders || {};
config.needLoading = config.needLoading || {};
config.currentLang = config.currentLang || {};
return Promise.all(Object.keys(config.needLoading).map(async key => strings[key] = await config.loaders[key](lang, config)))
.then(() => {
return strings;
});
}
});
// Note(cg): we need to call use early as we need to inject `loaders`, `needLoading` ...
use(LANG);
/**
* mixin enabling component-based translation
* @param {Class} baseElement base class
* @param {Object} locale JSON object containing text for initial/defauld language
* @return {Class} extended class
*/
const EnableTranslation = (baseElement, locale) => {
const cls = class extends baseElement {
static get properties() {
return {
...super.properties,
language: { type: String }
};
}
static get locale() {
return locale;
}
constructor() {
super();
// Note(cg): adding loader first time the class instantiated. .
if (!translateConfig.strings[this.constructor.name]) {
translateConfig.currentLang[this.constructor.name] = LANG;
translateConfig.strings[this.constructor.name] = locale;
translateConfig.loaders[this.constructor.name] = this.translationLoader();
}
}
translationLoader() {
const name = this.constructor.name;
return async (lang, config) => {
if (lang === LANG) {
return locale;
}
// Note(cg): load localized resource on firebase.
return await firebase.database().ref(`/appSettingsLocale/component/${name}/${lang}`).once('value')
.then(snap => {
delete translateConfig.needLoading[name];
return snap.val();
});
};
}
updated(props) {
if (props.has('language')) {
const lang = translateConfig.currentLang[this.constructor.name];
translateConfig.currentLang[this.constructor.name] = this.language;
if (lang !== this.language) {
translateConfig.needLoading[this.constructor.name] = true;
}
use(this.language);
}
super.updated();
}
translate(key) {
return translate(`${this.constructor.name}.${key}`);
}
translateUnsafeHTML(key) {
return translateUnsafeHTML(`${this.constructor.name}.${key}`);
}
};
return connect(mapStateToProps, mapDispatchToProps)(cls);
};
export default EnableTranslation;