Skip to content

Commit

Permalink
Localize numbers in numeric fields
Browse files Browse the repository at this point in the history
  • Loading branch information
1ec5 committed Oct 27, 2021
1 parent 810ce60 commit a733225
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 38 deletions.
4 changes: 2 additions & 2 deletions ACCESSIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ for more info.
|| Browser language preference | iD tries to use the language set in the browser |
| ✅ | Base language fallback | E.g. if `pt_BR` is incomplete, `pt` should be tried before `en` | [#7996](https://github.com/openstreetmap/iD/issues/7996)
| ✅ | Custom fallback languages | If the preferred language is incomplete, user-specified ones should be tried before `en` (e.g. `kk``ru`) | [#7996](https://github.com/openstreetmap/iD/issues/7996)
| 🟠 | [`lang` HTML attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) | Helps with text-to-speech, text formatting, and auto-transliteration, particularly when iD mixes strings from different languages | [#7963](https://github.com/openstreetmap/iD/issues/7963)
| | [`lang` HTML attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) | Helps with text-to-speech, text formatting, and auto-transliteration, particularly when iD mixes strings from different languages | [#7998](https://github.com/openstreetmap/iD/pull/7998)
|| Locale URL parameters | `locale` and `rtl` can be used to manually set iD's locale preferences. See the [API](API.md#url-parameters) |
|| Language selection in UI | The mapper should be able to view and change iD's language in the interface at any time. Useful for public computers with fixed browser languages | [#3120](https://github.com/openstreetmap/iD/issues/3120) |
| 🟩 | Right-to-left layouts | The [`dir` HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) is properly set for languages like Hebrew and Arabic |
|| [Language-specific plurals](https://docs.transifex.com/localization-tips-workflows/plurals-and-genders#how-pluralized-strings-are-handled-by-transifex) | English has two plural forms, but some languages need more to be grammatically correct | [#597](https://github.com/openstreetmap/iD/issues/597), [#7991](https://github.com/openstreetmap/iD/issues/7991) |
| 🟠 | [Localized number formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) | Most in-text numbers are localized. Numeric fields are not | [#3615](https://github.com/openstreetmap/iD/issues/3615), [#7993](https://github.com/openstreetmap/iD/issues/7993) |
| | [Localized number formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) | Most in-text numbers are localized, including numeric fields | [#8769](https://github.com/openstreetmap/iD/pull/8769), [#7993](https://github.com/openstreetmap/iD/issues/7993) |
| 🟠 | Label icons | Icons should accompany text labels to illustrate the meaning of untranslated terms |

### Translatability
Expand Down
19 changes: 19 additions & 0 deletions modules/core/localizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,5 +391,24 @@ export function coreLocalizer() {
return code; // if not found, use the code
};

localizer.floatParser = (locale) => {
// https://stackoverflow.com/a/55366435/4585461
const format = new Intl.NumberFormat(locale);
const parts = format.formatToParts(12345.6);
const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
const index = new Map(numerals.map((d, i) => [d, i]));
const group = new RegExp(`[${parts.find(d => d.type === 'group').value}]`, 'g');
const decimal = new RegExp(`[${parts.find(d => d.type === 'decimal').value}]`);
const numeral = new RegExp(`[${numerals.join('')}]`, 'g');
const getIndex = d => index.get(d);
return (string) => {
string = string.trim()
.replace(group, '')
.replace(decimal, '.')
.replace(numeral, getIndex);
return string ? +string : NaN;
};
};

return localizer;
}
45 changes: 32 additions & 13 deletions modules/ui/fields/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function uiFieldText(field, context) {
var _entityIDs = [];
var _tags;
var _phoneFormats = {};
var parseLocaleFloat = localizer.floatParser(localizer.languageCode());

if (field.type === 'tel') {
fileFetcher.get('phone_formats')
Expand Down Expand Up @@ -114,8 +115,12 @@ export function uiFieldText(field, context) {
var raw_vals = input.node().value || '0';
var vals = raw_vals.split(';');
vals = vals.map(function(v) {
var num = parseFloat(v.trim(), 10);
return isFinite(num) ? clamped(num + d) : v.trim();
v = v.trim();
var num = parseLocaleFloat(v);
if (!isFinite(num)) return v;
num = parseFloat(num, 10);
if (!isFinite(num)) return v;
return clamped(num + d).toLocaleString(localizer.languageCode());
});
input.node().value = vals.join(';');
change()();
Expand Down Expand Up @@ -213,17 +218,20 @@ export function uiFieldText(field, context) {
// don't override multiple values with blank string
if (!val && Array.isArray(_tags[field.key])) return;

if (!onInput) {
if (field.type === 'number' && val) {
var vals = val.split(';');
vals = vals.map(function(v) {
var num = parseFloat(v.trim(), 10);
return isFinite(num) ? clamped(num) : v.trim();
});
val = vals.join(';');
}
utilGetSetValue(input, val);
var displayVal = val;
if (field.type === 'number' && val) {
var vals = val.split(';');
vals = vals.map(function(v) {
v = v.trim();
var num = parseLocaleFloat(v);
if (!isFinite(num)) return v;
num = parseFloat(num, 10);
if (!isFinite(num)) return v;
return clamped(num);
});
val = vals.join(';');
}
if (!onInput) utilGetSetValue(input, displayVal);
t[field.key] = val || undefined;
dispatch.call('change', this, t, onInput);
};
Expand All @@ -242,7 +250,18 @@ export function uiFieldText(field, context) {

var isMixed = Array.isArray(tags[field.key]);

utilGetSetValue(input, !isMixed && tags[field.key] ? tags[field.key] : '')
var val = !isMixed && tags[field.key] ? tags[field.key] : '';
if (field.type === 'number' && val) {
var vals = val.split(';');
vals = vals.map(function(v) {
v = v.trim();
var num = parseFloat(v, 10);
if (!isFinite(num)) return v;
return clamped(num).toLocaleString(localizer.languageCode());
});
val = vals.join(';');
}
utilGetSetValue(input, val)
.attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (field.placeholder() || t('inspector.unknown')))
.classed('mixed', isMixed);
Expand Down
39 changes: 27 additions & 12 deletions modules/ui/fields/roadheight.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection';
import * as countryCoder from '@ideditor/country-coder';

import { uiCombobox } from '../combobox';
import { t } from '../../core/localizer';
import { t, localizer } from '../../core/localizer';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';


Expand All @@ -16,6 +16,7 @@ export function uiFieldRoadheight(field, context) {
var _entityIDs = [];
var _tags;
var _isImperial;
var parseLocaleFloat = localizer.floatParser(localizer.languageCode());

var primaryUnits = [
{
Expand Down Expand Up @@ -124,16 +125,23 @@ export function uiFieldRoadheight(field, context) {

if (!primaryValue && !secondaryValue) {
tag[field.key] = undefined;
} else if (isNaN(primaryValue) || isNaN(secondaryValue) || !_isImperial) {
tag[field.key] = context.cleanTagValue(primaryValue);
} else {
if (primaryValue !== '') {
primaryValue = context.cleanTagValue(primaryValue + '\'');
}
if (secondaryValue !== '') {
secondaryValue = context.cleanTagValue(secondaryValue + '"');
var rawPrimaryValue = parseLocaleFloat(primaryValue);
if (isNaN(rawPrimaryValue)) rawPrimaryValue = primaryValue;
var rawSecondaryValue = parseLocaleFloat(secondaryValue);
if (isNaN(rawSecondaryValue)) rawSecondaryValue = secondaryValue;

if (isNaN(rawPrimaryValue) || isNaN(rawSecondaryValue) || !_isImperial) {
tag[field.key] = context.cleanTagValue(rawPrimaryValue);
} else {
if (rawPrimaryValue !== '') {
rawPrimaryValue = context.cleanTagValue(rawPrimaryValue + '\'');
}
if (rawSecondaryValue !== '') {
rawSecondaryValue = context.cleanTagValue(rawSecondaryValue + '"');
}
tag[field.key] = rawPrimaryValue + rawSecondaryValue;
}
tag[field.key] = primaryValue + secondaryValue;
}

dispatch.call('change', this, tag);
Expand All @@ -151,26 +159,33 @@ export function uiFieldRoadheight(field, context) {
if (primaryValue && (primaryValue.indexOf('\'') >= 0 || primaryValue.indexOf('"') >= 0)) {
secondaryValue = primaryValue.match(/(-?[\d.]+)"/);
if (secondaryValue !== null) {
secondaryValue = secondaryValue[1];
secondaryValue = parseFloat(secondaryValue[1], 10).toLocaleString(localizer.languageCode());
}
primaryValue = primaryValue.match(/(-?[\d.]+)'/);
if (primaryValue !== null) {
primaryValue = primaryValue[1];
primaryValue = parseFloat(primaryValue[1], 10).toLocaleString(localizer.languageCode());
}
_isImperial = true;
} else if (primaryValue) {
var rawValue = primaryValue;
primaryValue = parseFloat(rawValue, 10);
if (isNaN(primaryValue)) primaryValue = rawValue;
primaryValue = primaryValue.toLocaleString(localizer.languageCode());
_isImperial = false;
}
}

setUnitSuggestions();

// If feet are specified but inches are omitted, assume zero inches.
var inchesPlaceholder = (0).toLocaleString(localizer.languageCode());

utilGetSetValue(primaryInput, typeof primaryValue === 'string' ? primaryValue : '')
.attr('title', isMixed ? primaryValue.filter(Boolean).join('\n') : null)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : t('inspector.unknown'))
.classed('mixed', isMixed);
utilGetSetValue(secondaryInput, typeof secondaryValue === 'string' ? secondaryValue : '')
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (_isImperial ? '0' : null))
.attr('placeholder', isMixed ? t('inspector.multiple_values') : (_isImperial ? inchesPlaceholder : null))
.classed('mixed', isMixed)
.classed('disabled', !_isImperial)
.attr('readonly', _isImperial ? null : 'readonly');
Expand Down
31 changes: 20 additions & 11 deletions modules/ui/fields/roadspeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { select as d3_select } from 'd3-selection';
import * as countryCoder from '@ideditor/country-coder';

import { uiCombobox } from '../combobox';
import { t } from '../../core/localizer';
import { t, localizer } from '../../core/localizer';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';


Expand All @@ -14,6 +14,7 @@ export function uiFieldRoadspeed(field, context) {
var _entityIDs = [];
var _tags;
var _isImperial;
var parseLocaleFloat = localizer.floatParser(localizer.languageCode());

var speedCombo = uiCombobox(context, 'roadspeed');
var unitCombo = uiCombobox(context, 'roadspeed-unit')
Expand Down Expand Up @@ -85,8 +86,8 @@ export function uiFieldRoadspeed(field, context) {

function comboValues(d) {
return {
value: d.toString(),
title: d.toString()
value: d.toLocaleString(localizer.languageCode()),
title: d.toLocaleString(localizer.languageCode())
};
}

Expand All @@ -100,10 +101,14 @@ export function uiFieldRoadspeed(field, context) {

if (!value) {
tag[field.key] = undefined;
} else if (isNaN(value) || !_isImperial) {
tag[field.key] = context.cleanTagValue(value);
} else {
tag[field.key] = context.cleanTagValue(value + ' mph');
var rawValue = parseLocaleFloat(value);
if (isNaN(rawValue)) rawValue = value;
if (isNaN(rawValue) || !_isImperial) {
tag[field.key] = context.cleanTagValue(rawValue);
} else {
tag[field.key] = context.cleanTagValue(rawValue + ' mph');
}
}

dispatch.call('change', this, tag);
Expand All @@ -113,16 +118,20 @@ export function uiFieldRoadspeed(field, context) {
roadspeed.tags = function(tags) {
_tags = tags;

var value = tags[field.key];
var rawValue = tags[field.key];
var value = rawValue;
var isMixed = Array.isArray(value);

if (!isMixed) {
if (value && value.indexOf('mph') >= 0) {
value = parseInt(value, 10).toString();
_isImperial = true;
} else if (value) {
if (rawValue && rawValue.indexOf('mph') >= 0) {
_isImperial = rawValue && rawValue.indexOf('mph') >= 0;
} else if (rawValue) {
_isImperial = false;
}

value = parseInt(value, 10);
if (isNaN(value)) value = rawValue;
value = value.toLocaleString(localizer.languageCode());
}

setUnitSuggestions();
Expand Down
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
'spec/core/file_fetcher.js',
'spec/core/graph.js',
'spec/core/history.js',
'spec/core/localizer.js',
'spec/core/locations.js',
'spec/core/tree.js',
'spec/core/validator.js',
Expand Down
36 changes: 36 additions & 0 deletions test/spec/core/localizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
describe('iD.coreLocalizer', function () {
describe('#floatParser', function () {
it('roundtrips English numbers', function () {
var localizer = iD.coreLocalizer();
var parseFloat = localizer.floatParser('en');
expect(parseFloat((-0.1).toLocaleString(localizer.languageCode()))).to.eql(-0.1);
expect(parseFloat((1.234).toLocaleString(localizer.languageCode()))).to.eql(1.234);
expect(parseFloat(1234).toLocaleString(localizer.languageCode())).to.eql(1234);
expect(parseFloat(1234.56).toLocaleString(localizer.languageCode())).to.eql(1234.56);
});
it('roundtrips Spanish numbers', function () {
var localizer = iD.coreLocalizer();
var parseFloat = localizer.floatParser('es');
expect(parseFloat((-0.1).toLocaleString(localizer.languageCode()))).to.eql(-0.1);
expect(parseFloat((1.234).toLocaleString(localizer.languageCode()))).to.eql(1.234);
expect(parseFloat(1234).toLocaleString(localizer.languageCode())).to.eql(1234);
expect(parseFloat(1234.56).toLocaleString(localizer.languageCode())).to.eql(1234.56);
});
it('roundtrips Arabic numbers', function () {
var localizer = iD.coreLocalizer();
var parseFloat = localizer.floatParser('ar-EG');
expect(parseFloat((-0.1).toLocaleString(localizer.languageCode()))).to.eql(-0.1);
expect(parseFloat((1.234).toLocaleString(localizer.languageCode()))).to.eql(1.234);
expect(parseFloat(1234).toLocaleString(localizer.languageCode())).to.eql(1234);
expect(parseFloat(1234.56).toLocaleString(localizer.languageCode())).to.eql(1234.56);
});
it('roundtrips Bengali numbers', function () {
var localizer = iD.coreLocalizer();
var parseFloat = localizer.floatParser('bn');
expect(parseFloat((-0.1).toLocaleString(localizer.languageCode()))).to.eql(-0.1);
expect(parseFloat((1.234).toLocaleString(localizer.languageCode()))).to.eql(1.234);
expect(parseFloat(1234).toLocaleString(localizer.languageCode())).to.eql(1234);
expect(parseFloat(1234.56).toLocaleString(localizer.languageCode())).to.eql(1234.56);
});
});
});

0 comments on commit a733225

Please sign in to comment.