-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Date and number localization #2207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
eacc99b
fe812eb
64968dd
a527828
43b73e0
27037af
2a8d38f
d0e0158
592fc9e
614d5be
331b2da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,11 +137,20 @@ module.exports = { | |
|
|
||
| // Localization dictionaries | ||
| // Dictionaries can be provided either here (specific to one chart) or globally | ||
| // by registering them as modules. | ||
| // by registering them as modules (which contain dateFormat specs as well). | ||
| // Here `dictionaries` should be an object of objects | ||
| // {'da': {'Reset axes': 'Nulstil aksler', ...}, ...} | ||
| // When looking for a translation we look at these dictionaries first, then | ||
| // the ones registered as modules. If those fail, we strip off any | ||
| // regionalization ('en-US' -> 'en') and try each again | ||
| dictionaries: {} | ||
| dictionaries: {}, | ||
|
|
||
| // Localization specs for dates and numbers | ||
| // Each localization should be an object with keys matching most of d3.locale, | ||
| // see https://github.com/d3/d3-3.x-api-reference/blob/master/Localization.md | ||
| // {'da': {months: [...], shortMonths: [...], ...}, ...} | ||
| // Unlike d3.locale, every key is optional, we will fall back on English ('en'). | ||
| // Currently `grouping` and `currency` are ignored for our automatic number | ||
| // formatting, but can be used in custom formats. | ||
| formats: {} | ||
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -447,9 +447,19 @@ module.exports = function setConvert(ax, fullLayout) { | |
| ax._min = []; | ||
| ax._max = []; | ||
|
|
||
| // copy ref to fullLayout.separators so that | ||
| // Fropagate localization into the axis so that | ||
|
||
| // methods in Axes can use it w/o having to pass fullLayout | ||
| // Default (non-d3) number formatting uses separators directly | ||
| // dates and d3-formatted numbers use the d3 locale | ||
| // Fall back on default format for dummy axes that don't care about formatting | ||
| var locale = fullLayout._d3locale; | ||
| if(ax.type === 'date') { | ||
| ax._dateFormat = locale ? locale.timeFormat.utc : d3.time.format.utc; | ||
| } | ||
| // occasionally we need _numFormat to pass through | ||
| // even though it won't be needed by this axis | ||
| ax._separators = fullLayout.separators; | ||
| ax._numFormat = locale ? locale.numberFormat : d3.format; | ||
|
|
||
| // and for bar charts and box plots: reset forced minimum tick spacing | ||
| delete ax._minDtick; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -438,6 +438,8 @@ plots.supplyDefaults = function(gd) { | |
| }; | ||
| newFullLayout._traceWord = _(gd, 'trace'); | ||
|
|
||
| var formatObj = getD3FormatObj(gd); | ||
|
|
||
| // first fill in what we can of layout without looking at data | ||
| // because fullData needs a few things from layout | ||
|
|
||
|
|
@@ -447,15 +449,15 @@ plots.supplyDefaults = function(gd) { | |
| var oldWidth = oldFullLayout.width, | ||
| oldHeight = oldFullLayout.height; | ||
|
|
||
| plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); | ||
| plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj); | ||
|
|
||
| if(!newLayout.width) newFullLayout.width = oldWidth; | ||
| if(!newLayout.height) newFullLayout.height = oldHeight; | ||
| } | ||
| else { | ||
|
|
||
| // coerce the updated layout and autosize if needed | ||
| plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); | ||
| plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj); | ||
|
|
||
| var missingWidthOrHeight = (!newLayout.width || !newLayout.height), | ||
| autosize = newFullLayout.autosize, | ||
|
|
@@ -472,6 +474,8 @@ plots.supplyDefaults = function(gd) { | |
| } | ||
| } | ||
|
|
||
| newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); | ||
|
|
||
| newFullLayout._initialAutoSizeIsDone = true; | ||
|
|
||
| // keep track of how many traces are inputted | ||
|
|
@@ -563,6 +567,83 @@ function remapTransformedArrays(cd0, newTrace) { | |
| } | ||
| } | ||
|
|
||
| var formatKeys = [ | ||
| 'days', 'shortDays', 'months', 'shortMonths', 'periods', | ||
| 'dateTime', 'date', 'time', | ||
| 'decimal', 'thousands', 'grouping', 'currency' | ||
| ]; | ||
|
|
||
| /** | ||
| * getD3FormatObj: use _context to get the d3.locale argument object. | ||
| * decimal and thousands can be overridden later by layout.separators | ||
| * grouping and currency are not presently used by our automatic number | ||
| * formatting system but can be used by custom formats. | ||
| * | ||
| * @returns {object} d3.locale format object | ||
| */ | ||
| function getD3FormatObj(gd) { | ||
| var locale = gd._context.locale; | ||
| if(!locale) locale === 'en-US'; | ||
|
|
||
| var formatDone = false; | ||
| var formatObj = {}; | ||
|
|
||
| function includeFormat(newFormat) { | ||
| var formatFinished = true; | ||
| for(var i = 0; i < formatKeys.length; i++) { | ||
| var formatKey = formatKeys[i]; | ||
| if(!formatObj[formatKey]) { | ||
| if(newFormat[formatKey]) { | ||
| formatObj[formatKey] = newFormat[formatKey]; | ||
| } | ||
| else formatFinished = false; | ||
| } | ||
| } | ||
| if(formatFinished) formatDone = true; | ||
| } | ||
|
|
||
| // same as localize, look for format parts in each format spec in the chain | ||
| for(var i = 0; i < 2; i++) { | ||
| var formats = gd._context.formats; | ||
| for(var j = 0; j < 2; j++) { | ||
| var formatj = formats[locale]; | ||
| if(formatj) { | ||
| includeFormat(formatj); | ||
| if(formatDone) break; | ||
| } | ||
| formats = Registry.formatRegistry; | ||
| } | ||
|
|
||
| var baseLocale = locale.split('-')[0]; | ||
| if(formatDone || baseLocale === locale) break; | ||
| locale = baseLocale; | ||
| } | ||
|
|
||
| // lastly pick out defaults from english (non-US, as DMY is so much more common) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I strongly agree with picking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
OK, so what we're doing right now is a bit schizophrenic, but it has a certain logic to it: For strings, the only place non-translated is different is different is color/colour. I guess I don't care much about that, it currently only shows up in "Click to enter Colo(u)rscale title," and I see no legitimate reason to give an incomplete set of string translations unless you're in another english region (in which case Any quibbles with that logic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
None from me 👌 |
||
| if(!formatDone) includeFormat(Registry.formatRegistry.en); | ||
|
|
||
| return formatObj; | ||
| } | ||
|
|
||
| /** | ||
| * getFormatter: combine the final separators with the locale formatting object | ||
| * we pulled earlier to generate number and time formatters | ||
| * TODO: remove separators in v2, only use locale, so we don't need this step? | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm in strong agreement here too 👌 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't forget to add it #420 🚬 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That said, I just remembered that someone (in #1842) has asked for per-axis There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added #420 (comment)
commented over there #1842 (comment) |
||
| * | ||
| * @param {object} formatObj: d3.locale format object | ||
| * @param {string} separators: length-2 string to override decimal and thousands | ||
| * separators in number formatting | ||
| * | ||
| * @returns {object} {numberFormat, timeFormat} d3 formatter factory functions | ||
| * for numbers and time | ||
| */ | ||
| function getFormatter(formatObj, separators) { | ||
| formatObj.decimal = separators.charAt(0); | ||
| formatObj.thousands = separators.charAt(1); | ||
|
|
||
| return d3.locale(formatObj); | ||
| } | ||
|
|
||
| // Create storage for all of the data related to frames and transitions: | ||
| plots.createTransitionData = function(gd) { | ||
| // Set up the default keyframe if it doesn't exist: | ||
|
|
@@ -1144,7 +1225,7 @@ function applyTransforms(fullTrace, fullData, layout, fullLayout) { | |
| return dataOut; | ||
| } | ||
|
|
||
| plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { | ||
| plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { | ||
| function coerce(attr, dflt) { | ||
| return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); | ||
| } | ||
|
|
@@ -1183,7 +1264,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { | |
|
|
||
| coerce('paper_bgcolor'); | ||
|
|
||
| coerce('separators'); | ||
| coerce('separators', formatObj.decimal + formatObj.thousands); | ||
| coerce('hidesources'); | ||
|
|
||
| coerce('colorway'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ exports.layoutArrayContainers = []; | |
| exports.layoutArrayRegexes = []; | ||
| exports.traceLayoutAttributes = {}; | ||
| exports.localeRegistry = {}; | ||
| exports.formatRegistry = {}; | ||
|
|
||
| /** | ||
| * register a module as the handler for a trace type | ||
|
|
@@ -328,23 +329,38 @@ function getTraceType(traceType) { | |
| * the dictionary mapping input strings to localized strings | ||
| * generally the keys should be the literal input strings, but | ||
| * if default translations are provided you can use any string as a key. | ||
| * @param {object} module.format | ||
| * a `d3.locale` format specifier for this locale | ||
| * any omitted keys we'll fall back on en-US | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. Isn't the fallback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to be precise, we'll fall back on no translation (which means |
||
| */ | ||
| exports.registerLocale = function(_module) { | ||
| var locale = _module.name; | ||
| var baseLocale = locale.split('-')[0]; | ||
|
|
||
| var newDict = _module.dictionary; | ||
| var newFormat = _module.format; | ||
| var hasDict = newDict && Object.keys(newDict).length; | ||
| var hasFormat = newFormat && Object.keys(newFormat).length; | ||
|
|
||
| var locales = exports.localeRegistry; | ||
|
|
||
| var formats = exports.formatRegistry; | ||
|
|
||
| // Should we use this dict for the base locale? | ||
| // In case we're overwriting a previous dict for this locale, check | ||
| // whether the base matches the full locale dict now. If we're not | ||
| // overwriting, locales[locale] is undefined so this just checks if | ||
| // baseLocale already had a dict or not. | ||
| if(baseLocale !== locale && locales[baseLocale] === locales[locale]) { | ||
| locales[baseLocale] = newDict; | ||
| // Same logic for dateFormats | ||
| if(baseLocale !== locale) { | ||
| if(hasDict && locales[baseLocale] === locales[locale]) { | ||
| locales[baseLocale] = newDict; | ||
| } | ||
| if(hasFormat && formats[baseLocale] === formats[locale]) { | ||
| formats[baseLocale] = newFormat; | ||
| } | ||
| } | ||
|
|
||
| locales[locale] = newDict; | ||
| if(hasDict) locales[locale] = newDict; | ||
| if(hasFormat) formats[locale] = newFormat; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay | |
| zmask = cd0.zmask, | ||
| range = [trace.zmin, trace.zmax], | ||
| zhoverformat = trace.zhoverformat, | ||
| _separators = trace._separators, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔪 |
||
| x2 = x, | ||
| y2 = y, | ||
| xl, | ||
|
|
@@ -109,7 +108,8 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay | |
| type: 'linear', | ||
| range: range, | ||
| hoverformat: zhoverformat, | ||
| _separators: _separators | ||
| _separators: xa._separators, | ||
| _numFormat: xa._numFormat | ||
| }; | ||
| var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover'); | ||
| zLabel = zLabelObj.text; | ||
|
|
||

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.
This is for our default, adaptive date labels. I'm not too worried about it, as these formats are at least unambiguous. But it shouldn't be too hard to allow these to be localized too, essentially as an extension to the
d3.localeformat objects, should our international users feel strongly about it (and contribute their own alternative format strings).