From 2374124f0b9f758021648e8bd3d99c205b2e3aea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Oct 2025 13:07:21 -0300 Subject: language: compositional withStrings, update strings_htmlEscaped --- src/common-util/wiki-data.js | 2 + src/data/composite/things/language/index.js | 1 + src/data/composite/things/language/withStrings.js | 111 ++++++++++++++++++++++ src/data/things/language.js | 89 ++++++----------- 4 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 src/data/composite/things/language/index.js create mode 100644 src/data/composite/things/language/withStrings.js (limited to 'src') diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 3fde2495..6089b8fc 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -106,6 +106,8 @@ export const commentaryRegexCaseSensitive = export const commentaryRegexCaseSensitiveOneShot = new RegExp(commentaryRegexRaw); +export const languageOptionRegex = /{(?[A-Z0-9_]+)}/g; + // The #validators function isOldStyleLyrics() describes // what this regular expression detects against. export const multipleLyricsDetectionRegex = diff --git a/src/data/composite/things/language/index.js b/src/data/composite/things/language/index.js new file mode 100644 index 00000000..f22cdaf6 --- /dev/null +++ b/src/data/composite/things/language/index.js @@ -0,0 +1 @@ +export {default as withStrings} from './withStrings.js'; diff --git a/src/data/composite/things/language/withStrings.js b/src/data/composite/things/language/withStrings.js new file mode 100644 index 00000000..3b8d46b3 --- /dev/null +++ b/src/data/composite/things/language/withStrings.js @@ -0,0 +1,111 @@ +import {logWarn} from '#cli'; +import {input, templateCompositeFrom} from '#composite'; +import {empty, withEntries} from '#sugar'; +import {languageOptionRegex} from '#wiki-data'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withStrings`, + + inputs: { + from: input({defaultDependency: 'strings'}), + }, + + outputs: ['#strings'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('from'), + }).outputs({ + '#availability': '#stringsAvailability', + }), + + withResultOfAvailabilityCheck({ + from: 'inheritedStrings', + }).outputs({ + '#availability': '#inheritedStringsAvailability', + }), + + { + dependencies: [ + '#stringsAvailability', + '#inheritedStringsAvailability', + ], + + compute: (continuation, { + ['#stringsAvailability']: stringsAvailability, + ['#inheritedStringsAvailability']: inheritedStringsAvailability, + }) => + (stringsAvailability || inheritedStringsAvailability + ? continuation() + : continuation.raiseOutput({'#strings': null})), + }, + + { + dependencies: [input('from'), '#inheritedStringsAvailability'], + compute: (continuation, { + [input('from')]: strings, + ['#inheritedStringsAvailability']: inheritedStringsAvailability, + }) => + (inheritedStringsAvailability + ? continuation() + : continuation.raiseOutput({'#strings': strings})), + }, + + { + dependencies: ['inheritedStrings', '#stringsAvailability'], + compute: (continuation, { + ['inheritedStrings']: inheritedStrings, + ['#stringsAvailability']: stringsAvailability, + }) => + (stringsAvailability + ? continuation() + : continuation.raiseOutput({'#strings': inheritedStrings})), + }, + + { + dependencies: [input('from'), 'inheritedStrings', 'code'], + compute(continuation, { + [input('from')]: strings, + ['inheritedStrings']: inheritedStrings, + ['code']: code, + }) { + const validStrings = { + ...inheritedStrings, + ...strings, + }; + + const optionsFromTemplate = template => + Array.from(template.matchAll(languageOptionRegex)) + .map(({groups}) => groups.name); + + for (const [key, providedTemplate] of Object.entries(strings)) { + const inheritedTemplate = inheritedStrings[key]; + if (!inheritedTemplate) continue; + + const providedOptions = optionsFromTemplate(providedTemplate); + const inheritedOptions = optionsFromTemplate(inheritedTemplate); + + const missingOptionNames = + inheritedOptions.filter(name => !providedOptions.includes(name)); + + const misplacedOptionNames = + providedOptions.filter(name => !inheritedOptions.includes(name)); + + if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) { + logWarn`Not using ${code ?? '(no code)'} string ${key}:`; + if (!empty(missingOptionNames)) + logWarn`- Missing options: ${missingOptionNames.join(', ')}`; + if (!empty(misplacedOptionNames)) + logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`; + + validStrings[key] = inheritedStrings[key]; + } + } + + return continuation({'#strings': validStrings}); + }, + }, + ], +}); diff --git a/src/data/things/language.js b/src/data/things/language.js index 46cff26a..997cf31e 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -2,12 +2,12 @@ import {Temporal, toTemporalInstant} from '@js-temporal/polyfill'; import {withAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; -import {logWarn} from '#cli'; import {input} from '#composite'; import * as html from '#html'; -import {empty} from '#sugar'; +import {empty, withEntries} from '#sugar'; import {isLanguageCode} from '#validators'; import Thing from '#thing'; +import {languageOptionRegex} from '#wiki-data'; import { getExternalLinkStringOfStyleFromDescriptors, @@ -17,10 +17,11 @@ import { isExternalLinkStyle, } from '#external-links'; -import {exposeConstant} from '#composite/control-flow'; +import {exitWithoutDependency, exposeConstant, exposeDependency} + from '#composite/control-flow'; import {externalFunction, flag, name} from '#composite/wiki-properties'; -export const languageOptionRegex = /{(?[A-Z0-9_]+)}/g; +import {withStrings} from '#composite/things/language'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -62,52 +63,17 @@ export class Language extends Thing { // Mapping of translation keys to values (strings). Generally, don't // access this object directly - use methods instead. - strings: { - flags: {update: true, expose: true}, - update: {validate: (t) => typeof t === 'object'}, - - expose: { - dependencies: ['inheritedStrings', 'code'], - transform(strings, {inheritedStrings, code}) { - if (!strings && !inheritedStrings) return null; - if (!inheritedStrings) return strings; - - const validStrings = { - ...inheritedStrings, - ...strings, - }; - - const optionsFromTemplate = template => - Array.from(template.matchAll(languageOptionRegex)) - .map(({groups}) => groups.name); - - for (const [key, providedTemplate] of Object.entries(strings)) { - const inheritedTemplate = inheritedStrings[key]; - if (!inheritedTemplate) continue; - - const providedOptions = optionsFromTemplate(providedTemplate); - const inheritedOptions = optionsFromTemplate(inheritedTemplate); - - const missingOptionNames = - inheritedOptions.filter(name => !providedOptions.includes(name)); - - const misplacedOptionNames = - providedOptions.filter(name => !inheritedOptions.includes(name)); - - if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) { - logWarn`Not using ${code ?? '(no code)'} string ${key}:`; - if (!empty(missingOptionNames)) - logWarn`- Missing options: ${missingOptionNames.join(', ')}`; - if (!empty(misplacedOptionNames)) - logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`; - validStrings[key] = inheritedStrings[key]; - } - } + strings: [ + withStrings({ + from: input.updateValue({ + validate: t => typeof t === 'object', + }), + }), - return validStrings; - }, - }, - }, + exposeDependency({ + dependency: '#strings', + }), + ], // May be provided to specify "default" strings, generally (but not // necessarily) inherited from another Language object. @@ -163,19 +129,20 @@ export class Language extends Thing { }, // TODO: This currently isn't used. Is it still needed? - strings_htmlEscaped: { - flags: {expose: true}, - expose: { - dependencies: ['strings', 'inheritedStrings'], - compute({strings, inheritedStrings}) { - if (!(strings || inheritedStrings)) return null; - const allStrings = {...inheritedStrings, ...strings}; - return Object.fromEntries( - Object.entries(allStrings).map(([k, v]) => [k, html.escape(v)]) - ); - }, + strings_htmlEscaped: [ + withStrings(), + + exitWithoutDependency({ + dependency: '#strings', + }), + + { + dependencies: ['#strings'], + compute: ({'#strings': strings}) => + withEntries(strings, entries => entries + .map(([key, value]) => [key, html.escape(value)])), }, - }, + ], }); static #intlHelper (constructor, opts) { -- cgit 1.3.0-6-gf8a5