Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy-loading translations #7

Open
bennypowers opened this issue Jul 22, 2019 · 3 comments
Open

Lazy-loading translations #7

bennypowers opened this issue Jul 22, 2019 · 3 comments
Labels
enhancement New feature or request

Comments

@bennypowers
Copy link

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 { /* ... */ }
@andreasbm andreasbm added the enhancement New feature or request label Jul 31, 2019
@andreasbm
Copy link
Owner

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.

// The below example is how parts of the strings could be lazy loaded
listenForLangChanged( () => {
setTimeout(async () => {
const subpageStrings = await (await fetch(`./../assets/i18n/subpage-${translateConfig.lang}.json`)
.then(d => d.json()));
translateConfig.strings = {...translateConfig.strings, ...subpageStrings};
translateConfig.translationCache = {};
this.requestUpdate().then();
console.log(translateConfig, get("subpage.title"));
}, 2000);
});

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 😊

@bennypowers
Copy link
Author

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

@christophe-g
Copy link

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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants