-
Notifications
You must be signed in to change notification settings - Fork 39
Conversation
There was a problem hiding this 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>>(); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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>); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Current coverage is 94.69% (diff: 100%)@@ 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
|
The functional tests that fail here also fail on master (when tested locally). |
Also, dojo/i18n#19 has been resolved, so this issue PR has no outside blockers. |
5a390da
to
f495fbd
Compare
322cac0
to
955901f
Compare
1e7825d
to
ff36008
Compare
}; | ||
return i18n(bundle, 'fr').then(function () { | ||
const messages = localized.localizeBundle(bundle); | ||
assert.strictEqual(messages['hello'], 'Bonjour'); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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(() => { |
There was a problem hiding this comment.
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(...
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! |
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. |
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? |
If there is a generic widget that will be reused in a number of contexts (e.g., 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 ] });
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?
Right now, |
How would
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.
Does it update the state for all the widgets that are |
const input = createTextInput({
bundles: [ routeSpecificBundle ],
state: {
locale: 'fr',
labels: {
placeholder: 'routeSpecificPlaceholder'
}
}
});
If the root locale changes, registered widgets are only be invalidated if they don't have their own locale explicitly set. With |
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 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,
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' }
}
}); |
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 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 |
The same is true if we wanted to implement the 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 😄 |
Thanks, @agubler. That does clarify your position. This does have the benefit of separating the simplest components like |
@mwistrand this is failing in CI for a lint error. |
return locale; | ||
} | ||
|
||
const parent = (<any> instance).parent; |
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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>
?
There was a problem hiding this comment.
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.
a6c794e
to
fc97844
Compare
dir: string | null; | ||
} | ||
|
||
export type I18nWidget<M extends Messages, S extends I18nState> = I18nMixin<M> & Widget<S>; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 } } );
];
}
b8b80bb
to
add9a7f
Compare
@mwistrand This has a conflict because of the change from RxJS to ES8 Observables. |
2e75596
to
05d3c12
Compare
"dojo-shim": ">=2.0.0-beta.1", | ||
"globalize": "^1.1.1", |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instance.properties.locale
There was a problem hiding this 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 👍
10b1ac2
to
dbcaf7b
Compare
Resolve #81 and #199, and also resolve dojo/i18n#16.
Implementation notes:
createButton
is the only component that includescreateI18nMixin
by default.state.locale
is unspecified, thestate.locale
of each parent is looked up until one is found. Otherwise,i18n.locale
is used to determine which messages to load.state.labels
is specified, but it has not registered any bundles, then the widget's state is not updated.state.rtl
. Iftrue
, then the widget's node will have adir="rtl"
attribute. Iffalse
, the node will have adir="ltr"
attribute. If not a boolean value, then the node will not have adir
attribute at all.data-locale
attribute set to the widget'sstate.locale
(if exists). This can facilitate locale-specific styling for any application that may need it.