PolymerElements / app-localize-behavior

Polymer behaviour to help internationalize your application
48 stars 55 forks source link

Feature request: Support multiple locale/resource files #75

Closed davidsteinberger closed 7 years ago

davidsteinberger commented 7 years ago

Description

As a developer it would be great if app-localize-behavior would support loading multiple locale/json files. That would allow devs to split the locales into multiple files and prevent duplicated translations across components.

IMO that could be as achieved by having __onRequestResponse() merge the response into the resources object, instead of replacing it.

I'd be happy to work on a PR for that. What do you think?

H3dz commented 7 years ago

I agree that we need something like this.

JonathanWolfe commented 7 years ago

I solved this at my office by doing stuff like the below:

  1. Create a behavior that will utilize app-localize-behavior
  2. Add a property to store iteration count of all resources you want to load
  3. Override __onRequestResponse to wait till everything is loaded
  4. Merge all the resources together
  5. Use the new behavior in the element

In the element being translated

Polymer( {
    // ...
    behaviors: [
        window.app.behaviors.localizer( [ 'core', 'other', 'feature' ] ),
    ],
    // ...
} );

Helpers.js

class Helper {
    isObject( source ) {
        const nonNullObject = source && typeof source === 'object';
        const toString = Object.prototype.toString.call( source );

        return nonNullObject && toString === '[object Object]';
    }

    mergeObjects( target, ...sources ) {
        if ( !sources.length ) return target;

        const source = sources.shift();
        const helpers = this;

        if ( this.isObject( target ) && this.isObject( source ) ) {

            Object.keys( source ).forEach( function eachKey( key ) {

                if ( helpers.isObject( source[ key ] ) ) {

                    if ( !target[ key ] ) {
                        Object.assign( target, { [ key ]: {} } );
                    }

                    helpers.mergeObjects( target[ key ], source[ key ] );

                } else {
                    Object.assign( target, { [ key ]: source[ key ] } );
                }

            } );

        }

        return this.mergeObjects( target, ...sources );
    }
}

localizer.js

window.app.behaviors = window.app.behaviors || {};
window.app.behaviors.localizerImpl = function localizerImpl( features ) {
    if ( !Array.isArray( features ) ) features = [ features ];

    return {
        properties: {
            resourcesLeftToLoad: {
                type: Number,
                value: features.length,
            },
        },

        __onRequestResponse: function __onRequestResponse( event ) {
            this._tempResources = window.app.helper.mergeObjects( this._tempResources || {}, event.response );

            if ( this.resourcesLeftToLoad !== 1 ) {
                this.resourcesLeftToLoad -= 1;
            } else {
                this.resources = this._tempResources;

                this.fire( 'app-localize-resources-loaded', event, { bubbles: this.bubbleEvent } );
            }
        },

        attached: function attached() {
            const component = this;

            // ... determine language to load ...

            features.forEach( function eachFilter( feature ) {
                const url = 'http://google.com'; // build your url

                component.loadResources( url );
            } );
        },
    };
};

window.app.behaviors.localizer = function localizer( features ) {
    return [ Polymer.AppLocalizeBehavior, window.app.behaviors.localizerImpl( features ) ];
};

@notwaldorf: This could easily be adopted into the main repo. If you let me know your browser targets I can polyfill the needed methods and open a PR.

jab commented 7 years ago

I hit this too, but for the apps I'm building, the simpler changes from #110 were the only changes to AppLocalizeBehavior that were needed to achieve this.

With those changes, I'm able to pass a mergeResources callback to loadResources as the preprocessor parameter, which merges the newly fetched resources into the existing resources that have been fetched (both for other languages and for other pages, since it's a SPA).

You can also listen for the app-localize-resources-loaded event to do any additional bookkeeping needed once the resources have been updated (e.g. notifying Polymer about any deep paths that have changed).

jab commented 7 years ago

That said, even though the changes from #110 are sufficient for this (as well as being more generally useful), I think there is still room for the AppLocalizeBehavior to offer a more ergonomic API for this use case.

To that end, I just pushed https://github.com/jab/app-localize-behavior/commit/b9bb3b1 to explore another idea: This adds two more optional parameters to loadResources: language, and merge. If the language parameter is supplied, the resources that have just been fetched are assigned into resources.language rather than into resources. And if you pass merge = true, the new resources are merged into resources rather than clobbering them. (This uses Object.assign, but you can also pass your own merge function instead of true and that will be used instead, in case you need deep merge.)

This is quite a bit more ergonomic for our apps than what I described in my previous comment. It's also a better fit for us than @JonathanWolfe's solution, since each loadResources call remains independent of any others, and doesn't need to know or care whether any other resources have been loaded yet. This way, when we have a request in flight for the resources for the global nav elements at the same time we have a request in flight for the current page's, whichever one finishes first will result in that part of the UI getting displayed immediately, without having to wait for the other one to finish.

I pushed a small demo of an app that uses this up to https://uproxy.github.io/uproxy.org/ in case anyone wants to take a peek.

I also just submitted these changes for consideration in #111, in case it's easier to look at this more holistically.