From 0768953f9538f0bbd65835b0a4293e2ba438ce52 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 1 Nov 2023 09:00:55 -0300 Subject: data: tidy language loading code, add processLanguageSpec --- src/data/language.js | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 09466907..34de8779 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -5,35 +5,43 @@ import he from 'he'; import T from '#things'; -export async function processLanguageFile(file) { - const contents = await readFile(file, 'utf-8'); - const json = JSON.parse(contents); +export function processLanguageSpec(spec) { + const { + 'meta.languageCode': code, + 'meta.languageName': name, + + 'meta.languageIntlCode': intlCode = null, + 'meta.hidden': hidden = false, + + ...strings + } = spec; - const code = json['meta.languageCode']; if (!code) { throw new Error(`Missing language code (file: ${file})`); } - delete json['meta.languageCode']; - const intlCode = json['meta.languageIntlCode'] ?? null; - delete json['meta.languageIntlCode']; - - const name = json['meta.languageName']; if (!name) { throw new Error(`Missing language name (${code})`); } - delete json['meta.languageName']; - - const hidden = json['meta.hidden'] ?? false; - delete json['meta.hidden']; const language = new T.Language(); - language.code = code; - language.intlCode = intlCode; - language.name = name; - language.hidden = hidden; - language.escapeHTML = (string) => + + Object.assign(language, { + code, + intlCode, + name, + hidden, + strings, + }); + + language.escapeHTML = string => he.encode(string, {useNamedReferences: true}); - language.strings = json; + return language; } + +export async function processLanguageFile(file) { + const contents = await readFile(file, 'utf-8'); + const spec = JSON.parse(contents); + return processLanguageSpec(spec); +} -- cgit 1.3.0-6-gf8a5 From cc3a6e32b957c60aa29027fa575e4b3ca0c05c64 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 1 Nov 2023 09:12:23 -0300 Subject: data: more language loading refactoring --- src/data/language.js | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 34de8779..b71e55a2 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -1,10 +1,13 @@ import {readFile} from 'node:fs/promises'; -// It stands for "HTML Entities", apparently. Cursed. -import he from 'he'; +import chokidar from 'chokidar'; +import he from 'he'; // It stands for "HTML Entities", apparently. Cursed. +import {withAggregate} from '#sugar'; import T from '#things'; +const {Language} = T; + export function processLanguageSpec(spec) { const { 'meta.languageCode': code, @@ -16,23 +19,21 @@ export function processLanguageSpec(spec) { ...strings } = spec; - if (!code) { - throw new Error(`Missing language code (file: ${file})`); - } + withAggregate({message: `Errors validating language spec`}, ({push}) => { + if (!code) { + push(new Error(`Missing language code (file: ${file})`)); + } - if (!name) { - throw new Error(`Missing language name (${code})`); - } + if (!name) { + push(new Error(`Missing language name (${code})`)); + } + }); - const language = new T.Language(); + return {code, intlCode, name, hidden, strings}; +} - Object.assign(language, { - code, - intlCode, - name, - hidden, - strings, - }); +export function initializeLanguageObject() { + const language = new Language(); language.escapeHTML = string => he.encode(string, {useNamedReferences: true}); @@ -43,5 +44,10 @@ export function processLanguageSpec(spec) { export async function processLanguageFile(file) { const contents = await readFile(file, 'utf-8'); const spec = JSON.parse(contents); - return processLanguageSpec(spec); + + const language = initializeLanguageObject(); + const properties = processLanguageSpec(spec); + Object.assign(language, properties); + + return language; } -- cgit 1.3.0-6-gf8a5 From d497be7b5e1e4d9f9a8ca71de0a82def384467f8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 17:42:35 -0400 Subject: data: language: basic watchLanguageFile implementation --- src/data/language.js | 108 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 8 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index b71e55a2..ec38cbde 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -1,10 +1,19 @@ +import EventEmitter from 'node:events'; import {readFile} from 'node:fs/promises'; +import path from 'node:path'; import chokidar from 'chokidar'; import he from 'he'; // It stands for "HTML Entities", apparently. Cursed. -import {withAggregate} from '#sugar'; import T from '#things'; +import {colors, logWarn} from '#cli'; + +import { + annotateError, + annotateErrorWithFile, + showAggregate, + withAggregate, +} from '#sugar'; const {Language} = T; @@ -21,17 +30,43 @@ export function processLanguageSpec(spec) { withAggregate({message: `Errors validating language spec`}, ({push}) => { if (!code) { - push(new Error(`Missing language code (file: ${file})`)); + push(new Error(`Missing language code`)); } if (!name) { - push(new Error(`Missing language name (${code})`)); + push(new Error(`Missing language name`)); } }); return {code, intlCode, name, hidden, strings}; } +async function processLanguageSpecFromFile(file) { + let contents, spec; + + try { + contents = await readFile(file, 'utf-8'); + } catch (caughtError) { + throw annotateError( + new Error(`Failed to read language file`, {cause: caughtError}), + error => annotateErrorWithFile(error, file)); + } + + try { + spec = JSON.parse(contents); + } catch (caughtError) { + throw annotateError( + new Error(`Failed to parse language file as valid JSON`, {cause: caughtError}), + error => annotateErrorWithFile(error, file)); + } + + try { + return processLanguageSpec(spec); + } catch (caughtError) { + throw annotateErrorWithFile(caughtError, file); + } +} + export function initializeLanguageObject() { const language = new Language(); @@ -42,12 +77,69 @@ export function initializeLanguageObject() { } export async function processLanguageFile(file) { - const contents = await readFile(file, 'utf-8'); - const spec = JSON.parse(contents); + const language = initializeLanguageObject(); + const properties = await processLanguageSpecFromFile(file); + return Object.assign(language, properties); +} + +export function watchLanguageFile(file, { + logging = true, +} = {}) { + const basename = path.basename(file); + const events = new EventEmitter(); const language = initializeLanguageObject(); - const properties = processLanguageSpec(spec); - Object.assign(language, properties); - return language; + let emittedReady = false; + let successfullyAppliedLanguage = false; + + Object.assign(events, {language, close}); + + const watcher = chokidar.watch(file); + watcher.on('change', () => handleFileUpdated()); + + setImmediate(handleFileUpdated); + + return events; + + async function close() { + return watcher.close(); + } + + function checkReadyConditions() { + if (emittedReady) return; + if (!successfullyAppliedLanguage) return; + + events.emit('ready'); + emittedReady = true; + } + + async function handleFileUpdated() { + let properties; + + try { + properties = await processLanguageSpecFromFile(file); + } catch (error) { + if (logging) { + if (successfullyAppliedLanguage) { + logWarn`Failed to load language ${basename} - using existing version`; + } else { + logWarn`Failed to load language ${basename} - no prior version loaded`; + } + showAggregate(error, {showTraces: false}); + } + return; + } + + Object.assign(language, properties); + successfullyAppliedLanguage = true; + + if (logging && emittedReady) { + const timestamp = new Date().toLocaleString('en-US', {timeStyle: 'medium'}); + console.log(colors.green(`[${timestamp}] Updated language ${language.name} (${language.code})`)); + } + + events.emit('update'); + checkReadyConditions(); + } } -- cgit 1.3.0-6-gf8a5 From 06949e1d20d38d38eb05999ca236f2c7d150691e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 17:55:25 -0400 Subject: upd8: basic watchLanguageFile integration for internal language --- src/data/language.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index ec38cbde..5ab3936e 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -120,6 +120,8 @@ export function watchLanguageFile(file, { try { properties = await processLanguageSpecFromFile(file); } catch (error) { + events.emit('error', error); + if (logging) { if (successfullyAppliedLanguage) { logWarn`Failed to load language ${basename} - using existing version`; @@ -128,6 +130,7 @@ export function watchLanguageFile(file, { } showAggregate(error, {showTraces: false}); } + return; } -- cgit 1.3.0-6-gf8a5 From 9f3a1f476752059681fbe21f8a1f7bf11dd73c9b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 18:43:49 -0400 Subject: data: language: nicer language labelling for successive errors --- src/data/language.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 5ab3936e..99eaa58f 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -17,7 +17,7 @@ import { const {Language} = T; -export function processLanguageSpec(spec) { +export function processLanguageSpec(spec, {existingCode = null}) { const { 'meta.languageCode': code, 'meta.languageName': name, @@ -36,12 +36,16 @@ export function processLanguageSpec(spec) { if (!name) { push(new Error(`Missing language name`)); } + + if (code && existingCode && code !== existingCode) { + push(new Error(`Language code (${code}) doesn't match previous value\n(You'll have to reload hsmusic to load this)`)); + } }); return {code, intlCode, name, hidden, strings}; } -async function processLanguageSpecFromFile(file) { +async function processLanguageSpecFromFile(file, processLanguageSpecOpts) { let contents, spec; try { @@ -61,7 +65,7 @@ async function processLanguageSpecFromFile(file) { } try { - return processLanguageSpec(spec); + return processLanguageSpec(spec, processLanguageSpecOpts); } catch (caughtError) { throw annotateErrorWithFile(caughtError, file); } @@ -118,15 +122,25 @@ export function watchLanguageFile(file, { let properties; try { - properties = await processLanguageSpecFromFile(file); + properties = await processLanguageSpecFromFile(file, { + existingCode: + (successfullyAppliedLanguage + ? language.code + : null), + }); } catch (error) { events.emit('error', error); if (logging) { + const label = + (successfullyAppliedLanguage + ? `${language.name} (${language.code})` + : basename); + if (successfullyAppliedLanguage) { - logWarn`Failed to load language ${basename} - using existing version`; + logWarn`Failed to load language ${label} - using existing version`; } else { - logWarn`Failed to load language ${basename} - no prior version loaded`; + logWarn`Failed to load language ${label} - no prior version loaded`; } showAggregate(error, {showTraces: false}); } -- cgit 1.3.0-6-gf8a5 From bd3affb31b6b2e5cb0667c550bcdbde8af51a392 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 19:06:27 -0400 Subject: data: language: basic support for loading language from YAML --- src/data/language.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 99eaa58f..aed16057 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -4,6 +4,7 @@ import path from 'node:path'; import chokidar from 'chokidar'; import he from 'he'; // It stands for "HTML Entities", apparently. Cursed. +import yaml from 'js-yaml'; import T from '#things'; import {colors, logWarn} from '#cli'; @@ -56,11 +57,18 @@ async function processLanguageSpecFromFile(file, processLanguageSpecOpts) { error => annotateErrorWithFile(error, file)); } + let parseLanguage; try { - spec = JSON.parse(contents); + if (path.extname(file) === '.yaml') { + parseLanguage = 'YAML'; + spec = yaml.load(contents); + } else { + parseLanguage = 'JSON'; + spec = JSON.parse(contents); + } } catch (caughtError) { throw annotateError( - new Error(`Failed to parse language file as valid JSON`, {cause: caughtError}), + new Error(`Failed to parse language file as valid ${parseLanguage}`, {cause: caughtError}), error => annotateErrorWithFile(error, file)); } -- cgit 1.3.0-6-gf8a5 From 8d24f17f729c7da550824ab4134b89757754fb9c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 20:08:15 -0400 Subject: data: language: flatten language spec, allow for structuring --- src/data/language.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index aed16057..99efc03d 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -46,8 +46,24 @@ export function processLanguageSpec(spec, {existingCode = null}) { return {code, intlCode, name, hidden, strings}; } +function flattenLanguageSpec(spec) { + const recursive = (keyPath, value) => + (typeof value === 'object' + ? Object.assign({}, ... + Object.entries(value) + .map(([key, value]) => + (key === '_' + ? {[keyPath]: value} + : recursive( + (keyPath ? `${keyPath}.${key}` : key), + value)))) + : {[keyPath]: value}); + + return recursive('', spec); +} + async function processLanguageSpecFromFile(file, processLanguageSpecOpts) { - let contents, spec; + let contents; try { contents = await readFile(file, 'utf-8'); @@ -57,14 +73,16 @@ async function processLanguageSpecFromFile(file, processLanguageSpecOpts) { error => annotateErrorWithFile(error, file)); } + let rawSpec; let parseLanguage; + try { if (path.extname(file) === '.yaml') { parseLanguage = 'YAML'; - spec = yaml.load(contents); + rawSpec = yaml.load(contents); } else { parseLanguage = 'JSON'; - spec = JSON.parse(contents); + rawSpec = JSON.parse(contents); } } catch (caughtError) { throw annotateError( @@ -72,8 +90,10 @@ async function processLanguageSpecFromFile(file, processLanguageSpecOpts) { error => annotateErrorWithFile(error, file)); } + const flattenedSpec = flattenLanguageSpec(rawSpec); + try { - return processLanguageSpec(spec, processLanguageSpecOpts); + return processLanguageSpec(flattenedSpec, processLanguageSpecOpts); } catch (caughtError) { throw annotateErrorWithFile(caughtError, file); } -- cgit 1.3.0-6-gf8a5 From 7fa4f92c8a41754e198ade96a7d5d0dd5b0aa59e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 7 Nov 2023 09:12:47 -0400 Subject: upd8: add --no-language-reloading option, default for static-build --- src/data/language.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 99eaa58f..15c11933 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -17,7 +17,7 @@ import { const {Language} = T; -export function processLanguageSpec(spec, {existingCode = null}) { +export function processLanguageSpec(spec, {existingCode = null} = {}) { const { 'meta.languageCode': code, 'meta.languageName': name, -- cgit 1.3.0-6-gf8a5 From 1d991bb4bc877363532971a74f70e55939c637bb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 9 Nov 2023 20:16:30 -0400 Subject: upd8, data, test: export internal strings path cleanly, fix tests --- src/data/language.js | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src/data/language.js') diff --git a/src/data/language.js b/src/data/language.js index 6ffc31e0..3fc14da7 100644 --- a/src/data/language.js +++ b/src/data/language.js @@ -1,6 +1,7 @@ import EventEmitter from 'node:events'; import {readFile} from 'node:fs/promises'; import path from 'node:path'; +import {fileURLToPath} from 'node:url'; import chokidar from 'chokidar'; import he from 'he'; // It stands for "HTML Entities", apparently. Cursed. @@ -18,6 +19,14 @@ import { const {Language} = T; +export const DEFAULT_STRINGS_FILE = 'strings-default.yaml'; + +export const internalDefaultStringsFile = + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../', + DEFAULT_STRINGS_FILE); + export function processLanguageSpec(spec, {existingCode = null} = {}) { const { 'meta.languageCode': code, -- cgit 1.3.0-6-gf8a5