teach tz.format to recognize localization keys

refs CNVS-19516

so you can say e.g. tz.format(date, 'time.formats.tiny') and it will
choose the appropriate format string based on the locale.

at the same time, use localized format string (using this new
functionality) for $.dateString (and by extensions, datetimeString)
instead of hard-coded english-style format strings.

finally, refactor out I18nStubber javascript spec helper for testing
this.

test-plan:
 - change you profile locale to french
 - view a time formatted by $.datetimeString in the UI, e.g.
   due dates in a course's assignments page
 - should show with "<day> <month> <year> à <24-hour time>" rather than
   using "<month> <day>, <year>" and/or "<12-hour time with am/pm>"

Change-Id: Ic7779917d7af5e0fe9d4ef3cd99e6f12cf141c3c
Reviewed-on: https://gerrit.instructure.com/51447
Reviewed-by: Cody Cutrer <cody@instructure.com>
Tested-by: Jenkins
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
This commit is contained in:
Jacob Fugal 2015-04-01 15:01:43 -06:00
parent c5f77d76f4
commit 2c7a691bf0
5 changed files with 123 additions and 38 deletions

View File

@ -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) {

View File

@ -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][-_]?%(%%)*(?!%)/) &&

View File

@ -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

View File

@ -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'

View File

@ -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