Skip to content

Commit 5c5c9b5

Browse files
committed
Implement a schema for the measurement field
1 parent 7c14759 commit 5c5c9b5

File tree

10 files changed

+252
-10
lines changed

10 files changed

+252
-10
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ jobs:
2424
with:
2525
node-version: ${{ matrix.node-version }}
2626
- run: npm install
27+
- run: npm run build
2728
- run: npm run lint
2829
- run: npm run test

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ npm-debug.log
1010
/tests/workspace
1111

1212
transifex.auth
13+
schemas/generated

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,7 @@ A string specifying the UI and behavior of the field. Must be one of the followi
473473

474474
* `access` - Block of dropdowns for defining the `access=*` tags on a highway
475475
* `address` - Block of text and dropdown fields for entering address information (localized for editing location)
476-
* `roadspeed` - Numeric text field for speed and dropdown for "mph" / "km/h", defaulting to the speed unit used for roads in the feature's region
477-
* `roadheight` - Numeric text field for height and dropdowns for "m" / "ft" and "in", defaulting to the height unit used for roads in the feature's region
476+
* `measurement` - Numeric text field for dimensional numbers that have a unit of measurement, such as inches or kilometres-per-hour. The field defaults to the unit of measurement used in the feature's region. See [#measurement](#measurement) for details.
478477
* `restrictions` - Graphical field for editing turn restrictions
479478
* `wikidata` - Search field for selecting a Wikidata entity
480479
* `wikipedia` - Block of fields for selecting a wiki language and Wikipedia page
@@ -740,6 +739,34 @@ Combo field types can accept key-label pairs in the `options` value of the `stri
740739

741740
An optional property to reference to the icons of another field, indicated by using that field's name contained in brackets, like `{field}`. This is for example useful when there are multiple variants of fields for the same tag, which should all use the same icons.
742741

742+
##### `measurement`
743+
744+
Used when `type = measurement`. Defines the unit of measurements that are supported by this field. For example:
745+
746+
```json
747+
{
748+
"key": "diameter",
749+
"type": "measurement",
750+
"measurement": {
751+
// The dimension being measured. This constrains the permitted units.
752+
// The ID id defined by CLDR.
753+
"dimension": "length",
754+
755+
"units": {
756+
// The key defines the ID of the of the unit, as defined by CLDR.
757+
// The values define the suffix used in the OSM tag value. If there
758+
// are multiple values, then the first one is the preferred value.
759+
"meter": ["m"],
760+
"centimeter": ["cm"],
761+
"millimeter": [""], // mm is the default unit in OSM, so the tag value should have no suffix.
762+
"yard": ["yd"]
763+
}
764+
}
765+
}
766+
```
767+
768+
Translations for the `narrow` and `long` form of each unit are bundled into iD-tagging-schema's locale files.
769+
743770
### Deprecations
744771

745772
Use `deprecated.json` ([Example](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/deprecated.json), [Schema](https://github.com/ideditor/schema-builder/blob/main/schemas/deprecated.json)) to specify tag deprecations.

lib/build.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,25 @@ const defaultsSchema = require('../schemas/preset_defaults.json');
2020
const deprecatedSchema = require('../schemas/deprecated.json');
2121
const discardedSchema = require('../schemas/discarded.json');
2222

23+
/** @import { TranslationOptions } from "./translations.js" */
24+
25+
/** @typedef {{
26+
inDirectory: string;
27+
interimDirectory: string;
28+
outDirectory: string;
29+
sourceLocale: string;
30+
taginfoProjectInfo: unknown,
31+
processCategories: null | unknown;
32+
processFields: null | unknown;
33+
processPresets: null | unknown;
34+
listReusedIcons: boolean;
35+
}} BuildOptions */
36+
37+
/** @typedef {Partial<BuildOptions & TranslationOptions>} Options */
38+
2339
let _currBuild = null;
2440

41+
/** @param {Options} options */
2542
function validateData(options) {
2643
const START = '🔬 ' + styleText('yellow', 'Validating schema...');
2744
const END = '👍 ' + styleText('green', 'schema okay');
@@ -36,6 +53,7 @@ function validateData(options) {
3653
process.stdout.write('\n');
3754
}
3855

56+
/** @param {Options} options */
3957
function buildDev(options) {
4058

4159
if (_currBuild) return _currBuild;
@@ -53,6 +71,7 @@ function buildDev(options) {
5371
process.stdout.write('\n');
5472
}
5573

74+
/** @param {Options} options */
5675
function buildDist(options) {
5776

5877
if (_currBuild) return _currBuild;
@@ -78,7 +97,8 @@ function buildDist(options) {
7897
});
7998
}
8099

81-
function processData(options, type) {
100+
/** @internal @param {Options} options @returns {Options} */
101+
export function getDefaultOptions(options) {
82102
if (!options) options = {};
83103
options = Object.assign({
84104
inDirectory: 'data',
@@ -91,7 +111,15 @@ function processData(options, type) {
91111
processPresets: null,
92112
listReusedIcons: false
93113
}, options);
114+
return options;
115+
}
94116

117+
/**
118+
* @param {Options} options
119+
* @param {'build-interim' | 'build-dist' | 'validate'} type
120+
*/
121+
function processData(options, type) {
122+
options = getDefaultOptions(options);
95123
const dataDir = './' + options.inDirectory;
96124

97125
// Translation strings
@@ -241,8 +269,8 @@ function generateCategories(dataDir, tstrings) {
241269
return categories;
242270
}
243271

244-
245-
function generateFields(dataDir, tstrings, searchableFieldIDs) {
272+
/** @internal */
273+
export function generateFields(dataDir, tstrings, searchableFieldIDs) {
246274
let fields = {};
247275

248276
fs.globSync(dataDir + '/fields/**/*.json', {

lib/translations.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@ import fs from 'fs';
33
import fetch from 'node-fetch';
44
import YAML from 'js-yaml';
55
import { transifexApi } from '@transifex/api';
6+
import { getExternalTranslations } from './units.js';
67

78

9+
/** @typedef {{
10+
translOrgId: string;
11+
translProjectId: string;
12+
translResourceIds: string[];
13+
translReviewedOnly: false | string[];
14+
inDirectory: string;
15+
outDirectory: string;
16+
sourceLocale: string;
17+
}} TranslationOptions */
18+
19+
/** @param {Partial<TranslationOptions>} options */
820
function fetchTranslations(options) {
921

1022
// Transifex doesn't allow anonymous downloading
@@ -202,6 +214,8 @@ function fetchTranslations(options) {
202214
for (let code in allStrings) {
203215
let obj = {};
204216
obj[code] = allStrings[code] || {};
217+
Object.assign(obj[code], getExternalTranslations(code, options));
218+
205219
fs.writeFileSync(`${outDir}/${code}.json`, JSON.stringify(obj, null, 4));
206220
fs.writeFileSync(`${outDir}/${code}.min.json`, JSON.stringify(obj));
207221
}

lib/units.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// @ts-check
2+
import { createRequire } from 'node:module';
3+
import { generateFields, getDefaultOptions } from './build.js';
4+
5+
const require = createRequire(import.meta.url);
6+
7+
let cachedFields;
8+
9+
/**
10+
* @param {string} locale
11+
* @param {Partial<import('./translations.js').TranslationOptions>} options
12+
*/
13+
export function getExternalTranslations(locale, options) {
14+
options = getDefaultOptions(options);
15+
const language = locale.split('-')[0];
16+
17+
cachedFields ||= generateFields(options.inDirectory, { fields: {} }, {});
18+
19+
let localeData;
20+
let languageData;
21+
try {
22+
localeData = require(`cldr-units-full/main/${locale}/units.json`);
23+
} catch {
24+
// ignore
25+
}
26+
try {
27+
languageData = require(`cldr-units-full/main/${language}/units.json`);
28+
} catch {
29+
// ignore
30+
}
31+
32+
if (!localeData && !languageData) {
33+
// eslint-disable-next-line no-console
34+
console.warn(`No CLDR data for ${language}`);
35+
}
36+
37+
const output = {};
38+
39+
for (const field of Object.values(cachedFields)) {
40+
if (!field.measurement) continue;
41+
42+
const { dimension, units } = field.measurement;
43+
44+
for (const unit in units) {
45+
for (const type of ['long', 'narrow']) {
46+
const translation =
47+
localeData?.main[locale].units[type][`${dimension}-${unit}`]
48+
.displayName ||
49+
languageData?.main[language].units[type][`${dimension}-${unit}`]
50+
.displayName;
51+
52+
output[dimension] ||= {};
53+
output[dimension][unit] ||= {};
54+
output[dimension][unit][type] = translation;
55+
}
56+
}
57+
}
58+
59+
return { units: output };
60+
}

package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"exports": "./lib/index.js",
1414
"dependencies": {
1515
"@transifex/api": "^7.1.0",
16+
"cldr-core": "^47.0.0",
17+
"cldr-units-full": "^47.0.0",
1618
"js-yaml": "^4.0.0",
1719
"json-schema-to-typescript-lite": "^15.0.0",
1820
"jsonschema": "^1.1.0",
@@ -31,6 +33,7 @@
3133
"node": ">=22"
3234
},
3335
"scripts": {
36+
"build": "node scripts/build-schema.js",
3437
"lint": "eslint lib",
3538
"lint:fix": "eslint lib --fix",
3639
"test": "NODE_OPTIONS=--experimental-vm-modules jest schema-builder.test.js"

schemas/field.json

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "http://json-schema.org/draft-07/schema#",
3-
"$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/field.json",
3+
"$id": "https://cdn.jsdelivr.net/npm/@ideditor/schema-builder/schemas/field.json",
44
"title": "Field",
55
"description": "A reusable form element for presets",
66
"type": "object",
@@ -67,14 +67,13 @@
6767
"lanes",
6868
"localized",
6969
"manyCombo",
70+
"measurement",
7071
"multiCombo",
7172
"networkCombo",
7273
"number",
7374
"onewayCheck",
7475
"radio",
7576
"restrictions",
76-
"roadheight",
77-
"roadspeed",
7877
"schedule",
7978
"semiCombo",
8079
"structureRadio",
@@ -340,11 +339,11 @@
340339
},
341340
"additionalProperties": false
342341
},
343-
"urlFormat": {
342+
"urlFormat": {
344343
"description": "Permalink URL for `identifier` fields. Must contain a {value} placeholder",
345344
"type": "string"
346345
},
347-
"pattern": {
346+
"pattern": {
348347
"description": "Regular expression that a valid `identifier` value is expected to match",
349348
"type": "string"
350349
},
@@ -364,6 +363,32 @@
364363
"iconsCrossReference": {
365364
"description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
366365
"type": "string"
366+
},
367+
"measurement": {
368+
"type": "object",
369+
"description": "defines the units of measurement that this field uses. Only supported by the 'measurement' field type.",
370+
"properties": {
371+
"dimension": {
372+
"type": "string",
373+
"description": "The corresponding 'dimension' from CLDR",
374+
"$ref": "./generated/dimension.json"
375+
},
376+
"units": {
377+
"type": "object",
378+
"description": "Defines the permitted units. The key is the ID used by CLDR. The value is the value used in the OSM tag. If there are multiple values, the first one will be preferred. Use an empty string if the unit is not included in the OSM tag.",
379+
"additionalProperties": {
380+
"type": "array",
381+
"items": {
382+
"type": "string"
383+
},
384+
"minItems": 1
385+
},
386+
"minProperties": 1
387+
}
388+
},
389+
"allOf": [{ "$ref": "./generated/units.json" }],
390+
"additionalItems": false,
391+
"required": ["dimension", "units"]
367392
}
368393
},
369394
"additionalProperties": false,

0 commit comments

Comments
 (0)