diff --git a/public/javascripts/jquery.instructure_date_and_time.js b/public/javascripts/jquery.instructure_date_and_time.js index 345076c56dc..ebb96f4b176 100644 --- a/public/javascripts/jquery.instructure_date_and_time.js +++ b/public/javascripts/jquery.instructure_date_and_time.js @@ -133,7 +133,7 @@ var speakMessage = function ($this, message) { if (date == null) return ""; var timezone = options && options.timezone; var format = options && options.format; - format = (format !== 'medium') && $.sameYear(date, new Date()) ? '%b %-d' : '%b %-d, %Y'; + format = (format !== 'medium') && $.sameYear(date, new Date()) ? 'date.formats.short' : 'date.formats.medium'; if (typeof timezone == 'string' || timezone instanceof String) { return tz.format(date, format, timezone) || ''; } else { @@ -144,13 +144,10 @@ var speakMessage = function ($this, message) { $.timeString = function(date, options) { if (date == null) return ""; var timezone = options && options.timezone; - // lookup format according to locale, and then turn %l or %k into %-l or - // %-k, respectively, to avoid extra unnecessary space characters - var fmt = I18n.t("#time.formats.tiny").replace(/^%([kl])/, "%-$1"); if (typeof timezone == 'string' || timezone instanceof String) { - return tz.format(date, fmt, timezone) || ''; + return tz.format(date, 'time.formats.tiny', timezone) || ''; } else { - return tz.format(date, fmt) || ''; + return tz.format(date, 'time.formats.tiny') || ''; } }; $.datetimeString = function(datetime, options) { diff --git a/public/javascripts/timezone_core.js b/public/javascripts/timezone_core.js index 9e2e07f566c..cde2f4cef33 100644 --- a/public/javascripts/timezone_core.js +++ b/public/javascripts/timezone_core.js @@ -2,8 +2,9 @@ define([ "jquery", "underscore", "require", - "vendor/timezone" -], function($, _, require, tz) { + "vendor/timezone", + "i18nObj" +], function($, _, require, tz, I18n) { // start with the bare vendor-provided tz() function var _tz = tz; var _preloadedData = {}; @@ -44,6 +45,25 @@ define([ var datetime = tz.parse(value); if (datetime == null) return null; + // translate recognized 'date.formats.*' and 'time.formats.*' to + // appropriate format strings according to locale. + if (format.match(/^(date|time)\.formats\./)) { + var locale_format = I18n.lookup(format); + if (locale_format) { + // in the process, turn %l, %k, and %e into %-l, %-k, and %-e + // (respectively) to avoid extra unnecessary space characters + // + // javascript doesn't have lookbehind, so do the fixing on the reversed + // string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern + // in all the regexes is to make sure we match (once unreversed), e.g., + // both %l and %%%l (literal-% + %l) but not %%l (literal-% + l). + format = locale_format. + split("").reverse().join(""). + replace(/([lke])(?=%(%%)*(?!%))/, '$1-'). + split("").reverse().join(""); + } + } + // some locales may not (according to bigeasy's localization files) use // an am/pm distinction, but could then be incorrectly used with 12-hour // format strings (e.g. %l:%M%P), whether by erroneous format strings in @@ -51,12 +71,8 @@ define([ // result, you might get 3am and 3pm both formatting to the same value. // to prevent this, 12-hour indicators with an am/pm indicator should be // promoted to the equivalent 24-hour indicator when the locale defines - // %P as an empty string. - - // javascript doesn't have lookbehind, so do the fixing on the reversed - // string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern - // in all the regexes is to make sure we match (once unreversed), e.g., - // both %l and %%%l (literal-% + %l) but not %%l (literal-% + l). + // %P as an empty string. ("reverse, look-ahead, reverse" pattern for + // same reason as above) format = format.split("").reverse().join(""); if (_tz(datetime, '%P') === '' && ((format.match(/[lI][-_]?%(%%)*(?!%)/) && diff --git a/spec/coffeescripts/helpers/I18nStubber.coffee b/spec/coffeescripts/helpers/I18nStubber.coffee new file mode 100644 index 00000000000..72784f55d65 --- /dev/null +++ b/spec/coffeescripts/helpers/I18nStubber.coffee @@ -0,0 +1,33 @@ +define ['i18nObj'], (I18n) -> + frames = [] + + I18nStubber = + pushFrame: -> + frames.push + locale: I18n.locale + translations: I18n.translations + I18n.translations = {'en': {}} + + popFrame: -> + throw 'I18nStubber: pop without a stored frame' unless frames.length + {locale, translations} = frames.pop() + I18n.locale = locale + I18n.translations = translations + + stub: (locale, translations) -> + throw 'I18nStubber: stub without a stored frame' unless frames.length + scope = I18n.translations + scope[locale] = {} unless scope[locale] + locale = scope[locale] + for key, value of translations + scope = locale + parts = key.split('.') + last = parts.pop() + for part in parts + scope[part] = {} unless scope[part] + scope = scope[part] + scope[last] = value + + setLocale: (locale) -> + throw 'I18nStubber: setLocale without a stored frame' unless frames.length + I18n.locale = locale diff --git a/spec/coffeescripts/instructureDateAndTimeSpec.coffee b/spec/coffeescripts/instructureDateAndTimeSpec.coffee index b9ea43fec81..1c82ea30cbb 100644 --- a/spec/coffeescripts/instructureDateAndTimeSpec.coffee +++ b/spec/coffeescripts/instructureDateAndTimeSpec.coffee @@ -4,9 +4,9 @@ define [ 'vendor/timezone/America/Detroit' 'vendor/timezone/America/Juneau' 'vendor/timezone/pt_PT' - 'i18nObj' + 'helpers/I18nStubber' 'jquery.instructure_date_and_time' -], ($, tz, detroit, juneau, portuguese, I18n) -> +], ($, tz, detroit, juneau, portuguese, I18nStubber) -> module 'fudgeDateForProfileTimezone', setup: -> @snapshot = tz.snapshot() @@ -137,65 +137,73 @@ define [ ok !$.midnight(date3) module 'dateString', - setup: -> @snapshot = tz.snapshot() - teardown: -> tz.restore(@snapshot) + setup: -> + @snapshot = tz.snapshot() + I18nStubber.pushFrame() + + teardown: -> + tz.restore(@snapshot) + I18nStubber.popFrame() test 'should format in profile timezone', -> + I18nStubber.stub 'en', 'date.formats.medium': "%b %-d, %Y" tz.changeZone(detroit, 'America/Detroit') equal $.dateString(new Date(0)), 'Dec 31, 1969' module 'timeString', setup: -> @snapshot = tz.snapshot() - @localeWas = I18n.locale - @translationsWas = I18n.translations - I18n.translations = - 'en': {'time': {'formats': {'tiny': "%l:%M%P"}}} - 'en-GB': {'time': {'formats': {'tiny': "%k:%M"}}} + I18nStubber.pushFrame() + teardown: -> tz.restore(@snapshot) - I18n.locale = @localeWas - I18n.translations = @translationsWas + I18nStubber.popFrame() test 'should format in profile timezone', -> + I18nStubber.stub 'en', 'time.formats.tiny': "%l:%M%P" tz.changeZone(detroit, 'America/Detroit') equal $.timeString(new Date(0)), '7:00pm' test 'should format according to profile locale', -> - I18n.locale = 'en-GB' + I18nStubber.setLocale 'en-GB' + I18nStubber.stub 'en-GB', 'time.formats.tiny': "%k:%M" equal $.timeString(new Date(46800000)), '13:00' module 'datetimeString', setup: -> @snapshot = tz.snapshot() - @localeWas = I18n.locale - @translationsWas = I18n.translations - I18n.translations = - 'en': {'time': {'formats': {'tiny': "%l:%M%P"}}} - 'pt': {'time': {'event': "%{date} em %{time}"}} + I18nStubber.pushFrame() + teardown: -> tz.restore(@snapshot) - I18n.locale = @localeWas - I18n.translations = @translationsWas + I18nStubber.popFrame() test 'should format in profile timezone', -> tz.changeZone(detroit, 'America/Detroit') + I18nStubber.stub 'en', + 'date.formats.medium': "%b %-d, %Y" + 'time.formats.tiny': "%l:%M%P" + 'time.event': "%{date} at %{time}" equal $.datetimeString(new Date(0)), 'Dec 31, 1969 at 7:00pm' test 'should translate into the profile locale', -> tz.changeLocale(portuguese, 'pt_PT') - I18n.locale = 'pt' - equal $.datetimeString('1970-01-01 15:00:00Z'), "Jan 1, 1970 em 15:00" + I18nStubber.setLocale 'pt' + I18nStubber.stub 'pt', + 'date.formats.medium': "%-d %b %Y" + 'time.formats.tiny': "%k:%M" + 'time.event': "%{date} em %{time}" + equal $.datetimeString('1970-01-01 15:00:00Z'), "1 Jan 1970 em 15:00" # TODO: remove these second argument specs once the pickers know how to parse # localized datetimes test 'should not localize if second argument is false', -> tz.changeLocale(portuguese, 'pt_PT') - I18n.locale = 'pt' + I18nStubber.setLocale 'pt' equal $.datetimeString('1970-01-01 15:00:00Z', {localized: false}), "Jan 1, 1970 at 3:00pm" test 'should still apply profile timezone when second argument is false', -> tz.changeZone(detroit, 'America/Detroit') tz.changeLocale(portuguese, 'pt_PT') - I18n.locale = 'pt' + I18nStubber.setLocale 'pt' equal $.datetimeString(new Date(0), {localized: false}), 'Dec 31, 1969 at 7:00pm' diff --git a/spec/coffeescripts/timezoneSpec.coffee b/spec/coffeescripts/timezoneSpec.coffee index bc5719683ff..c178e7cf5c4 100644 --- a/spec/coffeescripts/timezoneSpec.coffee +++ b/spec/coffeescripts/timezoneSpec.coffee @@ -3,15 +3,20 @@ define [ 'vendor/timezone/America/Detroit' 'vendor/timezone/fr_FR' 'vendor/timezone/pt_PT' -], (tz, detroit, french, portuguese)-> + 'helpers/I18nStubber' +], (tz, detroit, french, portuguese, I18nStubber)-> + module 'timezone', setup: -> @snapshot = tz.snapshot() + I18nStubber.pushFrame() teardown: -> tz.restore(@snapshot) + I18nStubber.popFrame() moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56)) + epoch = new Date(Date.UTC(1970, 0, 1, 0, 0)) test 'parse(valid datetime string)', -> equal +tz.parse(moonwalk.toISOString()), +moonwalk @@ -68,6 +73,32 @@ define [ equal tz.format(time, '%I%P'), "15" equal tz.format(time, '%r'), "15:00:00" + test "format() should recognize date.formats.*", -> + I18nStubber.stub 'en', 'date.formats.short': '%b %-d' + equal tz.format(moonwalk, 'date.formats.short'), "Jul 21" + + test "format() should recognize time.formats.*", -> + I18nStubber.stub 'en', 'time.formats.tiny': '%-l:%M%P' + equal tz.format(epoch, 'time.formats.tiny'), "12:00am" + + test "format() should localize when given a localization key", -> + tz.changeLocale(french, 'fr_FR') + I18nStubber.setLocale 'fr_FR' + I18nStubber.stub 'fr_FR', 'date.formats.full': '%-d %b %Y %-l:%M%P' + equal tz.format(moonwalk, 'date.formats.full'), "21 juil. 1969 2:56" + + test "format() should automatically convert %l to %-l when given a localization key", -> + I18nStubber.stub 'en', 'time.formats.tiny': '%l:%M%P' + equal tz.format(moonwalk, 'time.formats.tiny'), "2:56am" + + test "format() should automatically convert %k to %-k when given a localization key", -> + I18nStubber.stub 'en', 'time.formats.tiny': '%k:%M' + equal tz.format(moonwalk, 'time.formats.tiny'), "2:56" + + test "format() should automatically convert %e to %-e when given a localization key", -> + I18nStubber.stub 'en', 'date.formats.short': '%b %e' + equal tz.format(epoch, 'date.formats.short'), "Jan 1" + test 'shift() should adjust the date as appropriate', -> equal +tz.shift(moonwalk, '-1 day'), moonwalk - 86400000