From 6bcb224a35eb8b6e6c04b007c3faf168996779a4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 10 May 2022 20:01:38 -0300 Subject: WIP basic Language object Also BuildDirective, which isn't used yet. --- src/data/things.js | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/data/yaml.js | 10 +++ src/util/strings.js | 63 +---------------- 3 files changed, 210 insertions(+), 62 deletions(-) diff --git a/src/data/things.js b/src/data/things.js index c02e07e..a37ede7 100644 --- a/src/data/things.js +++ b/src/data/things.js @@ -87,6 +87,12 @@ export class FlashAct extends CacheableObject {} // -> WikiInfo export class WikiInfo extends CacheableObject {} +// -> Language +export class Language extends CacheableObject {} + +// -> BuildDirective +export class BuildDirective extends CacheableObject {} + // Before initializing property descriptors, set additional independent // constants on the classes (which are referenced later). @@ -1363,3 +1369,196 @@ WikiInfo.propertyDescriptors = { enableArtTagUI: Thing.common.flag(false), enableGroupUI: Thing.common.flag(false), }; + +// -> Language + +const intlHelper = (constructor, opts) => ({ + flags: {expose: true}, + expose: { + dependencies: ['code', 'intlCode'], + compute: ({ code, intlCode }) => { + const constructCode = intlCode ?? code; + if (!constructCode) return null; + return Reflect.construct(constructor, [constructCode, opts]); + } + } +}); + +Language.propertyDescriptors = { + // Update & expose + + // General language code. This is used to identify the language distinctly + // from other languages (similar to how "Directory" operates in many data + // objects). + code: { + flags: {update: true, expose: true}, + update: {validate: isLanguageCode} + }, + + // Language code specific to JavaScript's Internationalization (Intl) API. + // Usually this will be the same as the language's general code, but it + // may be overridden to provide Intl constructors an alternative value. + intlCode: { + flags: {update: true, expose: true}, + update: {validate: isLanguageCode}, + expose: { + dependencies: ['code'], + transform: (intlCode, { code }) => intlCode ?? code + } + }, + + // 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 only + + intl_date: intlHelper(Intl.DateTimeFormat, {full: true}), + intl_number: intlHelper(Intl.NumberFormat), + intl_listConjunction: intlHelper(Intl.ListFormat, {type: 'conjunction'}), + intl_listDisjunction: intlHelper(Intl.ListFormat, {type: 'disjunction'}), + intl_listUnit: intlHelper(Intl.ListFormat, {type: 'unit'}), + intl_pluralCardinal: intlHelper(Intl.PluralRules, {type: 'cardinal'}), + intl_pluralOrdinal: intlHelper(Intl.PluralRules, {type: 'ordinal'}), + + validKeys: { + flags: {expose: true}, + + expose: { + dependencies: ['strings'], + compute: ({ strings }) => strings ? Object.keys(strings) : [] + } + }, +}; + +const countHelper = (stringKey, argName = stringKey) => function(value, {unit = false} = {}) { + return this.$( + (unit + ? `count.${stringKey}.withUnit.` + this.getUnitForm(value) + : `count.${stringKey}`), + {[argName]: this.formatNumber(value)}); +}; + +Object.assign(Language.prototype, { + $(key, args = {}) { + if (!this.validKeys.includes(key)) { + throw new Error(`Invalid key ${key} accessed`); + } + + const template = this.strings[key]; + + // Convert the keys on the args dict from camelCase to CONSTANT_CASE. + // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut + // like, who cares, dude?) Also, this is an array, 8ecause it's handy + // for the iterating we're a8out to do. + const processedArgs = Object.entries(args) + .map(([ k, v ]) => [k.replace(/[A-Z]/g, '_$&').toUpperCase(), v]); + + // Replacement time! Woot. Reduce comes in handy here! + const output = processedArgs.reduce( + (x, [ k, v ]) => x.replaceAll(`{${k}}`, v), + template); + + // Post-processing: if any expected arguments *weren't* replaced, that + // is almost definitely an error. + if (output.match(/\{[A-Z_]+\}/)) { + throw new Error(`Args in ${key} were missing - output: ${output}`); + } + + return output; + }, + + assertIntlAvailable(property) { + if (!this[property]) { + throw new Error(`Intl API ${property} unavailable`); + } + }, + + getUnitForm(value) { + this.assertIntlAvailable('intl_pluralCardinal'); + return this.intl_pluralCardinal.select(value); + }, + + formatDate(date) { + this.assertIntlAvailable('intl_date'); + return this.intl_date.format(date); + }, + + formatDateRange(startDate, endDate) { + this.assertIntlAvailable('intl_date'); + return this.intl_date.formatRange(startDate, endDate); + }, + + formatDuration(secTotal, {approximate = false, unit = false}) { + if (secTotal === 0) { + return strings('count.duration.missing'); + } + + const hour = Math.floor(secTotal / 3600); + const min = Math.floor((secTotal - hour * 3600) / 60); + const sec = Math.floor(secTotal - hour * 3600 - min * 60); + + const pad = val => val.toString().padStart(2, '0'); + + const stringSubkey = unit ? '.withUnit' : ''; + + const duration = (hour > 0 + ? this.$('count.duration.hours' + stringSubkey, { + hours: hour, + minutes: pad(min), + seconds: pad(sec) + }) + : this.$('count.duration.minutes' + stringSubkey, { + minutes: min, + seconds: pad(sec) + })); + + return (approximate + ? this.$('count.duration.approximate', {duration}) + : duration); + }, + + formatIndex(value) { + this.assertIntlAvailable('intl_pluralOrdinal'); + return this.$('count.index.' + this.intl_pluralOrdinal.select(value), {index: value}); + }, + + formatNumber(value) { + this.assertIntlAvailable('intl_number'); + return this.intl_number.format(value); + }, + + formatWordCount(value) { + const num = this.formatNumber(value > 1000 + ? Math.floor(value / 100) / 10 + : value); + + const words = (value > 1000 + ? strings('count.words.thousand', {words: num}) + : strings('count.words', {words: num})); + + return this.$('count.words.withUnit.' + this.getUnitForm(value), {words}); + }, + + // TODO: These are hard-coded. Is there a better way? + countAlbums: countHelper('albums'), + countCommentaryEntries: countHelper('commentaryEntries', 'entries'), + countContributions: countHelper('contributions'), + countCoverArts: countHelper('coverArts'), + countTimesReferenced: countHelper('timesReferenced'), + countTimesUsed: countHelper('timesUsed'), + countTracks: countHelper('tracks'), +}); + +// -> BuildDirective + +BuildDirective.propertyDescriptors = { + // Update & expose + + directive: Thing.common.directory(), + baseDirectory: Thing.common.directory(), + language: Thing.common.simpleString(), +}; diff --git a/src/data/yaml.js b/src/data/yaml.js index 4897d57..7e2d824 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -11,6 +11,7 @@ import { Album, Artist, ArtTag, + BuildDirective, Flash, FlashAct, Group, @@ -55,6 +56,7 @@ function inspect(value) { // --> YAML data repository structure constants export const WIKI_INFO_FILE = 'wiki-info.yaml'; +export const BUILD_DIRECTIVE_DATA_FILE = 'build-directives.yaml'; export const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml'; export const ARTIST_DATA_FILE = 'artists.yaml'; export const FLASH_DATA_FILE = 'flashes.yaml'; @@ -398,6 +400,14 @@ export const processWikiInfoDocument = makeProcessDocument(WikiInfo, { } }); +export const processBuildDirectiveDocument = makeProcessDocument(BuildDirective, { + propertyFieldMapping: { + directive: 'Directive', + baseDirectory: 'Base Directory', + language: 'Language', + } +}); + export const processHomepageLayoutDocument = makeProcessDocument(HomepageLayout, { propertyFieldMapping: { sidebarContent: 'Sidebar Content' diff --git a/src/util/strings.js b/src/util/strings.js index 88dd571..ffd257b 100644 --- a/src/util/strings.js +++ b/src/util/strings.js @@ -43,7 +43,7 @@ import { bindOpts } from './sugar.js'; export function genStrings(stringsJSON, { he, defaultJSON = null, - bindUtilities = [] + bindUtilities = {} }) { // genStrings will only 8e called once for each language, and it happens // right at the start of the program (or at least 8efore 8uilding pages). @@ -153,40 +153,6 @@ export function genStrings(stringsJSON, { const invalidKeysFound = []; const strings = (key, args = {}) => { - // Ok, with the warning out of the way, it's time to get to work. - // First make sure we're even accessing a valid key. (If not, return - // an error string as su8stitute.) - if (!validKeys.includes(key)) { - // We only want to warn a8out a given key once. More than that is - // just redundant! - if (!invalidKeysFound.includes(key)) { - invalidKeysFound.push(key); - logError`(${baseDirectory}) Accessing invalid key ${key}. Fix a typo or provide this in strings-default.json!`; - } - return `MISSING: ${key}`; - } - - const template = stringIndex[key]; - - // Convert the keys on the args dict from camelCase to CONSTANT_CASE. - // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut - // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. - const processedArgs = Object.entries(args) - .map(([ k, v ]) => [k.replace(/[A-Z]/g, '_$&').toUpperCase(), v]); - - // Replacement time! Woot. Reduce comes in handy here! - const output = processedArgs.reduce( - (x, [ k, v ]) => x.replaceAll(`{${k}}`, v), - template); - - // Post-processing: if any expected arguments *weren't* replaced, that - // is almost definitely an error. - if (output.match(/\{[A-Z_]+\}/)) { - logError`(${baseDirectory}) Args in ${key} were missing - output: ${output}`; - } - - return output; }; // And lastly, we add some utility stuff to the strings function. @@ -243,36 +209,9 @@ export const count = { }, duration: (secTotal, {strings, approximate = false, unit = false}) => { - if (secTotal === 0) { - return strings('count.duration.missing'); - } - - const hour = Math.floor(secTotal / 3600); - const min = Math.floor((secTotal - hour * 3600) / 60); - const sec = Math.floor(secTotal - hour * 3600 - min * 60); - - const pad = val => val.toString().padStart(2, '0'); - - const stringSubkey = unit ? '.withUnit' : ''; - - const duration = (hour > 0 - ? strings('count.duration.hours' + stringSubkey, { - hours: hour, - minutes: pad(min), - seconds: pad(sec) - }) - : strings('count.duration.minutes' + stringSubkey, { - minutes: min, - seconds: pad(sec) - })); - - return (approximate - ? strings('count.duration.approximate', {duration}) - : duration); }, index: (value, {strings}) => { - return strings('count.index.' + strings.intl.plural.ordinal.select(value), {index: value}); }, number: value => strings.intl.number.format(value), -- cgit 1.3.0-6-gf8a5