Skip to content

Commit 7fa4f17

Browse files
committed
Implement a schema for the measurement field
1 parent f15c5a2 commit 7fa4f17

File tree

9 files changed

+238
-6
lines changed

9 files changed

+238
-6
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

lib/build.js

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

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

39+
/** @param {Options} options */
2340
function validateData(options) {
2441
const START = '🔬 ' + styleText('yellow', 'Validating schema...');
2542
const END = '👍 ' + styleText('green', 'schema okay');
@@ -34,6 +51,7 @@ function validateData(options) {
3451
process.stdout.write('\n');
3552
}
3653

54+
/** @param {Options} options */
3755
function buildDev(options) {
3856

3957
if (_currBuild) return _currBuild;
@@ -51,6 +69,7 @@ function buildDev(options) {
5169
process.stdout.write('\n');
5270
}
5371

72+
/** @param {Options} options */
5473
function buildDist(options) {
5574

5675
if (_currBuild) return _currBuild;
@@ -76,7 +95,8 @@ function buildDist(options) {
7695
});
7796
}
7897

79-
function processData(options, type) {
98+
/** @internal @param {Options} options @returns {Options} */
99+
export function getDefaultOptions(options) {
80100
if (!options) options = {};
81101
options = Object.assign({
82102
inDirectory: 'data',
@@ -89,7 +109,15 @@ function processData(options, type) {
89109
processPresets: null,
90110
listReusedIcons: false
91111
}, options);
112+
return options;
113+
}
92114

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

95123
// Translation strings
@@ -238,8 +266,8 @@ function generateCategories(dataDir, tstrings) {
238266
return categories;
239267
}
240268

241-
242-
function generateFields(dataDir, tstrings, searchableFieldIDs) {
269+
/** @internal */
270+
export function generateFields(dataDir, tstrings, searchableFieldIDs) {
243271
let fields = {};
244272

245273
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
"jsonschema": "^1.1.0",
1820
"marky": "^1.2.4",
@@ -28,6 +30,7 @@
2830
"node": ">=22"
2931
},
3032
"scripts": {
33+
"build": "node scripts/build-schema.js",
3134
"lint": "eslint lib",
3235
"lint:fix": "eslint lib --fix",
3336
"test": "NODE_OPTIONS=--experimental-vm-modules jest schema-builder.test.js"

schemas/field.json

Lines changed: 33 additions & 3 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,6 +67,7 @@
6767
"lanes",
6868
"localized",
6969
"manyCombo",
70+
"measurement",
7071
"multiCombo",
7172
"networkCombo",
7273
"number",
@@ -334,11 +335,11 @@
334335
},
335336
"additionalProperties": false
336337
},
337-
"urlFormat": {
338+
"urlFormat": {
338339
"description": "Permalink URL for `identifier` fields. Must contain a {value} placeholder",
339340
"type": "string"
340341
},
341-
"pattern": {
342+
"pattern": {
342343
"description": "Regular expression that a valid `identifier` value is expected to match",
343344
"type": "string"
344345
},
@@ -358,6 +359,35 @@
358359
"iconsCrossReference": {
359360
"description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
360361
"type": "string"
362+
},
363+
"measurement": {
364+
"type": "object",
365+
"description": "defines the units of measurement that this field uses. Only supported by the 'measurement' field type.",
366+
"properties": {
367+
"dimension": {
368+
"type": "string",
369+
"description": "The corresponding 'dimension' from CLDR",
370+
"$ref": "./generated/dimension.json"
371+
},
372+
"usage": {
373+
"type": "string",
374+
"description": "The corresponding 'usage' from CLDR"
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": "string"
381+
},
382+
"minProperties": 1
383+
}
384+
},
385+
"allOf": [
386+
{ "$ref": "./generated/usage.json" },
387+
{ "$ref": "./generated/units.json" }
388+
],
389+
"additionalItems": false,
390+
"required": ["dimension", "usage", "units"]
361391
}
362392
},
363393
"additionalProperties": false,

scripts/build-schema.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// @ts-check
2+
import { promises as fs } from 'node:fs';
3+
import { join } from 'node:path';
4+
import unitPreference from 'cldr-core/supplemental/unitPreferenceData.json' with { type: 'json' };
5+
import unitTranslations from 'cldr-units-full/main/en/units.json' with { type: 'json' };
6+
7+
// this file auto-generates the files in schemas/generated/* based on npm dependencies
8+
9+
const dimension = {
10+
$schema: 'http://json-schema.org/draft-07/schema#',
11+
$id: 'https://cdn.jsdelivr.net/npm/@ideditor/schema-builder/schemas/generated/dimension.json',
12+
13+
enum: Object.keys(unitPreference.supplemental.unitPreferenceData),
14+
};
15+
16+
const usage = {
17+
$schema: 'http://json-schema.org/draft-07/schema#',
18+
$id: 'https://cdn.jsdelivr.net/npm/@ideditor/schema-builder/schemas/generated/usage.json',
19+
20+
allOf: Object.entries(unitPreference.supplemental.unitPreferenceData).map(
21+
([key, value]) => ({
22+
if: { properties: { dimension: { const: key } } },
23+
then: { properties: { usage: { enum: Object.keys(value) } } },
24+
}),
25+
),
26+
};
27+
28+
const units = {
29+
$schema: 'http://json-schema.org/draft-07/schema#',
30+
$id: 'https://cdn.jsdelivr.net/npm/@ideditor/schema-builder/schemas/generated/units.json',
31+
32+
allOf: dimension.enum.map((dimension) => {
33+
const values = Object.keys(unitTranslations.main.en.units.long)
34+
.filter((key) => key.startsWith(`${dimension}-`))
35+
.map((key) => key.split('-').slice(1).join('-'));
36+
37+
return {
38+
if: { properties: { dimension: { const: dimension } } },
39+
then: {
40+
properties: {
41+
units: {
42+
additionalProperties: false,
43+
properties: Object.fromEntries(
44+
values.map((value) => [
45+
value,
46+
{ type: 'array', items: { type: 'string' }, minItems: 1 },
47+
]),
48+
),
49+
},
50+
},
51+
},
52+
};
53+
}),
54+
};
55+
56+
const files = { dimension, usage, units };
57+
58+
const generatedFolder = join(import.meta.dirname, '../schemas/generated');
59+
await fs.mkdir(generatedFolder, { recursive: true });
60+
61+
for (const key in files) {
62+
// eslint-disable-next-line no-await-in-loop
63+
await fs.writeFile(
64+
join(generatedFolder, `${key}.json`),
65+
JSON.stringify(files[key], null, 4),
66+
);
67+
}

0 commit comments

Comments
 (0)