Skip to content
Merged
3 changes: 3 additions & 0 deletions lib/locale-en-us.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ module.exports = {
name: 'en-US',
dictionary: {
'Click to enter Colorscale title': 'Click to enter Colorscale title'
},
format: {
date: '%m/%d/%Y'
}
};
20 changes: 20 additions & 0 deletions lib/locale-en.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,25 @@ module.exports = {
name: 'en',
dictionary: {
'Click to enter Colorscale title': 'Click to enter Colourscale title'
},
format: {
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
],
shortMonths: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
periods: ['AM', 'PM'],
dateTime: '%a %b %e %X %Y',
date: '%d/%m/%Y',
time: '%H:%M:%S',
decimal: '.',
thousands: ',',
grouping: [3],
currency: ['$', '']
}
};
30 changes: 17 additions & 13 deletions src/lib/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ exports.cleanDate = function(v, dflt, calendar) {
* %{n}f where n is the max number of digits of fractional seconds
*/
var fracMatch = /%\d?f/g;
function modDateFormat(fmt, x, calendar) {
function modDateFormat(fmt, x, formatter, calendar) {

fmt = fmt.replace(fracMatch, function(match) {
var digits = Math.min(+(match.charAt(1)) || 6, 6),
Expand All @@ -387,7 +387,7 @@ function modDateFormat(fmt, x, calendar) {
return 'Invalid';
}
}
return utcFormat(fmt)(d);
return formatter(fmt)(d);
}

/*
Expand Down Expand Up @@ -433,10 +433,12 @@ function formatTime(x, tr) {
return timeStr;
}

var yearFormat = utcFormat('%Y'),
monthFormat = utcFormat('%b %Y'),
dayFormat = utcFormat('%b %-d'),
yearMonthDayFormat = utcFormat('%b %-d, %Y');
// TODO: do these strings need to be localized?
// ie this gives "Dec 13, 2017" but some languages may want eg "13-Dec 2017"
var yearFormatD3 = '%Y';
var monthFormatD3 = '%b %Y';
var dayFormatD3 = '%b %-d';
var yearMonthDayFormatD3 = '%b %-d, %Y';
Copy link
Collaborator Author

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.locale format objects, should our international users feel strongly about it (and contribute their own alternative format strings).


function yearFormatWorld(cDate) { return cDate.formatDate('yyyy'); }
function monthFormatWorld(cDate) { return cDate.formatDate('M yyyy'); }
Expand All @@ -450,6 +452,8 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy');
* fmt: optional, an explicit format string (d3 format, even for world calendars)
* tr: tickround ('y', 'm', 'd', 'M', 'S', or # digits)
* used if no explicit fmt is provided
* formatter: locale-aware d3 date formatter for standard gregorian calendars
* should be the result of exports.getD3DateFormat(gd)
* calendar: optional string, the world calendar system to use
*
* returns the date/time as a string, potentially with the leading portion
Expand All @@ -458,13 +462,13 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy');
* the axis may choose to strip things after it when they don't change from
* one tick to the next (as it does with automatic formatting)
*/
exports.formatDate = function(x, fmt, tr, calendar) {
exports.formatDate = function(x, fmt, tr, formatter, calendar) {
var headStr,
dateStr;

calendar = isWorldCalendar(calendar) && calendar;

if(fmt) return modDateFormat(fmt, x, calendar);
if(fmt) return modDateFormat(fmt, x, formatter, calendar);

if(calendar) {
try {
Expand All @@ -488,14 +492,14 @@ exports.formatDate = function(x, fmt, tr, calendar) {
else {
var d = new Date(Math.floor(x + 0.05));

if(tr === 'y') dateStr = yearFormat(d);
else if(tr === 'm') dateStr = monthFormat(d);
if(tr === 'y') dateStr = formatter(yearFormatD3)(d);
else if(tr === 'm') dateStr = formatter(monthFormatD3)(d);
else if(tr === 'd') {
headStr = yearFormat(d);
dateStr = dayFormat(d);
headStr = formatter(yearFormatD3)(d);
dateStr = formatter(dayFormatD3)(d);
}
else {
headStr = yearMonthDayFormat(d);
headStr = formatter(yearMonthDayFormatD3)(d);
dateStr = formatTime(x, tr);
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/plot_api/plot_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we could combine these into one, so context looks more like the register-able locale modules. I didn't do that because I wanted to preserve compatibility with #2195 but that hasn't been released to npm yet so we can change it if we want. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, dictionaries + formats would become locale as config argument?

If so, I'd vote 👍 for consistency with what Plotly.register expects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and don't worry about breaking things that haven't been released 😉

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combined dictionaries + formats -> locales in 27037af

};
4 changes: 2 additions & 2 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1273,7 +1273,7 @@ function formatDate(ax, out, hover, extraPrecision) {
else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr];
}

var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar),
var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar),
headStr;

var splitIndex = dateStr.indexOf('\n');
Expand Down Expand Up @@ -1451,7 +1451,7 @@ function numFormat(v, ax, fmtoverride, hover) {
if(ax.hoverformat) tickformat = ax.hoverformat;
}

if(tickformat) return d3.format(tickformat)(v).replace(/-/g, MINUS_SIGN);
if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN);

// 'epsilon' - rounding increment
var e = Math.pow(10, -tickRound) / 2;
Expand Down
12 changes: 11 additions & 1 deletion src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love to Fropagate whenever I can ;)

// 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;
Expand Down
6 changes: 3 additions & 3 deletions src/plots/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ module.exports = {
separators: {
valType: 'string',
role: 'style',
dflt: '.,',
editType: 'plot',
description: [
'Sets the decimal and thousand separators.',
'For example, *. * puts a \'.\' before decimals and',
'a space between thousands.'
'For example, *. * puts a \'.\' before decimals and a space',
'between thousands. In English locales, dflt is *.,* but',
'other locales may alter this default.'
].join(' ')
},
hidesources: {
Expand Down
89 changes: 85 additions & 4 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly agree with picking en (not en-us) as our default 👌

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

picking en (not en-us) as our default

OK, so what we're doing right now is a bit schizophrenic, but it has a certain logic to it: en-US is the default locale, but if you specify a different locale and it has an incomplete definition (After potentially combining the region and language definitions), it will fill in with non-translated strings (for which we've use US english, en-US) but en for the missing formatting fields. But the ONLY difference between en and en-US formatting is if you make a custom tickformat with %x (and possibly %c) in it - you get DD/MM/YYYY format from en vs MM/DD/YYYY from en-US. Seemed to me that since the US is about the only country in the world that uses the utterly nonsensical MM/DD/YYYY format, if you've already said you're not in the US we shouldn't fall back on that.

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 en is part of the inheritance chain anyway so you'll get your silly extra "u")

Any quibbles with that logic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any quibbles with that logic?

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in strong agreement here too 👌

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to add it #420 🚬

Copy link
Contributor

Choose a reason for hiding this comment

The 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 separators settings. I didn't really understand that person's use case though. Personally, I think separators are better served as locale/context options, but maybe I'm wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added #420 (comment)

That said, I just remembered that someone (in #1842) has asked for per-axis separators settings.

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:
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1183,7 +1264,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) {

coerce('paper_bgcolor');

coerce('separators');
coerce('separators', formatObj.decimal + formatObj.thousands);
coerce('hidesources');

coerce('colorway');
Expand Down
22 changes: 19 additions & 3 deletions src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ exports.layoutArrayContainers = [];
exports.layoutArrayRegexes = [];
exports.traceLayoutAttributes = {};
exports.localeRegistry = {};
exports.formatRegistry = {};

/**
* register a module as the handler for a trace type
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Isn't the fallback en ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be precise, we'll fall back on no translation (which means en-US) - more detail #2207 (comment)

*/
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;
};
4 changes: 2 additions & 2 deletions src/traces/heatmap/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we probably can 🔪 these now:

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔪 trace._separators in 592fc9e

x2 = x,
y2 = y,
xl,
Expand Down Expand Up @@ -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;
Expand Down
Loading