Skip to content

Commit 144c068

Browse files
committed
Implement a schema for the measurement field
1 parent 62b6950 commit 144c068

File tree

9 files changed

+242
-6
lines changed

9 files changed

+242
-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
@@ -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: 37 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",
@@ -340,11 +341,11 @@
340341
},
341342
"additionalProperties": false
342343
},
343-
"urlFormat": {
344+
"urlFormat": {
344345
"description": "Permalink URL for `identifier` fields. Must contain a {value} placeholder",
345346
"type": "string"
346347
},
347-
"pattern": {
348+
"pattern": {
348349
"description": "Regular expression that a valid `identifier` value is expected to match",
349350
"type": "string"
350351
},
@@ -364,6 +365,39 @@
364365
"iconsCrossReference": {
365366
"description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
366367
"type": "string"
368+
},
369+
"measurement": {
370+
"type": "object",
371+
"description": "defines the units of measurement that this field uses. Only supported by the 'measurement' field type.",
372+
"properties": {
373+
"dimension": {
374+
"type": "string",
375+
"description": "The corresponding 'dimension' from CLDR",
376+
"$ref": "./generated/dimension.json"
377+
},
378+
"usage": {
379+
"type": "string",
380+
"description": "The corresponding 'usage' from CLDR"
381+
},
382+
"units": {
383+
"type": "object",
384+
"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.",
385+
"additionalProperties": {
386+
"type": "array",
387+
"items": {
388+
"type": "string"
389+
},
390+
"minItems": 1
391+
},
392+
"minProperties": 1
393+
}
394+
},
395+
"allOf": [
396+
{ "$ref": "./generated/usage.json" },
397+
{ "$ref": "./generated/units.json" }
398+
],
399+
"additionalItems": false,
400+
"required": ["dimension", "usage", "units"]
367401
}
368402
},
369403
"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)