Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Add createI18nMixin. #82

Closed
wants to merge 0 commits into from
Closed

Conversation

mwistrand
Copy link
Contributor

@mwistrand mwistrand commented Oct 18, 2016

Resolve #81 and #199, and also resolve dojo/i18n#16.

Implementation notes:

  • createButton is the only component that includes createI18nMixin by default.
  • If a widget's state.locale is unspecified, the state.locale of each parent is looked up until one is found. Otherwise, i18n.locale is used to determine which messages to load.
  • If a widget's state.labels is specified, but it has not registered any bundles, then the widget's state is not updated.
  • The text direction is set via state.rtl. If true, then the widget's node will have a dir="rtl" attribute. If false, the node will have a dir="ltr" attribute. If not a boolean value, then the node will not have a dir attribute at all.
  • The widget node is also given a data-locale attribute set to the widget's state.locale (if exists). This can facilitate locale-specific styling for any application that may need it.

Copy link
Member

@kitsonk kitsonk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the code coverage is saying that none of the code is covered. Not sure why.

/**
* A map of bundles registered to each instance.
*/
const bundleMap = new WeakMap<I18nMixin<Messages, I18nState>, BundleMap<Messages>>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, since the key is the same for all these WeakMaps, it would be better to create an interface for all of them and store all this private data in the same weak map.

}

return bundleLabels;
}, {} as I18nLabels);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly use of Object.create(null) here?

}
}
}, (instance: I18nMixin<Messages, I18nState>, options?: I18nOptions<I18nState>) => {
bundleMap.set(instance, {} as BundleMap<Messages>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the coercion here?

}
}, (instance: I18nMixin<Messages, I18nState>, options?: I18nOptions<I18nState>) => {
bundleMap.set(instance, {} as BundleMap<Messages>);
const bundles = options && options.bundles || instance.bundles;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no merging of instance and options bundles? Instance bundles will only be used if there are no options bundles?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought here was that bundles would be either registered via instantiation or within mixin/extend, but not both. If you think it would be preferable to merge them, I don't have a problem with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should merge them, because you could see a situation where, in a complex widget, where you would have some strings that are coming from the class and some that you might want to configure on the instance, but still have the ones that were baked into the class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, thinking if a mixin has code that is dependent upon a bundle, and then some developer wants to add their own labels, the accidentally break that mixin. Obviously they could "overwrite" the bundle by just using the same key.

@@ -149,6 +151,7 @@ function generateID(cachedRender: RenderMixin<RenderMixinState>): string {
const widgetClassesMap = new WeakMap<RenderMixin<RenderMixinState>, string[]>();

const createRenderMixin = createStateful
.mixin(createI18nMixin)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we sort of talked about this, but this then assumes that EVERY widget will be an i18n widget. Do we want that hard dependency, since it does feel like RenderMixin doesn't have any real dependency on I18N from what I can see.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a tough call. On the one hand, mixing it directly into createRenderMixin automatically benefits other out-of-the-box widgets like createButton, so that users don't have to store a separate factory somewhere just to use i18n with those widgets. On the other hand, no, not every widget will need to be localized.

At the very least, I'd argue that the out-of-the-box widgets should include createI18nMixin. Whether they inherit that through createRenderMixin or mix it in themselves depends on whether the additional overhead in createRenderMixin does or does not outweigh the inconvenience of users needing to mix createI18nMixin explicitly into their own widgets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They would only need to include it if they have some localised text they would need to display. Many widgets, such as a layout panel, or a tabbed bar even, wouldn't have localised strings. I agree, we should include it in any out-of the box widgets where they display a text string of some sort.

@mwistrand
Copy link
Contributor Author

dojo-i18n has not been published to npm yet, so it currently requires a symlink.

@codecov-io
Copy link

codecov-io commented Oct 18, 2016

Current coverage is 94.69% (diff: 100%)

Merging #82 into master will increase coverage by 0.39%

@@             master        #82   diff @@
==========================================
  Files            11         12     +1   
  Lines           386        415    +29   
  Methods           6          6          
  Messages          0          0          
  Branches         70         77     +7   
==========================================
+ Hits            364        393    +29   
  Misses           10         10          
  Partials         12         12          

Powered by Codecov. Last update 366e435...10b1ac2

@mwistrand
Copy link
Contributor Author

The functional tests that fail here also fail on master (when tested locally).

@mwistrand
Copy link
Contributor Author

Also, dojo/i18n#19 has been resolved, so this issue PR has no outside blockers.

@dylans dylans added this to the 2016.10 milestone Oct 24, 2016
@dylans dylans modified the milestones: 2016.11, 2016.10 Oct 31, 2016
@mwistrand mwistrand changed the title Add createI18nMixin; mix into createRenderMixin. Add createI18nMixin. Dec 4, 2016
};
return i18n(bundle, 'fr').then(function () {
const messages = localized.localizeBundle(bundle);
assert.strictEqual(messages['hello'], 'Bonjour');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this not be messages.hello?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be; I didn't realize TS had been updated to allow dot notation on [key: string] interfaces (previously the threw warnings with dot notation).

}));
localized.localizeBundle(bundle);

setTimeout(dfd.callback(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use dojo-core/timing#delays and return the promise over this.async(...

@dylans dylans modified the milestones: 2016.11, 2016.12 Dec 5, 2016
@agubler
Copy link
Member

agubler commented Dec 5, 2016

General question, I'm not sure that we should support the ability to pass bundles via options, to me everything i18n should be statically declared within a widget and I cannot think of a reasonable use case for being able to pass in bundles and specify keys via state (get's really messy if they need to be update/configured/modified by actions too)... if you get to that point the author would probably be better to extend a widget and import their nls in the custom widget?

Also do we think we need to support per widget locales? what is the use case to have multiple widgets with differing locales on a page? I do think that there is a use case for support changing the main locale however.

Lastly when the locale changes to how does the new nls bundle get loaded and all the widgets invalidated? (assume this happens internally in the i18n module?)

Sorry for all the questions!

@dylans
Copy link
Member

dylans commented Dec 5, 2016

Main use case is if someone wanted to build the UI for something like Google translate, or to have an intentionally bilingual UI (common scenario is apps in Canada where the country strongly encourages two main languages, and someone needs to quickly support both languages).

The main use case we've used it for is training purposes, e.g. to show how to use i18n, it helps to be able to show two languages with two instances of the same widget on a page.

@agubler
Copy link
Member

agubler commented Dec 5, 2016

Main use case is if someone wanted to build the UI for something like Google translate, or to have an intentionally bilingual UI (common scenario is apps in Canada where the country strongly encourages two main languages, and someone needs to quickly support both languages).

Is per widget locale required to support this? Wouldn't this being able to change the main/app locale be sufficient for something like a UI for google translate or a bilingual UI?

@mwistrand
Copy link
Contributor Author

General question, I'm not sure that we should support the ability to pass bundles via options, to me everything i18n should be statically declared within a widget and I cannot think of a reasonable use case for being able to pass in bundles and specify keys via state (get's really messy if they need to be update/configured/modified by actions too)... if you get to that point the author would probably be better to extend a widget and import their nls in the custom widget?

If there is a generic widget that will be reused in a number of contexts (e.g., createSelect that renders a list of options), then forcing users to write something like:

const createCountrySelect = createSelect
    .mixin({
        mixin: { bundles: [ countriesBundle ] }
    });
const countrySelect = createCountrySelect();

Seems more circuitous than it needs to be, with something like the following being preferable:

const countrySelect = createSelect({ bundles: [ countriesBundle ] });

Also do we think we need to support per widget locales? what is the use case to have multiple widgets with differing locales on a page? I do think that there is a use case for support changing the main locale however.

Most applications will not need multiple locales side-by-side, but there are use cases. For example, a language-learning application would have instructions in one locale, but language materials in a different locale. Unless the additional flexibility severely aggravates performance, is there a reason for removing it?

Lastly when the locale changes to how does the new nls bundle get loaded and all the widgets invalidated? (assume this happens internally in the i18n module?)

Right now, dojo-i18n/i18n registers Stateful objects for update when the locale changes. However, dojo-i18n should arguably know nothing about Stateful objects, and as such once dojo/shim#59 lands, dojo-i18n should (imo) be updated to rely on observables.

@agubler
Copy link
Member

agubler commented Dec 5, 2016

If there is a generic widget that will be reused in a number of contexts (e.g., createSelect that renders a list of options), then forcing users to write something like:

const createCountrySelect = createSelect
.mixin({
mixin: { bundles: [ countriesBundle ] }
});
const countrySelect = createCountrySelect();
Seems more circuitous than it needs to be, with something like the following being preferable:

const countrySelect = createSelect({ bundles: [ countriesBundle ] });

How would createSelect ever know what to do with that bundle or what the keys were without intimate knowledge of the bundle itself (i.e. which would basically need to be done author time). I don't think createSelect would be a generic widget like suggested unless it offered something specific outside of dealing with the nls bundles.

Most applications will not need multiple locales side-by-side, but there are use cases. For example, a language-learning application would have instructions in one locale, but language materials in a different locale. Unless the additional flexibility severely aggravates performance, is there a reason for removing it?

I guess I was just worried about overcomplicating/bloating i18n before we'd got the basics in place for how to integrate into a solution with widgets before extended it to support per widget locale switching.

Right now, dojo-i18n/i18n registers Stateful objects for update when the locale changes. However, dojo-i18n should arguably know nothing about Stateful objects, and as such once dojo/shim#59 lands, dojo-i18n should (imo) be updated to rely on observables.

Does it update the state for all the widgets that are i18n registered? How many renders would this cause in a complex page of internationalized widgets?

@mwistrand
Copy link
Contributor Author

mwistrand commented Dec 5, 2016

How would createSelect ever know what to do with that bundle or what the keys were without intimate knowledge of the bundle itself

createI18nMixin uses state.labels to map message bundle keys to state properties (and individual widget factories could piggy back off this as needed). Something like createTextInput might be a better example than createSelect:

const input = createTextInput({
    bundles: [ routeSpecificBundle ],
    state: {
        locale: 'fr',
        labels: {
            placeholder: 'routeSpecificPlaceholder'
        }
    }
});

Does it update the state for all the widgets that are i18n registered? How many renders would this cause in a complex page of internationalized widgets?

If the root locale changes, registered widgets are only be invalidated if they don't have their own locale explicitly set. With dojo-shim/Observable, createI18nMixin would subscribe the widget, and then invalidate itself if it does not have its own locale, or if its own state.locale is changed. The locale switch does cascade to children, so there potentially could be a lot of renders especially for complex applications with only a single locale at the root. Then again, dynamic locale switching will happen rarely in the overwhelming majority of applications, and the build will ensure that supported locale data are bundled with the rest of the application, in which case the messages will be available before the next render cycle.

@agubler
Copy link
Member

agubler commented Dec 5, 2016

createI18nMixin uses state.labels to map message bundle keys to state properties (and individual widget factories could piggy back off this as needed). Something like createTextInput might be a better example than createSelect:

const input = createTextInput({
   bundles: [ routeSpecificBundle ],
   state: {
       locale: 'fr',
       labels: {
           placeholder: 'routeSpecificPlaceholder'
       }
   }
});

For locale's I would envisage that I would always access them in this fashion:

	const messages = this.localizeBundle(textInput);
	return { innerHTML: messages.placeholder };

So if bundles were accepted as an option I would expect the replacement to have the same key as the default bundle within createTextInput and not reference a state attribute placeholder (which at the moment would get set by i18n processing the labels section of the state as I understand it).

Perhaps it would be good to get a demo to show examples of the usage in real widgets set up to go other this as it is pretty hard to completely understand from this alone? What do you think?

@mwistrand
Copy link
Contributor Author

Perhaps it would be good to get a demo to show examples of the usage in real widgets set up to go other this as it is pretty hard to completely understand from this alone? What do you think?

It's outdated, and will be updated again with this implementation, but you can see how this is incorporated into todo-mvc here. As an example, createTodoFooter uses the createButton factory provided out-of-the-box by dojo-widgets to render the "Clear completed" button. Without i18n, the "Clear completed" text is put directly on state.label. With createI18nMixin, the same generic widget is used, but state.labels is used, and state.label is updated with the internationalize "Clear completed" text once the associated bundle has loaded.

localizeBundle could be used to the same effect, but then createButton would have to be extended just for this particular button:

const createClearCompleted = createButton
    .mixin({
        mixin: {
            bundles: [ bundle ],
            nodeAttributes: [
                function (attributes) {
                    const messages = this.localizeBundle(bundle);
                    return { label: messages.clearCompleted };
                }
            }
        }
    });

I'm having a hard time seeing how that is better than the following, which seems to me to be simpler:

const clearCompletedButton = createButton({
    bundles: [ bundle ],
    state: {
        labels: { label: 'clearCompleted' }
    }
});

@agubler
Copy link
Member

agubler commented Dec 5, 2016

For me in the example of the "clear completed" button in todo-mvc that actual i18n bundle wouldn't be dealt with at the button level. It would be dealt with at the enclosing widgets level, in this case the createTodoFooter and then injected in as state, something like:

import { Widget, WidgetOptions, WidgetState, DNode } from 'dojo-widgets/interfaces';
import createWidgetBase from 'dojo-widgets/createWidgetBase';
import d from 'dojo-widgets/d';
import createButton from 'dojo-widgets/components/button/createButton';
import { clearCompleted } from '../actions/userActions';
import createTodoFilter from './createTodoFilter';
import todoFooterBundle from './nls/todoFooterBundle';

export type TodoFooterState = WidgetState & {
	activeFilter?: string;
	activeCount?: number;
	completedCount?: number;
};

export type TodoFooterOptions = WidgetOptions<TodoFooterState>;

export type TodoFooter = Widget<TodoFooterState>;

const createTodoFooter = createWidgetBase
.mixin(createI18nMixin)
.mixin({
	mixin: {
		tagName: 'footer',
		classes: [ 'footer' ],
                bundle: [ todoFooterBundle ],
		getChildrenNodes: function(this: TodoFooter): (DNode | null)[] {
			const { activeCount, activeFilter, completedCount } = this.state;
                        const messages = this.localizeBundle(todoFooterBundle);
			const countLabel = activeCount === 1 ? messages.itemLeft : messages.itemsLeft;

			return [
				d('span', { 'class': 'todo-count' }, [
					d('strong', [ activeCount + ' ' ]),
					d('span', [ countLabel ])
				]),
				d(createTodoFilter, {
					state: {
						classes: [ 'filters' ],
						activeFilter
					}
				}),
				completedCount ? d(createButton, {
					listeners: {
						click: clearCompleted
					},
					state: {
						label: messages.clearCompleted,
						classes: [ 'clear-completed' ]
					}
				}) : null
			];
		}
	}
});

export default createTodoFooter;

It seems reasonable to assume that generic widgets such as createButton, createTextInput etc etc will actually be managed by an enclosing stateful widget that can specify the bundle required for the application.

@agubler
Copy link
Member

agubler commented Dec 5, 2016

The same is true if we wanted to implement the createSelect example mentioned earlier, a widget cannot be standalone it has to be at least wrapped in a projector. For the example that uses createSelect and simply displays a list of countries it could look something like this:

createSelect

import { ComposeFactory } from 'dojo-compose/compose';
import d from 'dojo-widgets/d';
import createWidgetBase from 'dojo-widgets/createWidgetBase';
import { DNode, Widget, WidgetState, WidgetOptions } from 'dojo-widgets/interfaces';

type SelectWidgetState = WidgetState & {
     items: string[];
};

type SelectWidget = Widget<SelectWidgetState>;

type SelectWidgetFactory = ComposeFactory<SelectWidget, WidgetOptions<SelectWidgetState>>;

const createSelect: SelectWidgetFactory = createWidgetBase.mixin({
    mixin: {
        tagName: 'ul',
        getChildrenNodes(this: SelectWidget): DNode[] {
            const { state } = this;
            
            return state.items.map((item) =>
                return d('li', [ item ]);
            });
        }
    }
});

export default createSelect;

Main App Widget

import { DNode } from 'dojo-widgets/interfaces';
import createProjector, { Projector } from 'dojo-widgets/createProjector';
import createI18nMixin from 'dojo-widgets/mixins/createI18nMixin';
import d from 'dojo-widgets/d';

import createSelect, { SelectWidget } from './createSelect';
import bundle from 'nls/appBundle';

const createApp = createProjector
.mixin(createI18nMixin)
.mixin({
    mixin: {
        bundle: [ bundle ],
        getNode(this: Projector): DNode {
            const messages = this.localizeBundle(bundle);

            const items = Object.keys(messages).map((key) => {
                return messages[key];
            });

            return d(createSelect, { state: { items } });
        }         
    }
});

export default createApp;

With the final usage being something like:

import createApp from './createApp';

const app = createApp();

app.append().then(() => {
    console.log('application attached');
});

Apologies if there are any typos or syntax errors I wrote this in the comment, I hope it kind of gives an idea of where I am coming from 😄

@mwistrand
Copy link
Contributor Author

Thanks, @agubler. That does clarify your position. This does have the benefit of separating the simplest components like createButton from needing to mix in createI18nMixin. I will simplify the mixin accordingly and push that as soon as possible.

@agubler
Copy link
Member

agubler commented Dec 7, 2016

@mwistrand this is failing in CI for a lint error.

return locale;
}

const parent = (<any> instance).parent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have no reference to the parent any more in widgets on the instance.

/**
* A map of bundles registered to the instance.
*/
bundles: BundleMap<Messages>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a real Map - Map<string, Messages>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're using localizeBundle instead of state.labels, this is actually no longer needed.

dir: string | null;
}

export type I18nWidget<M extends Messages, S extends I18nState> = I18nMixin<M> & Widget<S>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Widget only requires a single generic, and since Messages is a simple [key: string]: string interface, does is it make sense to specify Messages as another generic for I18nWidget?

Widgets can be internationalized by mixing in `dojo-widgets/mixins/createI18nMixin`. [Message bundles](https://github.com/dojo/i18n) are added by specifying a `bundles` array to the widget factory prototype. When manually accessing localized messages, the bundle should first be passed to `localizeBundle`. If the bundle supports the widget's current locale, but those locale-specific messages have not yet been loaded, then the default messages are returned and the widget will be invalidated once the locale-specific messages have been loaded. Each widget can have its own locale by setting its `state.locale`; if no locale is set, then the default locale as set by [`dojo-i18n`](https://github.com/dojo/i18n) is assumed.

```typescript
const createI18nWidget = createWidgetBase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an example where we use this in getChildrenNodes and pass the i18n values to a child widget via the state?

something like

getChildrenNodes: function() {
    const messages = this.localizedBundle(greetings);

    return [
        d(createButton, { state: { label: messages.buttonLabel } } );
    ];
}

@dylans
Copy link
Member

dylans commented Dec 20, 2016

@mwistrand This has a conflict because of the change from RxJS to ES8 Observables.

@dylans dylans modified the milestones: 2017.01, 2016.12 Dec 21, 2016
"dojo-shim": ">=2.0.0-beta.1",
"globalize": "^1.1.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this a peer dependency and not a dependency of i18n?

*/
function getLocaleMessages(instance: I18nWidget<Messages, I18nProperties>, bundle: Bundle<Messages>): Messages | void {
const { properties } = instance;
const locale = properties['locale'] || i18n.locale;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties.locale?

const createI18nMixin: I18nFactory = compose<I18nMixin<Messages>, WidgetOptions<WidgetState, I18nProperties>>({
nodeAttributes: [
function (this: I18nWidget<Messages, I18nProperties>, attributes: VNodeProperties): VNodeProperties {
const properties = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of properties going on in here, can we call the variable for VNodeProperties vNodeProperties or similar?

}, (instance: I18nWidget<Messages, I18nProperties>) => {
const subscription = observeLocale({
next() {
if (!instance.properties['locale']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instance.properties.locale

Copy link
Member

@agubler agubler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of nits, but otherwise 👍

@mwistrand mwistrand closed this Jan 4, 2017
@mwistrand mwistrand deleted the 81-createI18nMixin branch February 16, 2017 15:56
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorporate dojo-i18n into the widgeting system. LTR and RTL
5 participants