Skip to content

Commit 1a29aec

Browse files
committed
Implement a schema for the measurement field
1 parent 4cbf8d4 commit 1a29aec

File tree

9 files changed

+234
-6
lines changed

9 files changed

+234
-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
@@ -19,8 +19,25 @@ const defaultsSchema = require('../schemas/preset_defaults.json');
1919
const deprecatedSchema = require('../schemas/deprecated.json');
2020
const discardedSchema = require('../schemas/discarded.json');
2121

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

40+
/** @param {Options} options */
2441
function validateData(options) {
2542
const START = '🔬 ' + chalk.yellow('Validating schema...');
2643
const END = '👍 ' + chalk.green('schema okay');
@@ -35,6 +52,7 @@ function validateData(options) {
3552
process.stdout.write('\n');
3653
}
3754

55+
/** @param {Options} options */
3856
function buildDev(options) {
3957

4058
if (_currBuild) return _currBuild;
@@ -52,6 +70,7 @@ function buildDev(options) {
5270
process.stdout.write('\n');
5371
}
5472

73+
/** @param {Options} options */
5574
function buildDist(options) {
5675

5776
if (_currBuild) return _currBuild;
@@ -77,7 +96,8 @@ function buildDist(options) {
7796
});
7897
}
7998

80-
function processData(options, type) {
99+
/** @internal @param {Options} options @returns {Options} */
100+
export function getDefaultOptions(options) {
81101
if (!options) options = {};
82102
options = Object.assign({
83103
inDirectory: 'data',
@@ -90,7 +110,15 @@ function processData(options, type) {
90110
processPresets: null,
91111
listReusedIcons: false
92112
}, options);
113+
return options;
114+
}
93115

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

96124
// Translation strings
@@ -239,8 +267,8 @@ function generateCategories(dataDir, tstrings) {
239267
return categories;
240268
}
241269

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

246274
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
@@ -205,6 +217,8 @@ function fetchTranslations(options) {
205217
for (let code in allStrings) {
206218
let obj = {};
207219
obj[code] = allStrings[code] || {};
220+
Object.assign(obj[code], getExternalTranslations(code, options));
221+
208222
fs.writeFileSync(`${outDir}/${code}.json`, JSON.stringify(obj, null, 4));
209223
fs.writeFileSync(`${outDir}/${code}.min.json`, JSON.stringify(obj));
210224
}

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
@@ -14,6 +14,8 @@
1414
"dependencies": {
1515
"@transifex/api": "^7.1.0",
1616
"chalk": "^5.0.1",
17+
"cldr-core": "^47.0.0",
18+
"cldr-units-full": "^47.0.0",
1719
"glob": "^11.0.2",
1820
"js-yaml": "^4.0.0",
1921
"jsonschema": "^1.1.0",
@@ -29,6 +31,7 @@
2931
"node": ">=20"
3032
},
3133
"scripts": {
34+
"build": "node scripts/build-schema.js",
3235
"lint": "eslint lib",
3336
"lint:fix": "eslint lib --fix",
3437
"test": "NODE_OPTIONS=--experimental-vm-modules jest schema-builder.test.js"

schemas/field.json

Lines changed: 29 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",
@@ -63,6 +63,7 @@
6363
"lanes",
6464
"localized",
6565
"manyCombo",
66+
"measurement",
6667
"multiCombo",
6768
"networkCombo",
6869
"number",
@@ -262,11 +263,11 @@
262263
},
263264
"additionalProperties": false
264265
},
265-
"urlFormat": {
266+
"urlFormat": {
266267
"description": "Permalink URL for `identifier` fields. Must contain a {value} placeholder",
267268
"type": "string"
268269
},
269-
"pattern": {
270+
"pattern": {
270271
"description": "Regular expression that a valid `identifier` value is expected to match",
271272
"type": "string"
272273
},
@@ -282,6 +283,31 @@
282283
"iconsCrossReference": {
283284
"description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
284285
"type": "string"
286+
},
287+
"measurement": {
288+
"type": "object",
289+
"description": "defines the units of measurement that this field uses. Only supported by the 'measurement' field type.",
290+
"properties": {
291+
"dimension": {
292+
"type": "string",
293+
"description": "The corresponding 'dimension' from CLDR",
294+
"$ref": "./generated/dimension.json"
295+
},
296+
"usage": {
297+
"type": "string",
298+
"description": "The corresponding 'usage' from CLDR"
299+
},
300+
"units": {
301+
"type": "object",
302+
"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."
303+
}
304+
},
305+
"allOf": [
306+
{ "$ref": "./generated/usage.json" },
307+
{ "$ref": "./generated/units.json" }
308+
],
309+
"additionalItems": false,
310+
"required": ["dimension", "usage", "units"]
285311
}
286312
},
287313
"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)