Skip to content

Commit 1cf856a

Browse files
author
James Burke
committed
Initial support for i18n bundles, also allow modules just to define a plain object instead of a callback, useful if no dependencies.
1 parent ab62fa4 commit 1cf856a

16 files changed

Lines changed: 376 additions & 31 deletions

File tree

run.js

Lines changed: 193 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
}
2424
}
2525

26+
//regexp for matching nls (i18n) module names.
27+
var nlsRegExp = /(^.*(^|\.)nls(\.|$))([^\.]*)\.?([^\.]*)/;
28+
2629
/**
2730
* The function that loads modules or executes code that has dependencies
2831
* on other modules.
@@ -34,7 +37,7 @@
3437
if (typeof name == "string") {
3538
//Defining a module.
3639
//First check if there are no dependencies, and adjust args.
37-
if (typeof deps == "function" || deps instanceof Function) {
40+
if (!(deps instanceof Array) && typeof deps != "array") {
3841
contextName = callback;
3942
callback = deps;
4043
deps = [];
@@ -43,7 +46,7 @@
4346
contextName = contextName || run._currContextName;
4447

4548
//If module already defined for context, leave.
46-
var context = contexts[contextName];
49+
var context = run._contexts[contextName];
4750
if (context && context.specified && context.specified[name]) {
4851
return run;
4952
}
@@ -76,10 +79,10 @@
7679

7780
contextName = contextName || run._currContextName;
7881

79-
if (contextName != "run._currContextName") {
82+
if (contextName != run._currContextName) {
8083
//If nothing is waiting on being loaded in the current context,
8184
//then switch run._currContextName to current contextName.
82-
var loaded = contexts[run._currContextName] && contexts[run._currContextName].loaded,
85+
var loaded = run._contexts[run._currContextName] && run._contexts[run._currContextName].loaded,
8386
empty = {},
8487
canSetContext = true;
8588
if (loaded) {
@@ -98,9 +101,12 @@
98101
}
99102

100103
//Grab the context, or create a new one for the given context name.
101-
var context = contexts[contextName] || (contexts[contextName] = {
104+
var context = run._contexts[contextName] || (run._contexts[contextName] = {
102105
waiting: [],
106+
nlsWaiting: {},
103107
baseUrl: run.baseUrl || "./",
108+
locale: typeof navigator == "undefined"? "root" :
109+
(navigator.language || navigator.userLanguage || "root").toLowerCase(),
104110
paths: {},
105111
waitSeconds: 7,
106112
specified: {
@@ -120,7 +126,8 @@
120126
}
121127
return run.apply(window, args);
122128
}
123-
}
129+
},
130+
nls: {}
124131
});
125132

126133
//If have a config object, update the context object with
@@ -147,6 +154,10 @@
147154
}
148155
}
149156
}
157+
158+
if (config.locale) {
159+
context.locale = config.locale.toLowerCase();
160+
}
150161
}
151162

152163
//Store the module for later evaluation.
@@ -167,15 +178,104 @@
167178
context.specified[name] = true;
168179
}
169180

181+
//If the callback is not an actual function, it means it already
182+
//has the definition of the module as a literal value.
183+
if (callback && typeof callback != "function" && !(callback instanceof Function)) {
184+
context.defined[name] = callback;
185+
}
186+
187+
188+
//See if the modules is an nls module and handle it special.
189+
var match = nlsRegExp.exec(name);
190+
if (match) {
191+
//Reconstruct the master bundle name from parts of the regexp match
192+
//nlsRegExp.exec("foo.bar.baz.nls.en-ca.foo") gives:
193+
//["foo.bar.baz.nls.en-ca.foo", "foo.bar.baz.nls.", ".", ".", "en-ca", "foo"]
194+
//nlsRegExp.exec("foo.bar.baz.nls.foo") gives:
195+
//["foo.bar.baz.nls.foo", "foo.bar.baz.nls.", ".", ".", "foo", ""]
196+
//so, if match[5] is blank, it means this is the top bundle definition,
197+
//so it does not have to be handled. Only deal with ones that have a locale
198+
//(a match[4] value but no match[5])
199+
if (match[5]) {
200+
var master = match[1] + match[5];
201+
202+
//Track what locale bundle need to be generated once all the modules load.
203+
var nlsw = (context.nlsWaiting[master] || (context.nlsWaiting[master] = {}));
204+
nlsw[match[4]] = 1;
205+
206+
var bundle = context.nls[master];
207+
if (!bundle) {
208+
//No master bundle yet, ask for it.
209+
context.defined.run([master]);
210+
bundle = context.nls[master] = {};
211+
}
212+
//For nls modules, the callback is just a regular object,
213+
//so save it off in the bundle now.
214+
bundle[match[4]] = callback;
215+
}
216+
}
217+
170218
//Figure out if all the modules are loaded. If the module is not
171219
//being loaded or already loaded, add it to the "to load" list,
172220
//and request it to be loaded.
173221
var needLoad = false;
174222
for (var i = 0, dep; dep = deps[i]; i++) {
175-
if (!(dep in context.loaded)) {
176-
context.loaded[dep] = false;
177-
run.load(dep, contextName);
178-
needLoad = true;
223+
//If it is a string, then a plain dependency
224+
if (typeof dep == "string") {
225+
if (!(dep in context.loaded)) {
226+
context.loaded[dep] = false;
227+
run.load(dep, contextName);
228+
needLoad = true;
229+
}
230+
} else {
231+
//dep is an object, so it is an i18n nls thing.
232+
//Track it in the nls section of the context.
233+
//It may have already been created via a specific locale
234+
//request, so just mixin values in that case, to preserve
235+
//the specific locale bundle object.
236+
var bundle = context.nls[name];
237+
if (bundle) {
238+
run.mixin(bundle, dep);
239+
} else {
240+
context.nls[name] = dep;
241+
}
242+
243+
//Break apart the locale to get the parts.
244+
var parts = context.locale.split("-");
245+
246+
//Now see what bundles exist for each country/locale.
247+
//Want to walk up the chain, so if locale is en-us-foo,
248+
//look for en-us-foo, en-us, en, then root.
249+
var toLoad = [];
250+
var longestMatch = null;
251+
var nlsw = context.nlsWaiting[name] || (context.nlsWaiting[name] = {});
252+
for (var j = parts.length; j > -1; j--) {
253+
var loc = j == 0 ? "root" : parts.slice(0, j).join("-");
254+
var val = dep[loc];
255+
if (val) {
256+
//Store which bundle to use for the default bundle definition.
257+
if (!nlsw.__match) {
258+
nlsw.__match = loc;
259+
}
260+
261+
//Track that the locale needs to be resolved with its parts.
262+
nlsw[loc] = 1;
263+
264+
//If locale value is a string, it means it is a resource that
265+
//needs to be loaded. Track it to load if it has not already
266+
//been asked for.
267+
if (typeof val == "string"
268+
&& !context.specified[val]
269+
&& !(val in context.loaded)) {
270+
toLoad.push(val);
271+
}
272+
}
273+
}
274+
275+
//Load any bundles that are still needed.
276+
if (toLoad.length) {
277+
context.defined.run(toLoad);
278+
}
179279
}
180280
}
181281

@@ -190,7 +290,7 @@
190290
//default context too.
191291
var defContextName = "_runDefault";
192292
run._currContextName = defContextName;
193-
var contexts = {};
293+
run._contexts = {};
194294
var contextLoads = [];
195295

196296
//Set state for page loading.
@@ -227,7 +327,7 @@
227327
//First derive the path name for the module.
228328
var url = run.convertNameToPath(moduleName, contextName);
229329
run.attach(url, contextName, moduleName);
230-
contexts[contextName].startTime = (new Date()).getTime();
330+
run._contexts[contextName].startTime = (new Date()).getTime();
231331
}
232332
}
233333

@@ -242,7 +342,7 @@
242342
return moduleName;
243343
} else {
244344
//A module that needs to be converted to a path.
245-
var paths = contexts[contextName].paths;
345+
var paths = run._contexts[contextName].paths;
246346
var syms = moduleName.split(".");
247347
for (var i = syms.length; i > 0; i--) {
248348
var parentModule = syms.slice(0, i).join(".");
@@ -257,7 +357,7 @@
257357

258358
//Join the path parts together, then figure out if baseUrl is needed.
259359
var url = syms.join("/") + ".js";
260-
return ((url.charAt(0) == '/' || url.match(/^\w+:/)) ? "" : contexts[contextName].baseUrl) + url;
360+
return ((url.charAt(0) == '/' || url.match(/^\w+:/)) ? "" : run._contexts[contextName].baseUrl) + url;
261361
}
262362
}
263363

@@ -266,7 +366,7 @@
266366
* new ones in right dependency order.
267367
*/
268368
run.checkLoaded = function(contextName) {
269-
var context = contexts[contextName || run._currContextName];
369+
var context = run._contexts[contextName || run._currContextName];
270370
var waitInterval = context.waitSeconds * 1000;
271371
//It is possible to disable the wait interval by using waitSeconds of 0.
272372
var expired = waitInterval && (context.startTime + waitInterval) < (new Date()).getTime();
@@ -307,9 +407,63 @@
307407
//Resolve dependencies. First clean up state because the evaluation
308408
//of modules might create new loading tasks, so need to reset.
309409
var waiting = context.waiting;
410+
var nlsWaiting = context.nlsWaiting;
310411
context.waiting = [];
412+
context.nlsWaiting = {};
311413
context.loaded = {};
312-
414+
415+
//First, properly mix in any nls bundles waiting to happen.
416+
//Use an empty object to detect other bad JS code that modifies
417+
//Object.prototype.
418+
var empty = {};
419+
for (var prop in nlsWaiting) {
420+
if (!(prop in empty)) {
421+
//Each property is a master bundle name.
422+
var master = prop;
423+
var msWaiting = nlsWaiting[prop];
424+
var bundle = context.nls[master];
425+
var defLoc = null;
426+
427+
//Create the module name parts from the master name. So, if master
428+
//is foo.nls.bar, then the parts should be prefix: "foo.nls",
429+
// suffix: "bar", and the final locale's module name will be foo.nls.locale.bar
430+
var parts = master.split(".");
431+
var modulePrefix = parts.slice(0, parts.length - 1).join(".");
432+
var moduleSuffix = parts[parts.length - 1];
433+
//Cycle through the locale props on the waiting object and combine
434+
//the locales together.
435+
for (var loc in msWaiting) {
436+
if (!(loc in empty)) {
437+
if (loc == "__match") {
438+
//Found default locale to use for the top-level bundle name.
439+
defLoc = msWaiting[loc];
440+
} else {
441+
//Mix in the properties of this locale together.
442+
//Split the locale into pieces.
443+
var mixed = {};
444+
var parts = loc.split("-");
445+
for (var i = parts.length; i > 0; i--) {
446+
var locPart = parts.slice(0, i).join("-");
447+
if (locPart !== "root" && bundle[locPart]) {
448+
run.mixin(mixed, bundle[locPart]);
449+
}
450+
}
451+
if (bundle["root"]) {
452+
run.mixin(mixed, bundle["root"]);
453+
}
454+
455+
context.defined[modulePrefix + "." + loc + "." + moduleSuffix] = mixed;
456+
}
457+
}
458+
}
459+
460+
//Finally define the default locale. Wait to the end of the property
461+
//loop above so that the default locale bundle has been properly mixed
462+
//together.
463+
context.defined[master] = context.defined[modulePrefix + "." + defLoc + "." + moduleSuffix];
464+
}
465+
}
466+
313467
//Walk the dependencies, doing a depth first search.
314468
var orderedModules = [];
315469
for (var i = 0, module; module = waiting[i]; i++) {
@@ -333,22 +487,18 @@
333487
var depModule = context.defined[dep] || (context.defined[dep] = {});
334488
args.push(depModule);
335489
}
336-
if (module.callback) {
337-
var ret = module.callback.apply(window, args);
490+
491+
//Call the callback to define the module, if necessary.
492+
var cb = module.callback;
493+
if (cb && (typeof cb == "function" || cb instanceof Function)) {
494+
var ret = cb.apply(window, args);
338495
if (name) {
339496
var modDef = context.defined[name];
340497
if (modDef && ret) {
341498
//Mix in the contents of the ret object. This is done for
342499
//cases where we passed the placeholder module to a circular
343500
//dependency.
344-
//Use an empty placeholder object to avoid bad JS code that
345-
//adds things to Object.prototype.
346-
var empty = {};
347-
for (var prop in ret) {
348-
if (!(ret in empty)) {
349-
modDef[prop] = ret[prop];
350-
}
351-
}
501+
run.mixin(modDef, ret);
352502
} else {
353503
context.defined[name] = ret;
354504
}
@@ -440,7 +590,7 @@
440590
var moduleName = node.getAttribute("data-runmodule");
441591

442592
//Mark the module loaded.
443-
contexts[contextName].loaded[moduleName] = true;
593+
run._contexts[contextName].loaded[moduleName] = true;
444594

445595
run.checkLoaded(contextName);
446596

@@ -482,6 +632,22 @@
482632
return null;
483633
}
484634

635+
/**
636+
* Simple function to mix in properties from source into target,
637+
* but only if target does not already have a property of the same name.
638+
*/
639+
run.mixin = function(target, source) {
640+
//Use an empty object to avoid other bad JS code that modifies
641+
//Object.prototype.
642+
var empty = {};
643+
for (var prop in source) {
644+
if (!(prop in target)) {
645+
target[prop] = source[prop];
646+
}
647+
}
648+
return run;
649+
}
650+
485651
//****** START page load functionality ****************
486652
//Set up page on load callbacks. May separate this out.
487653
var isPageLoaded = !isBrowser;

tests/dimple.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
run(
2+
"tests.dimple",
3+
{
4+
color: "dimple-blue"
5+
}
6+
);

0 commit comments

Comments
 (0)