Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: moment/moment
base: 58998f7c35
...
head fork: moment/moment
compare: 3ce5de4778
Checking mergeability… Don't worry, you can still create the pull request.
  • 7 commits
  • 2 files changed
  • 19 commit comments
  • 2 contributors
Commits on Jun 08, 2012
@rockymeza rockymeza Instance Language Configuration
I have provided a `lang` method on both moment.fn and moment.duration.fn
to allow for instances of moments and durations to have their own
language configuration.  Additionally, the `lang` method is a getter
which returns either the moment's language definition object or the
global language definition object if the moment did not have one set.

Also, I modified the code surrounding meridiem to always expect a
function.  This simplifies the formatter code, and also ensures
inheritability of language configuration values.
64b0f7a
Commits on Jun 10, 2012
@rockymeza rockymeza Fix meridiem isUpper/isLower params to match docs
There were no tests ensuring that this worked before.  Now, the English
language definition relies on isUpper/isLower.  I think this should be a
good enough test of this.
ce99576
@rockymeza rockymeza expose the getLangDefinition function c44b3f0
@rockymeza rockymeza Condense the duration.fn a little bit. b8eb4db
Commits on Jun 11, 2012
@rockymeza rockymeza Condensing the getLangDefinition function 5b9d611
@rockymeza rockymeza getLangDefinition will now automatically load lang
I extracted the loadLang function out of moment.lang so that it could be
used internally in moment.  When developers use moment.fn.lang or
moment.langData now, if the language requested has not yet been loaded,
it will be loaded.  Unlike moment.lang, these functions do not change
the global language config.
02fac57
Commits on Jun 12, 2012
@timrwood timrwood Merge pull request #332 from timrwood/feature/instance-lang
Feature/instance lang
3ce5de4
Showing with 226 additions and 43 deletions.
  1. +128 −42 moment.js
  2. +98 −1 test/moment/lang.js
View
170 moment.js
@@ -20,8 +20,11 @@
// check for nodeJS
hasModule = (typeof module !== 'undefined'),
- // parameters to check for on the lang config
- langConfigProperties = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|weekdaysMin|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'),
+ // Parameters to check for on the lang config. This list of properties
+ // will be inherited from English if not provided in a language
+ // definition. monthsParse is also a lang config property, but it
+ // cannot be inherited and as such cannot be enumerated here.
+ langConfigProperties = 'months|monthsShort|weekdays|weekdaysShort|weekdaysMin|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'),
// ASP.NET json date format regex
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
@@ -86,9 +89,9 @@
// b = placeholder
// t = the current moment being formatted
// v = getValueAtKey function
- // o = moment.ordinal function
+ // o = language.ordinal function
// p = leftZeroFill function
- // m = moment.meridiem value or function
+ // m = language.meridiem value or function
M : '(a=t.month()+1)',
MMM : 'v("monthsShort",t.month())',
MMMM : 'v("months",t.month())',
@@ -101,8 +104,8 @@
w : '(a=new Date(t.year(),t.month(),t.date()-t.day()+5),b=new Date(a.getFullYear(),0,4),a=~~((a-b)/864e5/7+1.5))',
YY : 'p(t.year()%100,2)',
YYYY : 't.year()',
- a : 'm?m(t.hours(),t.minutes(),!1):t.hours()>11?"pm":"am"',
- A : 'm?m(t.hours(),t.minutes(),!0):t.hours()>11?"PM":"AM"',
+ a : 'm(t.hours(),t.minutes(),!0)',
+ A : 'm(t.hours(),t.minutes(),!1)',
H : 't.hours()',
h : 't.hours()%12||12',
m : 't.minutes()',
@@ -139,6 +142,7 @@
this._isUTC = !!isUTC;
this._a = date._a || null;
date._a = null;
+ this._lang = false;
}
// Duration Constructor
@@ -191,6 +195,8 @@
years += absRound(months / 12);
data.years = years;
+
+ this._lang = false;
}
@@ -274,6 +280,51 @@
return date;
}
+ // Loads a language definition into the `languages` cache. The function
+ // takes a key and optionally values. If not in the browser and no values
+ // are provided, it will load the language file module. As a convenience,
+ // this function also returns the language values.
+ function loadLang(key, values) {
+ var i,
+ parse = [];
+
+ if (!values && hasModule) {
+ values = require('./lang/' + key);
+ }
+
+ for (i = 0; i < langConfigProperties.length; i++) {
+ // If a language definition does not provide a value, inherit
+ // from English
+ values[langConfigProperties[i]] = values[langConfigProperties[i]] ||
+ languages.en[langConfigProperties[i]];
+ }
+
+ for (i = 0; i < 12; i++) {
+ parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i');
+ }
+ values.monthsParse = values.monthsParse || parse;
+
+ languages[key] = values;
+
+ return values;
+ }
+
+ // Determines which language definition to use and returns it.
+ //
+ // With no parameters, it will return the global language. If you
+ // pass in a language key, such as 'en', it will return the
+ // definition for 'en', so long as 'en' has already been loaded using
+ // moment.lang. If you pass in a moment or duration instance, it
+ // will decide the language based on that, or default to the global
+ // language.
+ function getLangDefinition(m) {
+ var langKey = (typeof m === 'string') && m ||
+ m && m._lang ||
+ currentLanguage;
+
+ return languages[langKey] || loadLang(langKey);
+ }
+
/************************************
Formatting
@@ -289,7 +340,7 @@
// helper for recursing long date formatting tokens
function replaceLongDateFormatTokens(input) {
- return moment.longDateFormat[input] || input;
+ return getLangDefinition().longDateFormat[input] || input;
}
function makeFormatFunction(format) {
@@ -298,9 +349,9 @@
Fn = Function; // get around jshint
// t = the current moment being formatted
// v = getValueAtKey function
- // o = moment.ordinal function
+ // o = language.ordinal function
// p = leftZeroFill function
- // m = moment.meridiem value or function
+ // m = language.meridiem value or function
return new Fn('t', 'v', 'o', 'p', 'm', output);
}
@@ -313,8 +364,10 @@
// format date using native date object
function formatMoment(m, format) {
+ var lang = getLangDefinition(m);
+
function getValueFromArray(key, index) {
- return moment[key].call ? moment[key](m, format) : moment[key][index];
+ return lang[key].call ? lang[key](m, format) : lang[key][index];
}
while (localFormattingTokens.test(format)) {
@@ -325,7 +378,7 @@
formatFunctions[format] = makeFormatFunction(format);
}
- return formatFunctions[format](m, getValueFromArray, moment.ordinal, leftZeroFill, moment.meridiem);
+ return formatFunctions[format](m, getValueFromArray, lang.ordinal, leftZeroFill, lang.meridiem);
}
@@ -392,7 +445,7 @@
case 'MMM' : // fall through to MMMM
case 'MMMM' :
for (a = 0; a < 12; a++) {
- if (moment.monthsParse[a].test(input)) {
+ if (getLangDefinition().monthsParse[a].test(input)) {
datePartArray[1] = a;
break;
}
@@ -539,14 +592,14 @@
// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
- function substituteTimeAgo(string, number, withoutSuffix, isFuture) {
- var rt = moment.relativeTime[string];
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
+ var rt = lang.relativeTime[string];
return (typeof rt === 'function') ?
rt(number || 1, !!withoutSuffix, string, isFuture) :
rt.replace(/%d/i, number || 1);
}
- function relativeTime(milliseconds, withoutSuffix) {
+ function relativeTime(milliseconds, withoutSuffix, lang) {
var seconds = round(Math.abs(milliseconds) / 1000),
minutes = round(seconds / 60),
hours = round(minutes / 60),
@@ -564,6 +617,7 @@
years === 1 && ['y'] || ['yy', years];
args[2] = withoutSuffix;
args[3] = milliseconds > 0;
+ args[4] = lang;
return substituteTimeAgo.apply({}, args);
}
@@ -579,7 +633,8 @@
}
var date,
matched,
- isUTC;
+ isUTC,
+ ret;
// parse Moment object
if (moment.isMoment(input)) {
date = new Date(+input._d);
@@ -601,7 +656,14 @@
typeof input === 'string' ? makeDateFromString(input) :
new Date(input);
}
- return new Moment(date, isUTC);
+
+ ret = new Moment(date, isUTC);
+
+ if (moment.isMoment(input)) {
+ ret.lang(input._lang);
+ }
+
+ return ret;
};
// creating with utc
@@ -623,7 +685,8 @@
moment.duration = function (input, key) {
var isDuration = moment.isDuration(input),
isNumber = (typeof input === 'number'),
- duration = (isDuration ? input._data : (isNumber ? {} : input));
+ duration = (isDuration ? input._data : (isNumber ? {} : input)),
+ ret;
if (isNumber) {
if (key) {
@@ -633,7 +696,13 @@
}
}
- return new Duration(duration);
+ ret = new Duration(duration);
+
+ if (isDuration) {
+ ret._lang = input._lang;
+ }
+
+ return ret;
};
// humanizeDuration
@@ -649,35 +718,29 @@
// default format
moment.defaultFormat = isoFormat;
- // language switching and caching
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
moment.lang = function (key, values) {
- var i, req,
- parse = [];
+ var i;
+
if (!key) {
return currentLanguage;
}
- if (values) {
- for (i = 0; i < 12; i++) {
- parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i');
- }
- values.monthsParse = values.monthsParse || parse;
- languages[key] = values;
+ if (values || !languages[key]) {
+ loadLang(key, values);
}
if (languages[key]) {
+ // deprecated, to get the language definition variables, use the
+ // moment.fn.lang method or the getLangDefinition function.
for (i = 0; i < langConfigProperties.length; i++) {
- moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]] ||
- languages.en[langConfigProperties[i]];
+ moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]];
}
currentLanguage = key;
- } else {
- if (hasModule) {
- req = require('./lang/' + key);
- moment.lang(key, req);
- }
}
};
- // set default language
+ // Set default language, other languages will inherit from English.
moment.lang('en', {
months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
@@ -691,7 +754,13 @@
LLL : "MMMM D YYYY LT",
LLLL : "dddd, MMMM D YYYY LT"
},
- meridiem : false,
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+ },
calendar : {
sameDay : '[Today at] LT',
nextDay : '[Tomorrow at] LT',
@@ -724,6 +793,9 @@
}
});
+ // returns language data
+ moment.langData = getLangDefinition;
+
// compare moment object
moment.isMoment = function (obj) {
return obj instanceof Moment;
@@ -833,7 +905,7 @@
},
from : function (time, withoutSuffix) {
- return moment.duration(this.diff(time)).humanize(!withoutSuffix);
+ return moment.duration(this.diff(time)).lang(this._lang).humanize(!withoutSuffix);
},
fromNow : function (withoutSuffix) {
@@ -842,7 +914,7 @@
calendar : function () {
var diff = this.diff(moment().sod(), 'days', true),
- calendar = moment.calendar,
+ calendar = this.lang().calendar,
allElse = calendar.sameElse,
format = diff < -6 ? allElse :
diff < -1 ? calendar.lastWeek :
@@ -915,6 +987,18 @@
daysInMonth : function () {
return moment.utc([this.year(), this.month() + 1, 0]).date();
+ },
+
+ // If passed a language key, it will set the language for this
+ // instance. Otherwise, it will return the language configuration
+ // variables for this instance.
+ lang : function (lang) {
+ if (lang === undefined) {
+ return getLangDefinition(this);
+ } else {
+ this._lang = lang;
+ return this;
+ }
}
};
@@ -958,15 +1042,17 @@
humanize : function (withSuffix) {
var difference = +this,
- rel = moment.relativeTime,
- output = relativeTime(difference, !withSuffix);
+ rel = this.lang().relativeTime,
+ output = relativeTime(difference, !withSuffix, this.lang());
if (withSuffix) {
output = (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output);
}
return output;
- }
+ },
+
+ lang : moment.fn.lang
};
function makeDurationGetter(name) {
View
99 test/moment/lang.js
@@ -1,7 +1,7 @@
var moment = require("../../moment");
exports.lang = {
- "getter" : function(test) {
+ "library getter" : function(test) {
test.expect(4);
moment.lang('en');
@@ -17,5 +17,102 @@ exports.lang = {
test.equal(moment.lang(), 'en', 'Lang should reset');
test.done();
+ },
+
+ "library ensure inheritance" : function(test) {
+ test.expect(2);
+
+ moment.lang('made-up', {
+ // I put them out of order
+ months : "February_March_April_May_June_July_August_September_October_November_December_January".split("_")
+ // the rest of the properties should be inherited.
+ });
+
+ test.equal(moment([2012, 5, 6]).format('MMMM'), 'July', 'Override some of the configs');
+ test.equal(moment([2012, 5, 6]).format('MMM'), 'Jun', 'But not all of them');
+
+ test.done();
+ },
+
+ "library langData" : function(test) {
+ test.expect(3);
+ moment.lang('en');
+
+ test.equal(moment.langData().months[0], 'January', 'no arguments returns global');
+ test.equal(moment.langData('zh-cn').months[0], '一月', 'a string returns the language based on key');
+ test.equal(moment.langData(moment().lang('es')).months[0], 'Enero', "if you pass in a moment it uses the moment's language");
+
+ test.done();
+ },
+
+ "instance lang method" : function(test) {
+ test.expect(3);
+ moment.lang('en');
+
+ test.equal(moment([2012, 5, 6]).format('MMMM'), 'June', 'Normally default to global');
+ test.equal(moment([2012, 5, 6]).lang('es').format('MMMM'), 'Junio', 'Use the instance specific language');
+ test.equal(moment([2012, 5, 6]).format('MMMM'), 'June', 'Using an instance specific language does not affect other moments');
+
+ test.done();
+ },
+
+ "instance lang persists with manipulation" : function(test) {
+ test.expect(3);
+ moment.lang('en');
+
+ test.equal(moment([2012, 5, 6]).lang('es').add({days: 1}).format('MMMM'), 'Junio', 'With addition');
+ test.equal(moment([2012, 5, 6]).lang('es').day(0).format('MMMM'), 'Junio', 'With day getter');
+ test.equal(moment([2012, 5, 6]).lang('es').eod().format('MMMM'), 'Junio', 'With eod');
+
+ test.done();
+ },
+
+ "instance lang persists with cloning" : function(test) {
+ test.expect(2);
+ moment.lang('en');
+
+ var a = moment([2012, 5, 6]).lang('es'),
+ b = a.clone(),
+ c = moment(a);
+
+ test.equal(b.format('MMMM'), 'Junio', 'using moment.fn.clone()');
+ test.equal(b.format('MMMM'), 'Junio', 'using moment()');
+
+ test.done();
+ },
+
+ "duration lang method" : function(test) {
+ test.expect(3);
+ moment.lang('en');
+
+ test.equal(moment.duration({seconds: 44}).humanize(), 'a few seconds', 'Normally default to global');
+ test.equal(moment.duration({seconds: 44}).lang('es').humanize(), 'unos segundos', 'Use the instance specific language');
+ test.equal(moment.duration({seconds: 44}).humanize(), 'a few seconds', 'Using an instance specific language does not affect other durations');
+
+ test.done();
+ },
+
+ "duration lang persists with cloning" : function(test) {
+ test.expect(1);
+ moment.lang('en');
+
+ var a = moment.duration({seconds: 44}).lang('es'),
+ b = moment.duration(a);
+
+ test.equal(b.humanize(), 'unos segundos', 'using moment.duration()');
+ test.done();
+ },
+
+ "instance lang used with from" : function(test) {
+ test.expect(2);
+ moment.lang('en');
+
+ var a = moment([2012, 5, 6]).lang('es'),
+ b = moment([2012, 5, 7]);
+
+ test.equal(a.from(b), 'hace un día', 'preserve language of first moment');
+ test.equal(b.from(a), 'in a day', 'do not preserve language of second moment');
+
+ test.done();
}
};

Showing you all comments on commits in this comparison.

@timrwood
Owner

This should stay as isLower to match the docs.

@timrwood
Owner

The !0 and !1 should be switched here so that it works with the correct signature in the docs. http://momentjs.com/docs/#/customization/am-pm/

I think it has been wrong this whole time but as nothing was relying on it, no tests broke.

@timrwood
Owner

Also needs to be switched as outlined above.

@timrwood
Owner

Nice, this will make it really easy to deprecate. Have you tried commenting this out (as if it were fully deprecated) and running the tests to make sure you didn't miss anything?

@rockymeza

Just tested it, the tests pass, but I don't know if any plugins rely on those.

Without the language variables on the global moment object, there is no way to access the currentLanguage. You can get the language name, but not any of the values. If some code needs to use the language definitions outside of moment instances or durations, they will have a difficult time getting the values. Perhaps we can expose the language definitions? We could put getLangDefinnition on the moment global object, and change it a little to accept a key. It could have these signatures I guess:

getLangDefinition(Moment);
getLangDefinition(String);
getLangDefinition();
@rockymeza

We could save some bytes if we put it inside the moment.duration.fn object definition:

...
lang : moment.fn.lang
...

@timrwood, you're always up for saving some bytes right?

@rockymeza

this is the second time moment.isMoment is called inside the constructor. Should we cache the result in a variable?

@timrwood
Owner

They can access it with moment.fn.lang().months or with this.lang() inside a prototype method. I'm not sure how often someone would want to access the lang values from outside a prototype function (or without a moment object).

Maybe, in addition to switching the language, moment.lang(String) can return the object that was loaded with moment.lang(String, Object)? Then to get the default lang they would need to do moment.lang(moment.lang()) which is kinda weird, but I guess using the prototype is probably better in this case.

@timrwood
Owner

Maybe inside the first if block we can do something like the following:

if (moment.isMoment(input)) {
    return new Moment(new Date(+input._d), input._isUTC, input._lang);
}

Then change the Moment constructor to accept the lang as the third parameter. We can drop the ret and isUTC local vars here as well.

@timrwood
Owner

Yep, sounds good!

@rockymeza

I don't think it would be appropriate for moment.lang to do both, switching the global language, and returning a language. I think that that is a bad side affect that is waiting to happen.

I already implemented the langData method in a later commit, but we can revert that if you don't think it's a good idea. I just don't like the idea of not exposing the languages---it seems like a dumb limitation.

@timrwood
Owner

This can probably be switched to the following to save some bytes

var langKey = (typeof m === 'string') && m ||
    m && m._lang ||
    currentLanguage;
@timrwood
Owner

Yeah, that's much cleaner. moment.langData it is.

@rockymeza

Thank you, I was having trouble figuring a good way to do it.

@rockymeza

For the record, this function call will fail in the following example.

moment.lang('de'); // load german
moment.langData('de'); // works

moment.langData('zh-cn'); // I haven't loaded Chinese, so it's a fail.

I don't know if that is something that we have to worry about though. Thoughts?

@timrwood
Owner

Hmm, that could be an annoying bug to hunt down, as it would only happen the first time you started up a process. Maybe we could do something like this in langData?

var old = currentLanguage;
moment.lang('zh-cn').lang(old);
@timrwood
Owner

That wouldn't solve the problem in the browser, but the responsibility is on developers to load the languages in browser anyway.

Also. people doing moment().lang('zh-cn') would run into the same problem as your use case above.

@rockymeza

If we do add this feature, I don't think that it is a good solution to write moment.lang('zh-cn').lang(old). This opens a potential race condition and it has side effects. We should extract the function that adds languages to the languages dictionary into an internal function that both getLangDefinition and moment.lang have access to.

You do have a good point about the moment().lang('zh-cn') scenario too. Should we fix this or should we tell developers to be smart about what they are doing?

@timrwood
Owner

Yeah, maybe an internal loadLang(key) function would be good. It'd be pretty simple.

function loadLang(key) {
    if (hasModule && !languages[key]) {
        moment.lang(key, require('./lang/' + key));
    }
}

Actually, now that I think about it, it should probably be loadLang(key, values) and do everything from line 627 to line 633.

I think this is better than forcing devs to add boilerplate like this:

var moment = require('moment');
var langs = 'en es fr'.split(' '); // make sure all the langs are loaded before we use them.
for (var i = 0; i < langs.length; i++) {
    moment.lang(langs[i]);
}
Something went wrong with that request. Please try again.