lit-translate icon indicating copy to clipboard operation
lit-translate copied to clipboard

Lazy-loading translations

Open bennypowers opened this issue 6 years ago • 3 comments
trafficstars

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 { /* ... */ }

bennypowers avatar Jul 22 '19 05:07 bennypowers

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 😊

andreasbm avatar Jul 31 '19 15:07 andreasbm

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

bennypowers avatar Aug 01 '19 06:08 bennypowers

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;

christophe-g avatar Oct 16 '20 07:10 christophe-g