Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build
- run: npm run lint
- run: npm run test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ npm-debug.log
/tests/workspace

transifex.auth
schemas/generated
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

[#x]: https://github.com/ideditor/schema-builder/issues/x
-->
# Unreleased

* :warning: Add _measurement_ field type ([#198], thanks [@k-yle])
* Data consumers who don't support `type=measurement` should treat it as a `type=combo` if `autoSuggestions=true`, otherwise treat it as `type=text`.

[#198]: https://github.com/ideditor/schema-builder/pull/198

# 6.5.1
##### 2024-Mar-14
Expand Down
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ data/
defaults.json
deprecated.json
discarded.json
units.json
```

The format for each file is defined in the [`schemas`](schemas) directory.
Expand Down Expand Up @@ -473,8 +474,7 @@ A string specifying the UI and behavior of the field. Must be one of the followi

* `access` - Block of dropdowns for defining the `access=*` tags on a highway
* `address` - Block of text and dropdown fields for entering address information (localized for editing location)
* `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
* `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
* `measurement` - Numeric text field with associated unit of measurement, such as inches or kilometers-per-hour. The field may have multiple units. See [#measurement](#measurement) for details.
* `restrictions` - Graphical field for editing turn restrictions
* `wikidata` - Search field for selecting a Wikidata entity
* `wikipedia` - Block of fields for selecting a wiki language and Wikipedia page
Expand Down Expand Up @@ -740,6 +740,38 @@ Combo field types can accept key-label pairs in the `options` value of the `stri

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.

##### `measurement`

Used when `type = measurement`. Defines the unit of measurements that are supported by this field. For example:

```json
{
"key": "diameter",
"type": "measurement",
"measurement": {
// The dimension being measured. This constrains the permitted units.
// The ID id defined by CLDR.
"dimension": "length",

// The corresponding 'usage' from CLDR.
"usage": "default",

// If the field only allows some units, you can list them here
// using CLDR's unit names. If not specified, then, all units from
// this dimension are allowed.
"units": ["meter", "centimeter", "foot-and-inch"],

// Some OSM tags have a default unit, which does not need to be explicitly included in the tag value.
// This field defines how to interpret a unit-less value:
"impliedUnit": "meter"
}
}
```

To convert the unit IDs into the values used by OSM, see [§Units](#Units).

Translations for the [`narrow` and `long` form](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) of each unit are bundled into iD-tagging-schema's locale files.

### Deprecations

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.
Expand Down Expand Up @@ -774,6 +806,26 @@ To update a specific tag to a specific new tag
},
```

## Units

The [`units.json` file](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/units.json) defines the suffix which is used in the OSM tag value for every unit of measurement.

```jsonc
{
"power": {
// The key defines the ID of the of the unit, as defined by CLDR.
// The values define the suffix used in the OSM tag value.
"megawatt": ["MW"],

// If there are multiple values in the array, then the first one
// is the preferred value, but iD will still recognise the alternative/s.
"kilowatt": ["kW", "KW"],

// `horsepower` is not included, therefore it can't be used by any fields.
},
}
```

## Contributing

iD's [code of conduct](https://github.com/openstreetmap/iD/blob/release/CODE_OF_CONDUCT.md) and
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default [
'no-await-in-loop': 'error',
'no-caller': 'error',
'no-catch-shadow': 'error',
'no-console': 'warn',
'no-console': 'off',
'no-constructor-return': 'error',
'no-div-regex': 'error',
'no-duplicate-imports': 'warn',
Expand Down
42 changes: 39 additions & 3 deletions lib/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,27 @@ const categorySchema = require('../schemas/preset_category.json');
const defaultsSchema = require('../schemas/preset_defaults.json');
const deprecatedSchema = require('../schemas/deprecated.json');
const discardedSchema = require('../schemas/discarded.json');
const unitsSchema = require('../schemas/generated/units.json');

/** @import { TranslationOptions } from "./translations.js" */

/** @typedef {{
inDirectory: string;
interimDirectory: string;
outDirectory: string;
sourceLocale: string;
taginfoProjectInfo: unknown,
processCategories: null | unknown;
processFields: null | unknown;
processPresets: null | unknown;
listReusedIcons: boolean;
}} BuildOptions */

/** @typedef {Partial<BuildOptions & TranslationOptions>} Options */

let _currBuild = null;

/** @param {Options} options */
function validateData(options) {
const START = '🔬 ' + styleText('yellow', 'Validating schema...');
const END = '👍 ' + styleText('green', 'schema okay');
Expand All @@ -36,6 +54,7 @@ function validateData(options) {
process.stdout.write('\n');
}

/** @param {Options} options */
function buildDev(options) {

if (_currBuild) return _currBuild;
Expand All @@ -53,6 +72,7 @@ function buildDev(options) {
process.stdout.write('\n');
}

/** @param {Options} options */
function buildDist(options) {

if (_currBuild) return _currBuild;
Expand All @@ -78,7 +98,8 @@ function buildDist(options) {
});
}

function processData(options, type) {
/** @internal @param {Options} options @returns {Options} */
export function getDefaultOptions(options) {
if (!options) options = {};
options = Object.assign({
inDirectory: 'data',
Expand All @@ -91,7 +112,15 @@ function processData(options, type) {
processPresets: null,
listReusedIcons: false
}, options);
return options;
}

/**
* @param {Options} options
* @param {'build-interim' | 'build-dist' | 'validate'} type
*/
function processData(options, type) {
options = getDefaultOptions(options);
const dataDir = './' + options.inDirectory;

// Translation strings
Expand All @@ -114,6 +143,11 @@ function processData(options, type) {
validateSchema(dataDir + '/discarded.json', discarded, discardedSchema);
}

const units = read(dataDir + '/units.json');
if (units) {
validateSchema(dataDir + '/units.json', units, unitsSchema);
}

let categories = generateCategories(dataDir, tstrings);
if (options.processCategories) options.processCategories(categories);

Expand Down Expand Up @@ -176,6 +210,7 @@ function processData(options, type) {
if (defaults) fs.writeFileSync(distDir + '/preset_defaults.json', JSON.stringify(defaults, null, 4));
if (deprecated) fs.writeFileSync(distDir + '/deprecated.json', JSON.stringify(deprecated, null, 4));
if (discarded) fs.writeFileSync(distDir + '/discarded.json', JSON.stringify(discarded, null, 4));
if (units) fs.writeFileSync(distDir + '/units.json', JSON.stringify(units, null, 4));

expandTStrings(tstrings);
let translationsForJson = {};
Expand All @@ -193,6 +228,7 @@ function processData(options, type) {
minifyJSON(distDir + '/preset_defaults.json', distDir + '/preset_defaults.min.json'),
minifyJSON(distDir + '/deprecated.json', distDir + '/deprecated.min.json'),
minifyJSON(distDir + '/discarded.json', distDir + '/discarded.min.json'),
minifyJSON(distDir + '/units.json', distDir + '/units.min.json'),
minifyJSON(distDir + '/translations/' + sourceLocale + '.json', distDir + '/translations/' + sourceLocale + '.min.json'),
generateTypeDefs(distDir),
];
Expand Down Expand Up @@ -246,8 +282,8 @@ function generateCategories(dataDir, tstrings) {
return categories;
}


function generateFields(dataDir, tstrings, searchableFieldIDs) {
/** @internal */
export function generateFields(dataDir, tstrings, searchableFieldIDs) {
let fields = {};

fs.globSync(dataDir + '/fields/**/*.json', {
Expand Down
14 changes: 14 additions & 0 deletions lib/translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'fs';
import fetch from 'node-fetch';
import YAML from 'js-yaml';
import { transifexApi } from '@transifex/api';
import { getExternalTranslations } from './units.js';

export function expandTStrings(tstrings) {
const presets = tstrings.presets || {};
Expand Down Expand Up @@ -82,6 +83,17 @@ export function sortObject(original) {
return sorted;
}

/** @typedef {{
translOrgId: string;
translProjectId: string;
translResourceIds: string[];
translReviewedOnly: false | string[];
inDirectory: string;
outDirectory: string;
sourceLocale: string;
}} TranslationOptions */

/** @param {Partial<TranslationOptions>} options */
function fetchTranslations(options) {

// Transifex doesn't allow anonymous downloading
Expand Down Expand Up @@ -217,6 +229,8 @@ function fetchTranslations(options) {
for (let code in allStrings) {
let obj = {};
obj[code] = allStrings[code] || {};
Object.assign(obj[code], getExternalTranslations(code, options));

fs.writeFileSync(`${outDir}/${code}.json`, JSON.stringify(obj, null, 4));
fs.writeFileSync(`${outDir}/${code}.min.json`, JSON.stringify(obj));
}
Expand Down
59 changes: 59 additions & 0 deletions lib/units.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @ts-check
import { createRequire } from 'node:module';
import { generateFields, getDefaultOptions } from './build.js';

const require = createRequire(import.meta.url);

let cachedFields;

/**
* @param {string} locale
* @param {Partial<import('./translations.js').TranslationOptions>} options
*/
export function getExternalTranslations(locale, options) {
options = getDefaultOptions(options);
const language = locale.split('-')[0];

cachedFields ||= generateFields(options.inDirectory, { fields: {} }, {});

let localeData;
let languageData;
try {
localeData = require(`cldr-units-full/main/${locale}/units.json`);
Comment thread
k-yle marked this conversation as resolved.
} catch {
// ignore
}
try {
languageData = require(`cldr-units-full/main/${language}/units.json`);
} catch {
// ignore
}

if (!localeData && !languageData) {
console.warn(`No CLDR data for ${language}`);
}

const output = {};

for (const field of Object.values(cachedFields)) {
if (!field.measurement) continue;

const { dimension, units } = field.measurement;

for (const unit in units) {
for (const type of ['long', 'narrow']) {
const translation =
localeData?.main[locale].units[type][`${dimension}-${unit}`]
.displayName ||
languageData?.main[language].units[type][`${dimension}-${unit}`]
.displayName;

output[dimension] ||= {};
output[dimension][unit] ||= {};
output[dimension][unit][type] = translation;
}
}
}

return { units: output };
}
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"exports": "./lib/index.js",
"dependencies": {
"@transifex/api": "^7.1.0",
"cldr-core": "^47.0.0",
"cldr-units-full": "^47.0.0",
"js-yaml": "^4.0.0",
"json-schema-to-typescript-lite": "^15.0.0",
"jsonschema": "^1.1.0",
Expand All @@ -31,6 +33,7 @@
"node": ">=22"
},
"scripts": {
"build": "node scripts/build-schema.js",
"lint": "eslint lib",
"lint:fix": "eslint lib --fix",
"test": "NODE_OPTIONS=--experimental-vm-modules jest schema-builder.test.js"
Expand Down
Loading