From 150b9332c70e05ff2558297f7cdc54597f626c6b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:16:56 -0300 Subject: thumbs: remove guts from clearThumbs --- src/gen-thumbs.js | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 3d441bc9..6fe889f4 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -88,7 +88,7 @@ const thumbnailSpec = { import {spawn} from 'node:child_process'; import {createHash} from 'node:crypto'; import {createReadStream} from 'node:fs'; -import {readFile, stat, unlink, writeFile} from 'node:fs/promises'; +import {readFile, stat, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; import dimensionsOf from 'image-size'; @@ -333,14 +333,13 @@ function generateImageThumbnails({ promisifyProcess(convert('.' + name, details), false))); } -export async function clearThumbs(mediaPath, { +export async function processThumbs(mediaPath, { queueSize = 0, } = {}) { if (!mediaPath) { throw new Error('Expected mediaPath to be passed'); } - logInfo`Looking for thumbnails to clear out...`; const thumbFiles = await traverse(mediaPath, { pathStyle: 'device', @@ -349,8 +348,7 @@ export async function clearThumbs(mediaPath, { }); if (thumbFiles.length) { - // Double-check files. Since we're unlinking (deleting) files, - // we're better off safe than sorry! + // Double-check files. const thumbtacks = Object.keys(thumbnailSpec); const unsafeFiles = thumbFiles.filter(file => { if (path.extname(file) !== '.jpg') return true; @@ -369,14 +367,13 @@ export async function clearThumbs(mediaPath, { return {success: false}; } - logInfo`Clearing out ${thumbFiles.length} thumbs.`; const errored = []; - await progressPromiseAll(`Removing thumbnail files`, queue( + await progressPromiseAll(`Processing thumbnail files`, queue( thumbFiles.map(file => async () => { try { - await unlink(file); + /* no-op */ } catch (error) { if (error.code !== 'ENOENT') { errored.push(file); @@ -386,18 +383,18 @@ export async function clearThumbs(mediaPath, { queueSize)); if (errored.length) { - logError`Couldn't remove these paths (${errored.length}):`; + logError`Couldn't process these paths (${errored.length}):`; for (const file of errored) { console.error(file); } - logError`Check for permission errors?`; + logError`It's possible there were permission errors. After you've`; + logError`investigated, running again should work to process these.`; return {success: false}; } else { - logInfo`Successfully deleted all ${thumbFiles.length} thumbnail files!`; + logInfo`Successfully processed all ${thumbFiles.length} thumbnail files!`; } } else { - logInfo`Didn't find any thumbs in media directory.`; - logInfo`${mediaPath}`; + logInfo`Didn't find any thumbnails to process.`; } let cacheExists = false; @@ -406,7 +403,7 @@ export async function clearThumbs(mediaPath, { cacheExists = true; } catch (error) { if (error.code === 'ENOENT') { - logInfo`Cache file already missing, nothing to remove there.`; + logInfo`No cache file present here. (${CACHE_FILE})`; } else { logWarn`Failed to access cache file. Check its permissions?`; } @@ -414,10 +411,11 @@ export async function clearThumbs(mediaPath, { if (cacheExists) { try { - unlink(path.join(mediaPath, CACHE_FILE)); - logInfo`Removed thumbnail cache file.`; + /* no-op */ + logInfo`Processed thumbnail cache file.`; } catch (error) { - logWarn`Failed to remove cache file. Check its permissions?`; + logWarn`Failed to process cache file. (${CACHE_FILE})`; + logWarn`Check its permissions.`; } } -- cgit 1.3.0-6-gf8a5 From 5074a2d0182590d1645af26e402c35ed9333e1d7 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:19:58 -0300 Subject: thumbs: processThumbs -> migrateThumbsIntoDedicatedCacheDirectory --- src/gen-thumbs.js | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 6fe889f4..3d1d66e6 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -88,7 +88,7 @@ const thumbnailSpec = { import {spawn} from 'node:child_process'; import {createHash} from 'node:crypto'; import {createReadStream} from 'node:fs'; -import {readFile, stat, writeFile} from 'node:fs/promises'; +import {mkdir, readFile, rename, stat, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; import dimensionsOf from 'image-size'; @@ -333,13 +333,23 @@ function generateImageThumbnails({ promisifyProcess(convert('.' + name, details), false))); } -export async function processThumbs(mediaPath, { +export async function migrateThumbsIntoDedicatedCacheDirectory({ + mediaPath, + mediaCachePath, + queueSize = 0, -} = {}) { +}) { if (!mediaPath) { - throw new Error('Expected mediaPath to be passed'); + throw new Error('Expected mediaPath'); } + if (!mediaCachePath) { + throw new Error(`Expected mediaCachePath`); + } + + logInfo`Migrating thumbnail files into dedicated directory.`; + logInfo`Moving thumbs from: ${mediaPath}`; + logInfo`Moving thumbs into: ${mediaCachePath}`; const thumbFiles = await traverse(mediaPath, { pathStyle: 'device', @@ -367,13 +377,20 @@ export async function processThumbs(mediaPath, { return {success: false}; } + logInfo`Moving ${thumbFiles.length} thumbs.`; + + await mkdir(mediaCachePath, {recursive: true}); const errored = []; - await progressPromiseAll(`Processing thumbnail files`, queue( + await progressPromiseAll(`Moving thumbnail files`, queue( thumbFiles.map(file => async () => { try { - /* no-op */ + const filePathInMedia = file; + const filePath = path.relative(mediaPath, filePathInMedia); + const filePathInCache = path.join(mediaCachePath, filePath); + await mkdir(path.dirname(filePathInCache), {recursive: true}); + await rename(filePathInMedia, filePathInCache); } catch (error) { if (error.code !== 'ENOENT') { errored.push(file); @@ -383,18 +400,18 @@ export async function processThumbs(mediaPath, { queueSize)); if (errored.length) { - logError`Couldn't process these paths (${errored.length}):`; + logError`Couldn't move these paths (${errored.length}):`; for (const file of errored) { console.error(file); } logError`It's possible there were permission errors. After you've`; - logError`investigated, running again should work to process these.`; + logError`investigated, running again should work to move these.`; return {success: false}; } else { - logInfo`Successfully processed all ${thumbFiles.length} thumbnail files!`; + logInfo`Successfully moved all ${thumbFiles.length} thumbnail files!`; } } else { - logInfo`Didn't find any thumbnails to process.`; + logInfo`Didn't find any thumbnails to move.`; } let cacheExists = false; @@ -411,11 +428,13 @@ export async function processThumbs(mediaPath, { if (cacheExists) { try { - /* no-op */ - logInfo`Processed thumbnail cache file.`; + await rename( + path.join(mediaPath, CACHE_FILE), + path.join(mediaCachePath, CACHE_FILE)); + logInfo`Moved thumbnail cache file.`; } catch (error) { - logWarn`Failed to process cache file. (${CACHE_FILE})`; - logWarn`Check its permissions.`; + logWarn`Failed to move cache file. (${CACHE_FILE})`; + logWarn`Check its permissions, or try copying/pasting.`; } } -- cgit 1.3.0-6-gf8a5 From c3c689fec11a305037fa4fe0775d028d70f8e0a1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:22:13 -0300 Subject: thumbs: determineMediaCachePath --- src/gen-thumbs.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 3d1d66e6..4ceb1bd4 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -88,9 +88,17 @@ const thumbnailSpec = { import {spawn} from 'node:child_process'; import {createHash} from 'node:crypto'; import {createReadStream} from 'node:fs'; -import {mkdir, readFile, rename, stat, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; +import { + mkdir, + readdir, + readFile, + rename, + stat, + writeFile, +} from 'node:fs/promises'; + import dimensionsOf from 'image-size'; import {delay, empty, queue} from '#sugar'; @@ -333,6 +341,82 @@ function generateImageThumbnails({ promisifyProcess(convert('.' + name, details), false))); } +export async function determineMediaCachePath({ + mediaPath, + providedMediaCachePath, + disallowDoubling = false, +}) { + if (!mediaPath) { + return { + annotation: 'media path not provided', + mediaCachePath: null, + }; + } + + if (providedMediaCachePath) { + return { + annotation: 'custom path provided', + mediaCachePath: providedMediaCachePath, + }; + } + + let mediaIncludesThumbnailCache; + + try { + const files = await readdir(mediaPath); + mediaIncludesThumbnailCache = files.includes(CACHE_FILE); + } catch (error) { + mediaIncludesThumbnailCache = false; + } + + if (mediaIncludesThumbnailCache === true && !disallowDoubling) { + return { + annotation: 'media path doubles as cache', + mediaCachePath: mediaPath, + }; + } + + const inferredPath = + path.join( + path.dirname(mediaPath), + path.basename(mediaPath) + '-cache'); + + let inferredIncludesThumbnailCache; + + try { + const files = await readdir(inferredPath); + inferredIncludesThumbnailCache = files.includes(CACHE_FILE); + } catch (error) { + if (error.code === 'ENOENT') { + inferredIncludesThumbnailCache = null; + } else { + inferredIncludesThumbnailCache = undefined; + } + } + + if (inferredIncludesThumbnailCache === true) { + return { + annotation: 'inferred path has cache', + mediaCachePath: inferredPath, + }; + } else if (inferredIncludesThumbnailCache === false) { + return { + annotation: 'inferred path does not have cache', + mediaCachePath: null, + }; + } else if (inferredIncludesThumbnailCache === null) { + return { + annotation: 'inferred path will be created', + mediaCachePath: inferredPath, + }; + } else { + return { + annotation: 'inferred path not readable', + mediaCachePath: null, + }; + } +} + export async function migrateThumbsIntoDedicatedCacheDirectory({ mediaPath, mediaCachePath, -- cgit 1.3.0-6-gf8a5 From 31298379eb88f37e880afb67c2b39a71916552e6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:23:01 -0300 Subject: thumbs: update thumbnail generation to save into separate cache --- src/gen-thumbs.js | 83 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 4ceb1bd4..3210805d 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -312,18 +312,30 @@ async function getSpawnMagick(tool) { // Note: This returns an array of no-argument functions, suitable for passing // to queue(). function generateImageThumbnails({ + mediaPath, + mediaCachePath, filePath, dimensions, spawnConvert, }) { - const dirname = path.dirname(filePath); - const extname = path.extname(filePath); - const basename = path.basename(filePath, extname); - const output = (name) => path.join(dirname, basename + name + '.jpg'); - - const convert = (name, {size, quality}) => - spawnConvert([ - filePath, + const filePathInMedia = path.join(mediaPath, filePath); + + function getOutputPath(thumbtack) { + return path.join( + mediaCachePath, + path.dirname(filePath), + [ + path.basename(filePath, path.extname(filePath)), + thumbtack, + 'jpg' + ].join('.')); + } + + function startConvertProcess(outputPathInCache, details) { + const {size, quality} = details; + + return spawnConvert([ + filePathInMedia, '-strip', '-resize', `${size}x${size}>`, @@ -331,14 +343,20 @@ function generateImageThumbnails({ 'Plane', '-quality', `${quality}%`, - output(name), + outputPathInCache, ]); + } return ( getThumbnailsAvailableForDimensions(dimensions) - .map(([name]) => [name, thumbnailSpec[name]]) - .map(([name, details]) => () => - promisifyProcess(convert('.' + name, details), false))); + .map(([thumbtack]) => [thumbtack, thumbnailSpec[thumbtack]]) + .map(([thumbtack, details]) => async () => { + const outputPathInCache = getOutputPath(thumbtack); + await mkdir(path.dirname(outputPathInCache), {recursive: true}); + + const convertProcess = startConvertProcess(outputPathInCache, details); + await promisifyProcess(convertProcess, false); + })); } export async function determineMediaCachePath({ @@ -525,11 +543,14 @@ export async function migrateThumbsIntoDedicatedCacheDirectory({ return {success: true}; } -export default async function genThumbs(mediaPath, { +export default async function genThumbs({ + mediaPath, + mediaCachePath, + queueSize = 0, magickThreads = defaultMagickThreads, quiet = false, -} = {}) { +}) { if (!mediaPath) { throw new Error('Expected mediaPath to be passed'); } @@ -555,13 +576,13 @@ export default async function genThumbs(mediaPath, { quietInfo`Running up to ${magickThreads + ' magick threads'} simultaneously.`; - let cache, - firstRun = false; + let cache = null; + let firstRun = false; + try { - cache = JSON.parse(await readFile(path.join(mediaPath, CACHE_FILE))); + cache = JSON.parse(await readFile(path.join(mediaCachePath, CACHE_FILE))); quietInfo`Cache file successfully read.`; } catch (error) { - cache = {}; if (error.code === 'ENOENT') { firstRun = true; } else { @@ -573,7 +594,20 @@ export default async function genThumbs(mediaPath, { } try { - await writeFile(path.join(mediaPath, CACHE_FILE), JSON.stringify(cache)); + await mkdir(mediaCachePath, {recursive: true}); + } catch (error) { + logError`Couldn't create the media cache directory: ${error.code}`; + logError`That's where the media files are going to go, so you'll`; + logError`have to investigate this - it's likely a permissions error.`; + return {success: false}; + } + + try { + await writeFile( + path.join(mediaCachePath, CACHE_FILE), + (firstRun + ? JSON.stringify({}) + : JSON.stringify(cache))); quietInfo`Writing to cache file appears to be working.`; } catch (error) { logWarn`Test of cache file writing failed: ${error}`; @@ -581,6 +615,7 @@ export default async function genThumbs(mediaPath, { logWarn`Cache read succeeded: Any newly written thumbs will be unnecessarily regenerated on the next run.`; } else if (firstRun) { logWarn`No cache found: All thumbs will be generated now, and will be unnecessarily regenerated next run.`; + logWarn`You may also have to provide ${'--media-cache-path'} ${mediaCachePath} next run.`; } else { logWarn`Cache read failed: All thumbs will be regenerated now, and will be unnecessarily regenerated again next run.`; } @@ -588,6 +623,10 @@ export default async function genThumbs(mediaPath, { await delay(WARNING_DELAY_TIME); } + if (firstRun) { + cache = {}; + } + const imagePaths = await traverseSourceImagePaths(mediaPath, {target: 'generate'}); const imageToMD5Entries = @@ -675,7 +714,9 @@ export default async function genThumbs(mediaPath, { const generateCalls = entriesToGenerate.flatMap(([filePath, md5]) => generateImageThumbnails({ - filePath: path.join(mediaPath, filePath), + mediaPath, + mediaCachePath, + filePath, dimensions: imageToDimensions[filePath], spawnConvert, }).map(call => async () => { @@ -711,7 +752,7 @@ export default async function genThumbs(mediaPath, { try { await writeFile( - path.join(mediaPath, CACHE_FILE), + path.join(mediaCachePath, CACHE_FILE), JSON.stringify(updatedCache) ); quietInfo`Updated cache file successfully written!`; -- cgit 1.3.0-6-gf8a5 From 70dd340c2cb38f0335ae5e3da4f0c682d5a4fc8c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:26:22 -0300 Subject: upd8: integrate new thumbnail generation to CLI/high-level steps --- src/upd8.js | 64 +++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 27445a8e..b87dd684 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -63,6 +63,7 @@ import genThumbs, { CACHE_FILE as thumbsCacheFile, clearThumbs, defaultMagickThreads, + determineMediaCachePath, isThumb, verifyImagePaths, } from '#thumbs'; @@ -215,6 +216,11 @@ async function main() { type: 'value', }, + 'media-cache-path': { + help: `Specify path to media cache directory, including automatically generated thumbnails\n\nThis usually doesn't need to be provided, and will be inferred by adding "-cache" to the end of the media directory`, + type: 'value', + }, + // String files! For the most part, this is used for translating the // site to different languages, though you can also customize strings // for your own 8uild of the site if you'd like. Files here should all @@ -255,11 +261,6 @@ async function main() { type: 'flag', }, - 'clear-thumbs': { - help: `Clear the thumbnail cache and remove generated thumbnail files from media directory\n\n(This skips building. Run again without --clear-thumbs to build the site.)`, - type: 'flag', - }, - // Just working on data entries and not interested in actually // generating site HTML yet? This flag will cut execution off right // 8efore any site 8uilding actually happens. @@ -431,7 +432,6 @@ async function main() { const skipThumbs = cliOptions['skip-thumbs'] ?? false; const thumbsOnly = cliOptions['thumbs-only'] ?? false; - const clearThumbsFlag = cliOptions['clear-thumbs'] ?? false; const noBuild = cliOptions['no-build'] ?? false; showStepStatusSummary = cliOptions['show-step-summary'] ?? false; @@ -473,6 +473,39 @@ async function main() { }); } + const {mediaCachePath, annotation: mediaCachePathAnnotation} = + await determineMediaCachePath({ + mediaPath, + providedMediaCachePath: + cliOptions['media-cache-path'] || process.env.HSMUSIC_MEDIA_CACHE, + disallowDoubling: + migrateThumbs, + }); + + if (!mediaCachePath) { + logError`Couldn't determine a media cache path. (${mediaCachePathAnnotation})`; + switch (mediaCachePathAnnotation) { + case 'inferred path does not have cache': + logError`If you're certain this is the right path, you can provide it via`; + logError`${'--media-cache-path'} or ${'HSMUSIC_MEDIA_CACHE'}, and it should work.`; + break; + + case 'inferred path not readable': + logError`The folder couldn't be read, which usually indicates`; + logError`a permissions error. Try to resolve this, or provide`; + logError`a new path with ${'--media-cache-path'} or ${'HSMUSIC_MEDIA_CACHE'}.`; + break; + + case 'media path not provided': /* unreachable */ + logError`It seems a ${'--media-path'} (or ${'HSMUSIC_MEDIA'}) wasn't provided.`; + logError`Make sure one of these is actually pointing to a path that exists.`; + break; + } + return false; + } + + logInfo`Using media cache at: ${mediaCachePath} (${mediaCachePathAnnotation})`; + const niceShowAggregate = (error, ...opts) => { showAggregate(error, { showTraces: showAggregateTraces, @@ -486,17 +519,6 @@ async function main() { return false; } - if (clearThumbsFlag) { - const result = await clearThumbs(mediaPath, {queueSize}); - if (result.success) { - logInfo`All done! Remove ${'--clear-thumbs'} to run the next build.`; - if (skipThumbs) { - logInfo`And don't forget to remove ${'--skip-thumbs'} too, eh?`; - } - } - return true; - } - let thumbsCache; if (skipThumbs) { @@ -507,7 +529,7 @@ async function main() { stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; - const thumbsCachePath = path.join(mediaPath, thumbsCacheFile); + const thumbsCachePath = path.join(mediaCachePath, thumbsCacheFile); try { thumbsCache = JSON.parse(await readFile(thumbsCachePath)); @@ -556,7 +578,10 @@ async function main() { logInfo`Begin thumbnail generation... -----+`; - const result = await genThumbs(mediaPath, { + const result = await genThumbs({ + mediaPath, + mediaCachePath, + queueSize, magickThreads, quiet: !thumbsOnly, @@ -1042,6 +1067,7 @@ async function main() { cliOptions, dataPath, mediaPath, + mediaCachePath, queueSize, srcRootPath: __dirname, -- cgit 1.3.0-6-gf8a5 From 8b081bd9a470e108688f755f7ee6e8acf95979c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:26:22 -0300 Subject: upd8: integrate new thumbnail generation to CLI/high-level steps --- src/upd8.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/upd8.js b/src/upd8.js index b87dd684..0c52dc06 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -61,7 +61,6 @@ import { import genThumbs, { CACHE_FILE as thumbsCacheFile, - clearThumbs, defaultMagickThreads, determineMediaCachePath, isThumb, -- cgit 1.3.0-6-gf8a5 From 1a92f5e8198e5d1a56d0d9f7272f731f4e98bfce Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:27:11 -0300 Subject: upd8: integrate --migrate-thumbs option --- src/upd8.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/upd8.js b/src/upd8.js index 0c52dc06..a504275c 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -64,6 +64,7 @@ import genThumbs, { defaultMagickThreads, determineMediaCachePath, isThumb, + migrateThumbsIntoDedicatedCacheDirectory, verifyImagePaths, } from '#thumbs'; @@ -116,6 +117,9 @@ async function main() { loadThumbnailCache: {...defaultStepStatus, name: `load thumbnail cache file`}, + migrateThumbnails: + {...defaultStepStatus, name: `migrate thumbnails`}, + generateThumbnails: {...defaultStepStatus, name: `generate thumbnails`}, @@ -260,6 +264,11 @@ async function main() { type: 'flag', }, + 'migrate-thumbs': { + help: `Transfer automatically generated thumbnail files out of an existing media directory and into the easier-to-manage media-cache directory`, + type: 'flag', + }, + // Just working on data entries and not interested in actually // generating site HTML yet? This flag will cut execution off right // 8efore any site 8uilding actually happens. @@ -429,6 +438,7 @@ async function main() { const mediaPath = cliOptions['media-path'] || process.env.HSMUSIC_MEDIA; const langPath = cliOptions['lang-path'] || process.env.HSMUSIC_LANG; // Can 8e left unset! + const migrateThumbs = cliOptions['migrate-thumbs'] ?? false; const skipThumbs = cliOptions['skip-thumbs'] ?? false; const thumbsOnly = cliOptions['thumbs-only'] ?? false; const noBuild = cliOptions['no-build'] ?? false; @@ -505,6 +515,35 @@ async function main() { logInfo`Using media cache at: ${mediaCachePath} (${mediaCachePathAnnotation})`; + if (migrateThumbs) { + stepStatusSummary.migrateThumbnails.status = STATUS_STARTED_NOT_DONE; + + const result = await migrateThumbsIntoDedicatedCacheDirectory({ + mediaPath, + mediaCachePath, + queueSize, + }); + + if (result.succses) { + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_FATAL_ERROR, + annotation: `view log for details`, + }); + return false; + } + + stepStatusSummary.migrateThumbnails.status = STATUS_DONE_CLEAN; + + logInfo`Good to go! Run hsmusic again without ${'--migrate-thumbs'} to start`; + logInfo`using the migrated media cache.`; + return true; + } else { + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `--migrate-thumbs not provided`, + }); + } + const niceShowAggregate = (error, ...opts) => { showAggregate(error, { showTraces: showAggregateTraces, -- cgit 1.3.0-6-gf8a5 From 08da5be83b7fe7f67ce861fa311ce9b32bb22f55 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:28:20 -0300 Subject: urls, write: integrate separate media cache --- src/url-spec.js | 13 +++++++++++++ src/write/build-modes/live-dev-server.js | 5 ++++- src/write/build-modes/static-build.js | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/url-spec.js b/src/url-spec.js index 2ff0fa5b..699f2bef 100644 --- a/src/url-spec.js +++ b/src/url-spec.js @@ -79,12 +79,25 @@ const urlSpec = { albumCover: 'album-art/<>/cover.<>', albumWallpaper: 'album-art/<>/bg.<>', albumBanner: 'album-art/<>/banner.<>', + trackCover: 'album-art/<>/<>.<>', + artistAvatar: 'artist-avatar/<>.<>', + flashArt: 'flash-art/<>.<>', + albumAdditionalFile: 'album-additional/<>/<>', }, }, + + thumb: { + prefix: 'thumb/', + + paths: { + root: '', + path: '<>', + }, + }, }; // This gets automatically switched in place when working from a baseDirectory, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 1339c322..47d59f95 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -51,6 +51,7 @@ export async function go({ cliOptions, _dataPath, mediaPath, + mediaCachePath, defaultLanguage, languages, @@ -171,7 +172,7 @@ export async function go({ const { area: localFileArea, path: localFilePath - } = pathname.match(/^\/(?static|util|media)\/(?.*)/)?.groups ?? {}; + } = pathname.match(/^\/(?static|util|media|thumb)\/(?.*)/)?.groups ?? {}; if (localFileArea) { // Not security tested, man, this is a dev server!! @@ -182,6 +183,8 @@ export async function go({ localDirectory = path.join(srcRootPath, localFileArea); } else if (localFileArea === 'media') { localDirectory = mediaPath; + } else if (localFileArea === 'thumb') { + localDirectory = mediaCachePath; } let filePath; diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 09316999..96f2a0ed 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -84,6 +84,7 @@ export async function go({ cliOptions, _dataPath, mediaPath, + mediaCachePath, queueSize, defaultLanguage, @@ -133,6 +134,7 @@ export async function go({ await writeSymlinks({ srcRootPath, mediaPath, + mediaCachePath, outputPath, urls, }); @@ -414,6 +416,7 @@ async function writePage({ function writeSymlinks({ srcRootPath, mediaPath, + mediaCachePath, outputPath, urls, }) { @@ -421,6 +424,7 @@ function writeSymlinks({ link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'), link(path.join(srcRootPath, 'static'), 'shared.staticRoot'), link(mediaPath, 'media.root'), + link(mediaCachePath, 'thumb.root'), ]); async function link(directory, urlKey) { -- cgit 1.3.0-6-gf8a5 From 482edbc0ff58da60e51a5f41f8888873d8740a03 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 29 Oct 2023 15:29:20 -0300 Subject: content, client: integrate separate media cache --- src/content/dependencies/image.js | 7 ++++++- src/static/client2.js | 16 ++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 6c0aeecd..8aa9753b 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -77,6 +77,11 @@ export default { originalSrc = ''; } + // TODO: This feels janky. It's necessary to deal with static content that + // includes strings like , but processing the + // src string directly when a parts-formed path *is* available seems wrong. + // It should be possible to do urls.from(slots.path[0]).to(...slots.path), + // for example, but will require reworking the control flow here a little. let mediaSrc = null; if (originalSrc.startsWith(to('media.root'))) { mediaSrc = @@ -160,7 +165,7 @@ export default { // which is the HTML output-appropriate path including `../../` or // another alternate base path. const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc); - thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`); + thumbSrc = to('thumb.path', mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`)); const dimensions = getDimensionsOfImagePath(mediaSrc); availableThumbs = getThumbnailsAvailableForDimensions(dimensions); diff --git a/src/static/client2.js b/src/static/client2.js index 758d91a6..523b48d8 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -954,10 +954,14 @@ function handleImageLinkClicked(evt) { const thumbImage = document.getElementById('image-overlay-image-thumb'); const {href: originalSrc} = evt.target.closest('a'); - const {dataset: { - originalSize: originalFileSize, - thumbs: availableThumbList, - }} = evt.target.closest('a').querySelector('img'); + + const { + src: embeddedSrc, + dataset: { + originalSize: originalFileSize, + thumbs: availableThumbList, + }, + } = evt.target.closest('a').querySelector('img'); updateFileSizeInformation(originalFileSize); @@ -967,8 +971,8 @@ function handleImageLinkClicked(evt) { if (availableThumbList) { const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList); const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList); - mainSrc = originalSrc.replace(/\.(jpg|png)$/, `.${mainThumb}.jpg`); - thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${smallThumb}.jpg`); + mainSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`); + thumbSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`); // Show the thumbnail size on each element's data attributes. // Y'know, just for debugging convenience. mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; -- cgit 1.3.0-6-gf8a5 From ef6cdf04af3d73d5d42f9665511082a146091109 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 16:57:41 -0300 Subject: upd8: quick stepStatusSummary tweaks --- src/upd8.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index a504275c..5515f5d7 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -114,12 +114,12 @@ async function main() { Error.stackTraceLimit = Infinity; stepStatusSummary = { - loadThumbnailCache: - {...defaultStepStatus, name: `load thumbnail cache file`}, - migrateThumbnails: {...defaultStepStatus, name: `migrate thumbnails`}, + loadThumbnailCache: + {...defaultStepStatus, name: `load thumbnail cache file`}, + generateThumbnails: {...defaultStepStatus, name: `generate thumbnails`}, @@ -914,7 +914,7 @@ async function main() { Object.assign(stepStatusSummary.loadLanguageFiles, { status: STATUS_NOT_APPLICABLE, - annotation: `--lang-path and HSMUSIC_LANG not provided`, + annotation: `neither --lang-path nor HSMUSIC_LANG provided`, }); } -- cgit 1.3.0-6-gf8a5 From 47db4cec1b6203a4c14f6d01f396c2418787146a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 16:57:52 -0300 Subject: upd8: add verifyImagePaths to stepStatusSummary --- src/upd8.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/upd8.js b/src/upd8.js index 5515f5d7..c7fe0ca5 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -150,6 +150,9 @@ async function main() { initializeDefaultLanguage: {...defaultStepStatus, name: `initialize default language`}, + verifyImagePaths: + {...defaultStepStatus, name: `verify missing/misplaced image paths`}, + preloadFileSizes: {...defaultStepStatus, name: `preload file sizes`}, @@ -970,9 +973,30 @@ async function main() { const urls = generateURLs(urlSpec); - const {missing: missingImagePaths} = + stepStatusSummary.verifyImagePaths.status = STATUS_STARTED_NOT_DONE; + + const {missing: missingImagePaths, misplaced: misplacedImagePaths} = await verifyImagePaths(mediaPath, {urls, wikiData}); + if (empty(missingImagePaths) && empty(misplacedImagePaths)) { + stepStatusSummary.verifyImagePaths.status = STATUS_DONE_CLEAN; + } else if (empty(missingImagePaths)) { + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_HAS_WARNINGS, + annotation: `misplaced images detected`, + }); + } else if (empty(misplacedImagePaths)) { + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_HAS_WARNINGS, + annotation: `missing images detected`, + }); + } else { + Object.assign(stepStatusSummary.verifyImagePaths, { + status :STATUS_HAS_WARNINGS, + annotation: `missing and misplaced images detected`, + }); + } + const fileSizePreloader = new FileSizePreloader(); // File sizes of additional files need to be precalculated before we can -- cgit 1.3.0-6-gf8a5 From 5f8b0426c27b4aafa2325ace72117d5259ad743e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 16:59:46 -0300 Subject: upd8, write: stepStatusSummary warning for incomplete static-build --- src/upd8.js | 2 +- src/write/build-modes/static-build.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/upd8.js b/src/upd8.js index c7fe0ca5..10fa43ae 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1164,7 +1164,7 @@ async function main() { if (buildModeResult !== true) { Object.assign(stepStatusSummary.performBuild, { status: STATUS_HAS_WARNINGS, - message: `may not have completed - view log for details`, + annotation: `may not have completed - view log for details`, }); return false; diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 96f2a0ed..b6dc9643 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -374,6 +374,8 @@ export async function go({ logWarn`available - albeit possibly outdated! Please scroll up and send`; logWarn`the HSMusic developers a copy of the errors:`; fileIssue({topMessage: null}); + + return false; } return true; -- cgit 1.3.0-6-gf8a5 From 803413839d644cca0a14c2958c46d6ebe8f57e68 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 17:05:56 -0300 Subject: upd8: step summary output style adjustments --- src/upd8.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 10fa43ae..869d7324 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1199,15 +1199,28 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus Object.values(stepStatusSummary) .map(({name}) => name.length)); - const anyStepsNotClean = + const stepsNotClean = Object.values(stepStatusSummary) - .some(({status}) => + .map(({status}) => status === STATUS_HAS_WARNINGS || status === STATUS_FATAL_ERROR || status === STATUS_STARTED_NOT_DONE); - for (const {name, status, annotation} of Object.values(stepStatusSummary)) { - let message = `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + const anyStepsNotClean = + stepsNotClean.includes(true); + + const stepDetails = Object.values(stepStatusSummary); + + for (let index = 0; index < stepDetails.length; index++) { + const {name, status, annotation} = stepDetails[index]; + + let message = + (stepsNotClean[index] + ? `!! ` + : ` - `); + + message += `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + if (annotation) { message += ` (${annotation})`; } @@ -1245,6 +1258,8 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } else { console.error(colors.bright(`Final output is true and all steps are clean.`)); } + } else if (result === false) { + console.error(colors.bright(`Final output is false.`)); } else { console.error(colors.bright(`Final output is not true (${result}).`)); } -- cgit 1.3.0-6-gf8a5 From 8978169af6529321e2db6193bc5fe193126e8258 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 17:11:04 -0300 Subject: upd8: add determineMediaCachePath to stepStatusSummary --- src/upd8.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/upd8.js b/src/upd8.js index 869d7324..23182778 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -114,6 +114,9 @@ async function main() { Error.stackTraceLimit = Infinity; stepStatusSummary = { + determineMediaCachePath: + {...defaultStepStatus, name: `determine media cache path`}, + migrateThumbnails: {...defaultStepStatus, name: `migrate thumbnails`}, @@ -485,6 +488,8 @@ async function main() { }); } + stepStatusSummary.determineMediaCachePath.status = STATUS_STARTED_NOT_DONE; + const {mediaCachePath, annotation: mediaCachePathAnnotation} = await determineMediaCachePath({ mediaPath, @@ -496,6 +501,7 @@ async function main() { if (!mediaCachePath) { logError`Couldn't determine a media cache path. (${mediaCachePathAnnotation})`; + switch (mediaCachePathAnnotation) { case 'inferred path does not have cache': logError`If you're certain this is the right path, you can provide it via`; @@ -513,11 +519,22 @@ async function main() { logError`Make sure one of these is actually pointing to a path that exists.`; break; } + + Object.assign(stepStatusSummary.determineMediaCachePath, { + status: STATUS_FATAL_ERROR, + annotation: mediaCachePathAnnotation, + }); + return false; } logInfo`Using media cache at: ${mediaCachePath} (${mediaCachePathAnnotation})`; + Object.assign(stepStatusSummary.determineMediaCachePath, { + status: STATUS_DONE_CLEAN, + annotation: mediaCachePathAnnotation, + }); + if (migrateThumbs) { stepStatusSummary.migrateThumbnails.status = STATUS_STARTED_NOT_DONE; -- cgit 1.3.0-6-gf8a5 From cc6fff8d198953b66ec984248cd1a4e02937b55b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 18:24:08 -0300 Subject: util: add logicalCWD, logicalPathTo cli functions --- src/util/cli.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/util/cli.js b/src/util/cli.js index 4c08c085..973fef19 100644 --- a/src/util/cli.js +++ b/src/util/cli.js @@ -340,3 +340,34 @@ export function fileIssue({ console.error(colors.red(`- https://hsmusic.wiki/feedback/`)); console.error(colors.red(`- https://github.com/hsmusic/hsmusic-wiki/issues/`)); } + +export async function logicalCWD() { + if (process.env.PWD) { + return process.env.PWD; + } + + const {exec} = await import('node:child_process'); + const {stat} = await import('node:fs/promises'); + + try { + await stat('/bin/sh'); + } catch (error) { + // Not logical, so sad. + return process.cwd(); + } + + const proc = exec('/bin/pwd -L'); + + let output = ''; + proc.stdout.on('data', buf => { output += buf; }); + + await new Promise(resolve => proc.on('exit', resolve)); + + return output.trim(); +} + +export async function logicalPathTo(target) { + const {relative} = await import('node:path'); + const cwd = await logicalCWD(); + return relative(cwd, target); +} -- cgit 1.3.0-6-gf8a5 From cabae7f32a08c8d1e9ce9609b0325cfe81135b60 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 18:24:33 -0300 Subject: thumbs: verifyImagePaths: group whole directories, handle file exts --- src/gen-thumbs.js | 113 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 3210805d..a61baea7 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -101,7 +101,7 @@ import { import dimensionsOf from 'image-size'; -import {delay, empty, queue} from '#sugar'; +import {delay, empty, queue, unique} from '#sugar'; import {CacheableObject} from '#things'; import { @@ -110,6 +110,7 @@ import { logError, logInfo, logWarn, + logicalPathTo, parseOptions, progressPromiseAll, } from '#cli'; @@ -816,28 +817,114 @@ export function checkMissingMisplacedMediaFiles(expectedImagePaths, extantImageP export async function verifyImagePaths(mediaPath, {urls, wikiData}) { const expectedPaths = getExpectedImagePaths(mediaPath, {urls, wikiData}); const extantPaths = await traverseSourceImagePaths(mediaPath, {target: 'verify'}); - const {missing, misplaced} = checkMissingMisplacedMediaFiles(expectedPaths, extantPaths); - if (empty(missing) && empty(misplaced)) { + const {missing: missingPaths, misplaced: misplacedPaths} = + checkMissingMisplacedMediaFiles(expectedPaths, extantPaths); + + if (empty(missingPaths) && empty(misplacedPaths)) { logInfo`All image paths are good - nice! None are missing or misplaced.`; - return {missing, misplaced}; + return {missing: [], misplaced: []}; } - if (!empty(missing)) { - logWarn`** Some image files are missing! (${missing.length + ' files'}) **`; - for (const file of missing) { - console.warn(colors.yellow(` - `) + file); + const relativeMediaPath = await logicalPathTo(mediaPath); + + const dirnamesOfExpectedPaths = + unique(expectedPaths.map(file => path.dirname(file))); + + const dirnamesOfExtantPaths = + unique(extantPaths.map(file => path.dirname(file))); + + const dirnamesOfMisplacedPaths = + unique(misplacedPaths.map(file => path.dirname(file))); + + const completelyMisplacedDirnames = + dirnamesOfMisplacedPaths + .filter(dirname => !dirnamesOfExpectedPaths.includes(dirname)); + + const completelyMissingDirnames = + dirnamesOfExpectedPaths + .filter(dirname => !dirnamesOfExtantPaths.includes(dirname)); + + const individuallyMisplacedPaths = + misplacedPaths + .filter(file => !completelyMisplacedDirnames.includes(path.dirname(file))); + + const individuallyMissingPaths = + missingPaths + .filter(file => !completelyMissingDirnames.includes(path.dirname(file))); + + const wrongExtensionPaths = + misplacedPaths + .map(file => { + const stripExtension = file => + path.join( + path.dirname(file), + path.basename(file, path.extname(file))); + + const extantExtension = path.extname(file); + const basename = stripExtension(file); + + const expectedPath = + missingPaths + .find(file => stripExtension(file) === basename); + + if (!expectedPath) return null; + + const expectedExtension = path.extname(expectedPath); + return {basename, extantExtension, expectedExtension}; + }) + .filter(Boolean); + + if (!empty(missingPaths)) { + if (missingPaths.length === 1) { + logWarn`${1} expected image file is missing from ${relativeMediaPath}:`; + } else { + logWarn`${missingPaths.length} expected image files are missing:`; + } + + for (const dirname of completelyMissingDirnames) { + console.log(` - (missing) All files under ${colors.bright(dirname)}`); + } + + for (const file of individuallyMissingPaths) { + console.log(` - (missing) ${file}`); } } - if (!empty(misplaced)) { - logWarn`** Some image files are misplaced! (${misplaced.length + ' files'}) **`; - for (const file of misplaced) { - console.warn(colors.yellow(` - `) + file); + if (!empty(misplacedPaths)) { + if (misplacedPaths.length === 1) { + logWarn`${1} image file, present in ${relativeMediaPath}, wasn't expected:`; + } else { + logWarn`${misplacedPaths.length} image files, present in ${relativeMediaPath}, weren't expected:`; + } + + for (const dirname of completelyMisplacedDirnames) { + console.log(` - (misplaced) All files under ${colors.bright(dirname)}`); + } + + for (const file of individuallyMisplacedPaths) { + console.log(` - (misplaced) ${file}`); } } - return {missing, misplaced}; + if (!empty(wrongExtensionPaths)) { + if (wrongExtensionPaths.length === 1) { + logWarn`Of these, ${1} has an unexpected file extension:`; + } else { + logWarn`Of these, ${wrongExtensionPaths.length} have an unexpected file extension:`; + } + + for (const {basename, extantExtension, expectedExtension} of wrongExtensionPaths) { + console.log(` - (expected ${colors.green(expectedExtension)}) ${basename + colors.red(extantExtension)}`); + } + + logWarn`To handle unexpected file extensions:`; + logWarn` * Source and ${`replace`} with the correct file, or`; + logWarn` * Add ${`"Cover Art File Extension"`} field (or similar)`; + logWarn` to the respective document in YAML data files.`; + } + + return {missing: missingPaths, misplaced: misplacedPaths}; } // Recursively traverses the provided (extant) media path, filtering so only -- cgit 1.3.0-6-gf8a5 From c1f93e2d270292bca7e991c180db618f4902a3f9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 30 Oct 2023 22:35:18 -0300 Subject: content, data: fix places that assume coverArtDate defaults to date --- src/content/dependencies/generateArtTagGalleryPage.js | 2 +- .../dependencies/generateArtistInfoPageArtworksChunkedList.js | 10 +++++----- src/content/dependencies/generateTrackInfoPage.js | 2 +- src/content/dependencies/listArtistsByLatestContribution.js | 8 ++++---- src/data/things/art-tag.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index c04bfb68..e28b54cb 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -23,7 +23,7 @@ export default { const things = tag.taggedInThings.slice(); sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate, + getDate: thing => thing.coverArtDate ?? thing.date, latestFirst: true, }); diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index aa6efe5e..a3bcf687 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -30,7 +30,7 @@ export default { entry: { type: 'albumCover', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.coverArtistContribs, }, })), @@ -40,7 +40,7 @@ export default { entry: { type: 'albumWallpaper', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.wallpaperArtistContribs, }, })), @@ -50,7 +50,7 @@ export default { entry: { type: 'albumBanner', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.bannerArtistContribs, }, })), @@ -60,7 +60,7 @@ export default { entry: { type: 'trackCover', album: track.album, - date: track.coverArtDate, + date: track.coverArtDate ?? track.date, track: track, contribs: track.coverArtistContribs, }, @@ -69,7 +69,7 @@ export default { sortEntryThingPairs(entries, things => sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate, + getDate: thing => thing.coverArtDate ?? thing.date, })); const chunks = diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 1083d863..93334948 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -82,7 +82,7 @@ export default { ...artist.albumsAsCoverArtist, ...artist.tracksAsCoverArtist, ], { - getDate: albumOrTrack => albumOrTrack.coverArtDate, + getDate: thing => thing.coverArtDate ?? thing.date, }), }), diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js index b6ea8e96..3870afde 100644 --- a/src/content/dependencies/listArtistsByLatestContribution.js +++ b/src/content/dependencies/listArtistsByLatestContribution.js @@ -143,10 +143,10 @@ export default { ], [ // TODO: Per-artwork dates, see #90. - ...artist.tracksAsCoverArtist.map(track => track.coverArtDate), - ...artist.albumsAsCoverArtist.map(album => album.coverArtDate), - ...artist.albumsAsWallpaperArtist.map(album => album.coverArtDate), - ...artist.albumsAsBannerArtist.map(album => album.coverArtDate), + ...artist.tracksAsCoverArtist.map(track => track.coverArtDate ?? track.date), + ...artist.albumsAsCoverArtist.map(album => album.coverArtDate ?? album.date), + ...artist.albumsAsWallpaperArtist.map(album => album.coverArtDate ?? album.date), + ...artist.albumsAsBannerArtist.map(album => album.coverArtDate ?? album.date), ], ]); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 6503beec..8901ab39 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -54,7 +54,7 @@ export class ArtTag extends Thing { sortAlbumsTracksChronologically( [...albumData, ...trackData] .filter(({artTags}) => artTags.includes(artTag)), - {getDate: o => o.coverArtDate}), + {getDate: thing => thing.coverArtDate ?? thing.date}), }, }, }); -- cgit 1.3.0-6-gf8a5 From 0dcb532eaac79e5a3d2f8f633c967c3a7b80788c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 2 Nov 2023 13:38:46 -0300 Subject: thumbs: pass paths through sortByName before printing --- src/gen-thumbs.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index a61baea7..1bbcb9c1 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -103,6 +103,7 @@ import dimensionsOf from 'image-size'; import {delay, empty, queue, unique} from '#sugar'; import {CacheableObject} from '#things'; +import {sortByName} from '#wiki-data'; import { colors, @@ -769,7 +770,7 @@ export default async function genThumbs({ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { const fromRoot = urls.from('media.root'); - return [ + const paths = [ wikiData.albumData .flatMap(album => [ album.hasCoverArt && fromRoot.to('media.albumCover', album.directory, album.coverArtFileExtension), @@ -789,6 +790,10 @@ export function getExpectedImagePaths(mediaPath, {urls, wikiData}) { wikiData.flashData .map(flash => fromRoot.to('media.flashArt', flash.directory, flash.coverArtFileExtension)), ].flat(); + + sortByName(paths, {getName: path => path}); + + return paths; } export function checkMissingMisplacedMediaFiles(expectedImagePaths, extantImagePaths) { @@ -954,7 +959,7 @@ export async function traverseSourceImagePaths(mediaPath, {target}) { throw new Error(`Expected target to be 'verify' or 'generate', got ${target}`); } - return await traverse(mediaPath, { + const paths = await traverse(mediaPath, { pathStyle: (target === 'verify' ? 'posix' : 'device'), prefixPath: '', @@ -984,6 +989,10 @@ export async function traverseSourceImagePaths(mediaPath, {target}) { return true; }, }); + + sortByName(paths, {getName: path => path}); + + return paths; } export function isThumb(file) { -- cgit 1.3.0-6-gf8a5 From 8e174abde6a6b9b46e2cf885115c58bedcfd0802 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 2 Nov 2023 13:58:34 -0300 Subject: yaml: fix mis-nested errors in non-array reference fields --- src/data/yaml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/yaml.js b/src/data/yaml.js index f7856cb7..0ffe9682 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -1662,7 +1662,7 @@ export function filterReferenceErrors(wikiData) { } } - nest({message: `Reference errors in ${inspect(thing)}`}, ({push, filter}) => { + nest({message: `Reference errors in ${inspect(thing)}`}, ({nest, push, filter}) => { for (const [property, findFnKey] of Object.entries(propSpec)) { const value = CacheableObject.getUpdateValue(thing, property); -- cgit 1.3.0-6-gf8a5 From e42e5527b99426c1b74fea150cf62214de73087e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 20:20:27 -0300 Subject: group: add GroupCategory.directory, referenceType group-category --- src/data/things/group.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/data/things/group.js b/src/data/things/group.js index 8764a9db..7bb917ad 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -83,12 +83,15 @@ export class Group extends Thing { } export class GroupCategory extends Thing { + static [Thing.referenceType] = 'group-category'; static [Thing.friendlyName] = `Group Category`; static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose name: name('Unnamed Group Category'), + directory: directory(), + color: color(), groups: referenceList({ -- cgit 1.3.0-6-gf8a5 From 9e42c9f3773d431bc62fcf76f0da2cc852dfc329 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 20:22:00 -0300 Subject: data: wikiData: use validateWikiData instead of instance checks --- src/data/composite/wiki-properties/wikiData.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js index 4ea47785..5965b949 100644 --- a/src/data/composite/wiki-properties/wikiData.js +++ b/src/data/composite/wiki-properties/wikiData.js @@ -1,17 +1,20 @@ // General purpose wiki data constructor, for properties like artistData, // trackData, etc. -import {validateArrayItems, validateInstanceOf} from '#validators'; +import {validateWikiData} from '#validators'; -// TODO: Not templateCompositeFrom. +// TODO: Kludge. +import Thing from '../../things/thing.js'; -// TODO: This should validate with validateWikiData. +// TODO: Not templateCompositeFrom. export default function(thingClass) { + const referenceType = thingClass[Thing.referenceType]; + return { flags: {update: true}, update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), + validate: validateWikiData({referenceType}), }, }; } -- cgit 1.3.0-6-gf8a5 From e4974b2af9419acd644497274bd49f39880b2282 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 20:40:25 -0300 Subject: data: support stepless updating compositions --- src/data/things/composite.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 51525bc1..c3b08f86 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -802,8 +802,8 @@ export function compositeFrom(description) { }); } - if (!compositionNests && !anyStepsCompute && !anyStepsTransform) { - aggregate.push(new TypeError(`Expected at least one step to compute or transform`)); + if (!compositionNests && !compositionUpdates && !anyStepsCompute) { + aggregate.push(new TypeError(`Expected at least one step to compute`)); } aggregate.close(); @@ -1241,8 +1241,10 @@ export function compositeFrom(description) { expose.cache = base.cacheComposition; } } else if (compositionUpdates) { - expose.transform = (value, dependencies) => - _wrapper(value, null, dependencies); + if (!empty(steps)) { + expose.transform = (value, dependencies) => + _wrapper(value, null, dependencies); + } } else { expose.compute = (dependencies) => _wrapper(noTransformSymbol, null, dependencies); -- cgit 1.3.0-6-gf8a5 From c4eba52cd5023e3b503937b47e2bc6f77527d5c3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 20:43:29 -0300 Subject: data: wikiData: port to templateCompositeFrom syntax --- src/data/composite/wiki-properties/wikiData.js | 29 +++++++++++++++++--------- src/data/things/album.js | 19 +++++++++++++---- src/data/things/art-tag.js | 9 ++++++-- src/data/things/artist.js | 19 +++++++++++++---- src/data/things/flash.js | 18 ++++++++++++---- src/data/things/group.js | 13 +++++++++--- src/data/things/homepage-layout.js | 16 +++++++++----- src/data/things/track.js | 24 ++++++++++++++++----- src/data/things/wiki-info.js | 4 +++- 9 files changed, 113 insertions(+), 38 deletions(-) diff --git a/src/data/composite/wiki-properties/wikiData.js b/src/data/composite/wiki-properties/wikiData.js index 5965b949..5cea49a0 100644 --- a/src/data/composite/wiki-properties/wikiData.js +++ b/src/data/composite/wiki-properties/wikiData.js @@ -1,20 +1,29 @@ // General purpose wiki data constructor, for properties like artistData, // trackData, etc. +import {input, templateCompositeFrom} from '#composite'; import {validateWikiData} from '#validators'; +import {inputThingClass} from '#composite/wiki-data'; + // TODO: Kludge. import Thing from '../../things/thing.js'; -// TODO: Not templateCompositeFrom. +export default templateCompositeFrom({ + annotation: `wikiData`, + + compose: false, + + inputs: { + class: inputThingClass(), + }, -export default function(thingClass) { - const referenceType = thingClass[Thing.referenceType]; + update: ({ + [input.staticValue('class')]: thingClass, + }) => { + const referenceType = thingClass[Thing.referenceType]; + return {validate: validateWikiData({referenceType})}; + }, - return { - flags: {update: true}, - update: { - validate: validateWikiData({referenceType}), - }, - }; -} + steps: () => [], +}); diff --git a/src/data/things/album.js b/src/data/things/album.js index 546fda3b..af3eb042 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -121,10 +121,21 @@ export class Album extends Thing { // Update only - artistData: wikiData(Artist), - artTagData: wikiData(ArtTag), - groupData: wikiData(Group), - trackData: wikiData(Track), + artistData: wikiData({ + class: input.value(Artist), + }), + + artTagData: wikiData({ + class: input.value(ArtTag), + }), + + groupData: wikiData({ + class: input.value(Group), + }), + + trackData: wikiData({ + class: input.value(Track), + }), // Expose only diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 8901ab39..f9e5f0f3 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -40,8 +40,13 @@ export class ArtTag extends Thing { // Update only - albumData: wikiData(Album), - trackData: wikiData(Track), + albumData: wikiData({ + class: input.value(Album), + }), + + trackData: wikiData({ + class: input.value(Track), + }), // Expose only diff --git a/src/data/things/artist.js b/src/data/things/artist.js index ea19d2ba..e0350b86 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -45,10 +45,21 @@ export class Artist extends Thing { // Update only - albumData: wikiData(Album), - artistData: wikiData(Artist), - flashData: wikiData(Flash), - trackData: wikiData(Track), + albumData: wikiData({ + class: input.value(Album), + }), + + artistData: wikiData({ + class: input.value(Artist), + }), + + flashData: wikiData({ + class: input.value(Flash), + }), + + trackData: wikiData({ + class: input.value(Track), + }), // Expose only diff --git a/src/data/things/flash.js b/src/data/things/flash.js index e2afcef4..1bdda6c8 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -95,9 +95,17 @@ export class Flash extends Thing { // Update only - artistData: wikiData(Artist), - trackData: wikiData(Track), - flashActData: wikiData(FlashAct), + artistData: wikiData({ + class: input.value(Artist), + }), + + trackData: wikiData({ + class: input.value(Track), + }), + + flashActData: wikiData({ + class: input.value(FlashAct), + }), // Expose only @@ -159,6 +167,8 @@ export class FlashAct extends Thing { // Update only - flashData: wikiData(Flash), + flashData: wikiData({ + class: input.value(Flash), + }), }) } diff --git a/src/data/things/group.js b/src/data/things/group.js index 7bb917ad..75469bbd 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -34,8 +34,13 @@ export class Group extends Thing { // Update only - albumData: wikiData(Album), - groupCategoryData: wikiData(GroupCategory), + albumData: wikiData({ + class: input.value(Album), + }), + + groupCategoryData: wikiData({ + class: input.value(GroupCategory), + }), // Expose only @@ -102,6 +107,8 @@ export class GroupCategory extends Thing { // Update only - groupData: wikiData(Group), + groupData: wikiData({ + class: input.value(Group), + }), }); } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index bfa971ca..59c069bd 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -70,11 +70,17 @@ export class HomepageLayoutRow extends Thing { // Update only - // These aren't necessarily used by every HomepageLayoutRow subclass, but - // for convenience of providing this data, every row accepts all wiki data - // arrays depended upon by any subclass's behavior. - albumData: wikiData(Album), - groupData: wikiData(Group), + // These wiki data arrays aren't necessarily used by every subclass, but + // to the convenience of providing these, the superclass accepts all wiki + // data arrays depended upon by any subclass. + + albumData: wikiData({ + class: input.value(Album), + }), + + groupData: wikiData({ + class: input.value(Group), + }), }); } diff --git a/src/data/things/track.js b/src/data/things/track.js index db325a17..8d310611 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -256,11 +256,25 @@ export class Track extends Thing { // Update only - albumData: wikiData(Album), - artistData: wikiData(Artist), - artTagData: wikiData(ArtTag), - flashData: wikiData(Flash), - trackData: wikiData(Track), + albumData: wikiData({ + class: input.value(Album), + }), + + artistData: wikiData({ + class: input.value(Artist), + }), + + artTagData: wikiData({ + class: input.value(ArtTag), + }), + + flashData: wikiData({ + class: input.value(Flash), + }), + + trackData: wikiData({ + class: input.value(Track), + }), // Expose only diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 6286a267..89053d62 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -64,6 +64,8 @@ export class WikiInfo extends Thing { // Update only - groupData: wikiData(Group), + groupData: wikiData({ + class: input.value(Group), + }), }); } -- cgit 1.3.0-6-gf8a5 From 9f4c3b913fa6b12a236cedf76abe120f0321f53e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 21:01:23 -0300 Subject: data: validateWikiData: early exit for mixed items --- src/data/things/validators.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/data/things/validators.js b/src/data/things/validators.js index ee301f15..ea4303fc 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -433,18 +433,38 @@ export function validateWikiData({ OK = true; return true; } - const allRefTypes = - new Set(array.map(object => - object.constructor[Symbol.for('Thing.referenceType')])); + const allRefTypes = new Set(); - if (allRefTypes.has(undefined)) { - if (allRefTypes.size === 1) { - throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + let foundThing = false; + let foundOtherObject = false; + + for (const object of array) { + const {[Symbol.for('Thing.referenceType')]: referenceType} = object.constructor; + + if (referenceType === undefined) { + foundOtherObject = true; + + // Early-exit if a Thing has been found - nothing more can be learned. + if (foundThing) { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } } else { - throw new TypeError(`Expected array of wiki data objects, got mixed items`); + foundThing = true; + + // Early-exit if a non-Thing object has been found - nothing more can + // be learned. + if (foundOtherObject) { + throw new TypeError(`Expected array of wiki data objects, got mixed items`); + } + + allRefTypes.add(referenceType); } } + if (foundOtherObject && !foundThing) { + throw new TypeError(`Expected array of wiki data objects, got array of other objects`); + } + if (allRefTypes.size > 1) { if (allowMixedTypes) { OK = true; return true; -- cgit 1.3.0-6-gf8a5 From 5aad4eae6629eaa1e4dd849b03abff8888afdb4d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 21:01:48 -0300 Subject: data: validateWikiData: fix messaging for mismatch one-ref-type --- src/data/things/validators.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data/things/validators.js b/src/data/things/validators.js index ea4303fc..f60c363c 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -484,8 +484,10 @@ export function validateWikiData({ throw new TypeError(`Expected array of unmixed reference types, got multiple: ${types()}`); } - if (referenceType && !allRefTypes.has(referenceType)) { - throw new TypeError(`Expected array of ${referenceType}, got array of ${allRefTypes[0]}`) + const onlyRefType = Array.from(allRefTypes)[0]; + + if (referenceType && onlyRefType !== referenceType) { + throw new TypeError(`Expected array of ${referenceType}, got array of ${onlyRefType}`) } OK = true; return true; -- cgit 1.3.0-6-gf8a5 From 527e4618fdc57d80ac79ca9ceb3eed60fca90d6b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 4 Nov 2023 21:19:44 -0300 Subject: data: always require at least one step for nesting compositions --- src/data/things/composite.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/data/things/composite.js b/src/data/things/composite.js index c3b08f86..113f0a4f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -637,6 +637,10 @@ export function compositeFrom(description) { const compositionNests = description.compose ?? true; + if (compositionNests && empty(steps)) { + aggregate.push(new TypeError(`Expected at least one step`)); + } + // Steps default to exposing if using a shorthand syntax where flags aren't // specified at all. const stepsExpose = -- cgit 1.3.0-6-gf8a5 From e75c457449e49e5f18f07fffec963c9aa626662d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 15:45:30 -0400 Subject: upd8: move "not applicable" checks to top of upd8 evaluation --- src/upd8.js | 82 +++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 23182778..84e6ea84 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -488,6 +488,53 @@ async function main() { }); } + // Prepare not-applicable steps before anything else. + + if (skipThumbs) { + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `provided --skip-thumbs`, + }); + } else { + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_NOT_APPLICABLE, + annotation: `using cache from thumbnail generation`, + }); + } + + if (!migrateThumbs) { + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_NOT_APPLICABLE, + annotation: `--migrate-thumbs not provided`, + }); + } + + if (!precacheData) { + Object.assign(stepStatusSummary.precacheData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-data not provided`, + }); + } + + if (!langPath) { + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_NOT_APPLICABLE, + annotation: `neither --lang-path nor HSMUSIC_LANG provided`, + }); + } + + if (noBuild) { + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_NOT_APPLICABLE, + annotation: `--no-build provided`, + }); + } + + if (skipThumbs && thumbsOnly) { + logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; + return false; + } + stepStatusSummary.determineMediaCachePath.status = STATUS_STARTED_NOT_DONE; const {mediaCachePath, annotation: mediaCachePathAnnotation} = @@ -557,11 +604,6 @@ async function main() { logInfo`Good to go! Run hsmusic again without ${'--migrate-thumbs'} to start`; logInfo`using the migrated media cache.`; return true; - } else { - Object.assign(stepStatusSummary.migrateThumbnails, { - status: STATUS_NOT_APPLICABLE, - annotation: `--migrate-thumbs not provided`, - }); } const niceShowAggregate = (error, ...opts) => { @@ -572,19 +614,9 @@ async function main() { }); }; - if (skipThumbs && thumbsOnly) { - logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; - return false; - } - let thumbsCache; if (skipThumbs) { - Object.assign(stepStatusSummary.generateThumbnails, { - status: STATUS_NOT_APPLICABLE, - annotation: `provided --skip-thumbs`, - }); - stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; const thumbsCachePath = path.join(mediaCachePath, thumbsCacheFile); @@ -627,11 +659,6 @@ async function main() { logInfo`Skipping thumbnail generation.`; } else { - Object.assign(stepStatusSummary.loadThumbnailCache, { - status: STATUS_NOT_APPLICABLE, - annotation: `using cache from thumbnail generation`, - }); - stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE; logInfo`Begin thumbnail generation... -----+`; @@ -852,19 +879,9 @@ async function main() { .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); stepStatusSummary.precacheData.status = STATUS_DONE_CLEAN; - } else { - Object.assign(stepStatusSummary.precacheData, { - status: STATUS_NOT_APPLICABLE, - annotation: `--precache-data not provided`, - }); } if (noBuild) { - Object.assign(stepStatusSummary.performBuild, { - status: STATUS_NOT_APPLICABLE, - annotation: `--no-build provided`, - }); - displayCompositeCacheAnalysis(); if (precacheData) { @@ -931,11 +948,6 @@ async function main() { stepStatusSummary.loadLanguageFiles.status = STATUS_DONE_CLEAN; } else { languages = {}; - - Object.assign(stepStatusSummary.loadLanguageFiles, { - status: STATUS_NOT_APPLICABLE, - annotation: `neither --lang-path nor HSMUSIC_LANG provided`, - }); } stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE; -- cgit 1.3.0-6-gf8a5 From 6c0955b9e3d1f0f46b1f8c559b328238d946f71d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 15:47:04 -0400 Subject: upd8: --skip-reference-validation option --- src/upd8.js | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 84e6ea84..6f36bdb7 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -255,6 +255,11 @@ async function main() { type: 'flag', }, + 'skip-reference-validation': { + help: `Skips checking and reporting reference errors, which speeds up the build but may silently allow erroneous data to pass through`, + type: 'flag', + }, + // Thum8nail gener8tion is *usually* something you want, 8ut it can 8e // kinda a pain to run every time, since it does necessit8te reading // every media file at run time. Pass this to skip it. @@ -447,6 +452,7 @@ async function main() { const migrateThumbs = cliOptions['migrate-thumbs'] ?? false; const skipThumbs = cliOptions['skip-thumbs'] ?? false; const thumbsOnly = cliOptions['thumbs-only'] ?? false; + const skipReferenceValidation = cliOptions['skip-reference-validation'] ?? false; const noBuild = cliOptions['no-build'] ?? false; showStepStatusSummary = cliOptions['show-step-summary'] ?? false; @@ -509,6 +515,16 @@ async function main() { }); } + if (skipReferenceValidation) { + logWarn`Skipping reference validation. If any reference errors are present`; + logWarn`in data, they will be silently passed along to the build.`; + + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_NOT_APPLICABLE, + annotation: `--skip-reference-validation provided`, + }); + } + if (!precacheData) { Object.assign(stepStatusSummary.precacheData, { status: STATUS_NOT_APPLICABLE, @@ -833,25 +849,27 @@ async function main() { // Filter out any reference errors throughout the data, warning about them // too. - stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; + if (!skipReferenceValidation) { + stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; - const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); + const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); - try { - filterReferenceErrorsAggregate.close(); - logInfo`All references validated without any errors - nice!`; - stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN; - } catch (error) { - niceShowAggregate(error); + try { + filterReferenceErrorsAggregate.close(); + logInfo`All references validated without any errors - nice!`; + stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN; + } catch (error) { + niceShowAggregate(error); - logWarn`The above errors were detected while validating references in data files.`; - logWarn`The wiki will still build, but these connections between data objects`; - logWarn`will be completely skipped. Resolve the errors for more complete output.`; + logWarn`The above errors were detected while validating references in data files.`; + logWarn`The wiki will still build, but these connections between data objects`; + logWarn`will be completely skipped. Resolve the errors for more complete output.`; - Object.assign(stepStatusSummary.filterReferenceErrors, { - status: STATUS_HAS_WARNINGS, - annotation: `view log for details`, - }); + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + }); + } } // Sort data arrays so that they're all in order! This may use properties -- cgit 1.3.0-6-gf8a5 From 778ad4cafbc45a7de5e6251f317c62d115380c0b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 16:19:54 -0400 Subject: upd8: show step and total durations in step summary --- src/upd8.js | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 216 insertions(+), 42 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 6f36bdb7..59be9473 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -551,7 +551,10 @@ async function main() { return false; } - stepStatusSummary.determineMediaCachePath.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.determineMediaCachePath, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const {mediaCachePath, annotation: mediaCachePathAnnotation} = await determineMediaCachePath({ @@ -586,6 +589,7 @@ async function main() { Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_FATAL_ERROR, annotation: mediaCachePathAnnotation, + timeEnd: Date.now(), }); return false; @@ -596,10 +600,14 @@ async function main() { Object.assign(stepStatusSummary.determineMediaCachePath, { status: STATUS_DONE_CLEAN, annotation: mediaCachePathAnnotation, + timeEnd: Date.now(), }); if (migrateThumbs) { - stepStatusSummary.migrateThumbnails.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const result = await migrateThumbsIntoDedicatedCacheDirectory({ mediaPath, @@ -611,14 +619,20 @@ async function main() { Object.assign(stepStatusSummary.migrateThumbnails, { status: STATUS_FATAL_ERROR, annotation: `view log for details`, + timeEnd: Date.now(), }); + return false; } - stepStatusSummary.migrateThumbnails.status = STATUS_DONE_CLEAN; - logInfo`Good to go! Run hsmusic again without ${'--migrate-thumbs'} to start`; logInfo`using the migrated media cache.`; + + Object.assign(stepStatusSummary.migrateThumbnails, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + return true; } @@ -633,14 +647,15 @@ async function main() { let thumbsCache; if (skipThumbs) { - stepStatusSummary.loadThumbnailCache.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const thumbsCachePath = path.join(mediaCachePath, thumbsCacheFile); try { thumbsCache = JSON.parse(await readFile(thumbsCachePath)); - logInfo`Thumbnail cache file successfully read.`; - stepStatusSummary.loadThumbnailCache.status = STATUS_DONE_CLEAN; } catch (error) { if (error.code === 'ENOENT') { logError`The thumbnail cache doesn't exist, and it's necessary to build` @@ -651,6 +666,7 @@ async function main() { Object.assign(stepStatusSummary.loadThumbnailCache, { status: STATUS_FATAL_ERROR, annotation: `cache does not exist`, + timeEnd: Date.now(), }); return false; @@ -667,15 +683,26 @@ async function main() { Object.assign(stepStatusSummary.loadThumbnailCache, { status: STATUS_FATAL_ERROR, annotation: `cache malformed or unreadable`, + timeEnd: Date.now(), }); return false; } } + logInfo`Thumbnail cache file successfully read.`; + + Object.assign(stepStatusSummary.loadThumbnailCache, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + logInfo`Skipping thumbnail generation.`; } else { - stepStatusSummary.generateThumbnails.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); logInfo`Begin thumbnail generation... -----+`; @@ -694,12 +721,16 @@ async function main() { Object.assign(stepStatusSummary.generateThumbnails, { status: STATUS_FATAL_ERROR, annotation: `view log for details`, + timeEnd: Date.now(), }); return false; } - stepStatusSummary.generateThumbnails.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.generateThumbnails, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); if (thumbsOnly) { return true; @@ -720,7 +751,10 @@ async function main() { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } - stepStatusSummary.loadDataFiles.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); let processDataAggregate, wikiDataResult; @@ -736,6 +770,7 @@ async function main() { Object.assign(stepStatusSummary.loadDataFiles, { status: STATUS_FATAL_ERROR, annotation: `javascript error - view log for details`, + timeEnd: Date.now(), }); return false; @@ -780,15 +815,7 @@ async function main() { } catch (error) { niceShowAggregate(error); logWarn`The above errors were detected while processing data files.`; - logWarn`If the remaining valid data is complete enough, the wiki will`; - logWarn`still build - but all errored data will be skipped.`; - logWarn`(Resolve errors for more complete output!)`; errorless = false; - - Object.assign(stepStatusSummary.loadDataFiles, { - status: STATUS_HAS_WARNINGS, - annotation: `view log for details`, - }); } if (!wikiData.wikiInfo) { @@ -797,6 +824,7 @@ async function main() { Object.assign(stepStatusSummary.loadDataFiles, { status: STATUS_FATAL_ERROR, annotation: `wiki info object not available`, + timeEnd: Date.now(), }); return false; @@ -804,7 +832,21 @@ async function main() { if (errorless) { logInfo`All data files processed without any errors - nice!`; - stepStatusSummary.loadDataFiles.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + } else { + logWarn`If the remaining valid data is complete enough, the wiki will`; + logWarn`still build - but all errored data will be skipped.`; + logWarn`(Resolve errors for more complete output!)`; + + Object.assign(stepStatusSummary.loadDataFiles, { + status: STATUS_HAS_WARNINGS, + annotation: `view log for details`, + timeEnd: Date.now(), + }); } } @@ -812,16 +854,25 @@ async function main() { // complete, so properties (like dates!) are inherited where that's // appropriate. - stepStatusSummary.linkWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.linkWikiDataArrays, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); linkWikiDataArrays(wikiData); - stepStatusSummary.linkWikiDataArrays.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.linkWikiDataArrays, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); // Filter out any things with duplicate directories throughout the data, // warning about them too. - stepStatusSummary.filterDuplicateDirectories.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const filterDuplicateDirectoriesAggregate = filterDuplicateDirectories(wikiData); @@ -829,7 +880,11 @@ async function main() { try { filterDuplicateDirectoriesAggregate.close(); logInfo`No duplicate directories found - nice!`; - stepStatusSummary.filterDuplicateDirectories.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.filterDuplicateDirectories, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } catch (aggregate) { niceShowAggregate(aggregate); @@ -841,6 +896,7 @@ async function main() { Object.assign(stepStatusSummary.filterDuplicateDirectories, { status: STATUS_FATAL_ERROR, annotation: `duplicate directories found`, + timeEnd: Date.now(), }); return false; @@ -850,14 +906,22 @@ async function main() { // too. if (!skipReferenceValidation) { - stepStatusSummary.filterReferenceErrors.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const filterReferenceErrorsAggregate = filterReferenceErrors(wikiData); try { filterReferenceErrorsAggregate.close(); + logInfo`All references validated without any errors - nice!`; - stepStatusSummary.filterReferenceErrors.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.filterReferenceErrors, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } catch (error) { niceShowAggregate(error); @@ -868,6 +932,7 @@ async function main() { Object.assign(stepStatusSummary.filterReferenceErrors, { status: STATUS_HAS_WARNINGS, annotation: `view log for details`, + timeEnd: Date.now(), }); } } @@ -875,14 +940,23 @@ async function main() { // Sort data arrays so that they're all in order! This may use properties // which are only available after the initial linking. - stepStatusSummary.sortWikiDataArrays.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.sortWikiDataArrays, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); sortWikiDataArrays(wikiData); - stepStatusSummary.sortWikiDataArrays.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.sortWikiDataArrays, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); if (precacheData) { - stepStatusSummary.precacheData.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.precacheData, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); // TODO: Aggregate errors here, instead of just throwing. progressCallAll('Caching all data values', Object.entries(wikiData) @@ -896,7 +970,10 @@ async function main() { .flatMap(([_key, things]) => things) .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); - stepStatusSummary.precacheData.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.precacheData, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } if (noBuild) { @@ -907,13 +984,21 @@ async function main() { } } + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + let internalDefaultLanguage; try { internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); - stepStatusSummary.loadInternalDefaultLanguage.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } catch (error) { console.error(error); @@ -923,6 +1008,7 @@ async function main() { Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { status: STATUS_FATAL_ERROR, annotation: `see log for details`, + timeEnd: Date.now(), }); return false; @@ -931,7 +1017,10 @@ async function main() { let languages; if (langPath) { - stepStatusSummary.loadLanguageFiles.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const languageDataFiles = await traverse(langPath, { filterFile: name => path.extname(name) === '.json', @@ -954,6 +1043,7 @@ async function main() { Object.assign(stepStatusSummary.loadLanguageFiles, { status: STATUS_FATAL_ERROR, annotation: `see log for details`, + timeEnd: Date.now(), }); return false; @@ -963,12 +1053,18 @@ async function main() { Object.fromEntries( results.map((language) => [language.code, language])); - stepStatusSummary.loadLanguageFiles.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } else { languages = {}; } - stepStatusSummary.initializeDefaultLanguage.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; @@ -982,6 +1078,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_DONE_CLEAN, annotation: `using wiki-specified custom default language`, + timeEnd: Date.now(), }); } else if (wikiData.wikiInfo.defaultLanguage) { logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; @@ -994,6 +1091,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_FATAL_ERROR, annotation: `wiki specifies default language whose file is not available`, + timeEnd: Date.now(), }); return false; @@ -1005,6 +1103,7 @@ async function main() { Object.assign(stepStatusSummary.initializeDefaultLanguage, { status: STATUS_DONE_CLEAN, annotation: `no custom default language specified`, + timeEnd: Date.now(), }); } @@ -1020,30 +1119,44 @@ async function main() { const urls = generateURLs(urlSpec); - stepStatusSummary.verifyImagePaths.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); const {missing: missingImagePaths, misplaced: misplacedImagePaths} = await verifyImagePaths(mediaPath, {urls, wikiData}); if (empty(missingImagePaths) && empty(misplacedImagePaths)) { - stepStatusSummary.verifyImagePaths.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.verifyImagePaths, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } else if (empty(missingImagePaths)) { Object.assign(stepStatusSummary.verifyImagePaths, { status: STATUS_HAS_WARNINGS, annotation: `misplaced images detected`, + timeEnd: Date.now(), }); } else if (empty(misplacedImagePaths)) { Object.assign(stepStatusSummary.verifyImagePaths, { status: STATUS_HAS_WARNINGS, annotation: `missing images detected`, + timeEnd: Date.now(), }); } else { Object.assign(stepStatusSummary.verifyImagePaths, { - status :STATUS_HAS_WARNINGS, + status: STATUS_HAS_WARNINGS, annotation: `missing and misplaced images detected`, + timeEnd: Date.now(), }); } + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + const fileSizePreloader = new FileSizePreloader(); // File sizes of additional files need to be precalculated before we can @@ -1107,8 +1220,6 @@ async function main() { const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths); - stepStatusSummary.preloadFileSizes.status = STATUS_STARTED_NOT_DONE; - logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; fileSizePreloader.loadPaths(...additionalFilePaths.map((path) => path.device)); @@ -1127,10 +1238,15 @@ async function main() { Object.assign(stepStatusSummary.preloadFileSizes, { status: STATUS_HAS_WARNINGS, annotation: `see log for details`, + timeEnd: Date.now(), }); } else { logInfo`Done preloading filesizes without any errors - nice!`; - stepStatusSummary.preloadFileSizes.status = STATUS_DONE_CLEAN; + + Object.assign(stepStatusSummary.preloadFileSizes, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); } if (noBuild) { @@ -1167,7 +1283,10 @@ async function main() { .map(line => ` ` + line) .join('\n') + `\n-->`; - stepStatusSummary.performBuild.status = STATUS_STARTED_NOT_DONE; + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); let buildModeResult; @@ -1203,6 +1322,7 @@ async function main() { Object.assign(stepStatusSummary.performBuild, { status: STATUS_FATAL_ERROR, message: `javascript error - view log for details`, + timeEnd: Date.now(), }); return false; @@ -1212,12 +1332,16 @@ async function main() { Object.assign(stepStatusSummary.performBuild, { status: STATUS_HAS_WARNINGS, annotation: `may not have completed - view log for details`, + timeEnd: Date.now(), }); return false; } - stepStatusSummary.performBuild.status = STATUS_DONE_CLEAN; + Object.assign(stepStatusSummary.performBuild, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); return true; } @@ -1228,6 +1352,8 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus (async () => { let result; + const totalTimeStart = Date.now(); + try { result = await main(); } catch (error) { @@ -1238,7 +1364,28 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } } + const totalTimeEnd = Date.now(); + + const formatDuration = timeDelta => { + const seconds = timeDelta / 1000; + + if (seconds > 90) { + const modSeconds = Math.floor(seconds % 60); + const minutes = Math.floor(seconds - seconds % 60) / 60; + return `${minutes}m${modSeconds}s`; + } + + if (seconds < 0.1) { + return 'instant'; + } + + const precision = (seconds > 1 ? 3 : 2); + return `${seconds.toPrecision(precision)}s`; + }; + if (showStepStatusSummary) { + const totalDuration = formatDuration(totalTimeEnd - totalTimeStart); + console.error(colors.bright(`Step summary:`)); const longestNameLength = @@ -1258,15 +1405,40 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus const stepDetails = Object.values(stepStatusSummary); + const stepDurations = + stepDetails.map(({status, timeStart, timeEnd}) => { + if ( + status === STATUS_NOT_APPLICABLE || + status === STATUS_NOT_STARTED || + status === STATUS_STARTED_NOT_DONE + ) { + return '-'; + } + + if (typeof timeStart !== 'number' || typeof timeEnd !== 'number') { + return 'unknown'; + } + + return formatDuration(timeEnd - timeStart); + }); + + const longestDurationLength = + Math.max(...stepDurations.map(duration => duration.length)); + for (let index = 0; index < stepDetails.length; index++) { const {name, status, annotation} = stepDetails[index]; + const duration = stepDurations[index]; let message = (stepsNotClean[index] ? `!! ` : ` - `); - message += `${(name + ': ').padEnd(longestNameLength + 4, '.')} ${status}`; + message += `(${duration})`.padStart(longestDurationLength + 2, ' '); + message += ` `; + message += `${name}: `.padEnd(longestNameLength + 4, '.'); + message += ` `; + message += status; if (annotation) { message += ` (${annotation})`; @@ -1297,6 +1469,8 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } } + console.error(colors.bright(`Done in ${totalDuration}.`)); + if (result === true) { if (anyStepsNotClean) { console.error(colors.bright(`Final output is true, but some steps aren't clean.`)); -- cgit 1.3.0-6-gf8a5 From ff9ebc7232645971783c444d9a58953ebfd2da37 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 16:25:40 -0400 Subject: write: live-dev-server: add --skip-serving option for perf testing --- src/write/build-modes/live-dev-server.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 47d59f95..ab6ceecb 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -44,6 +44,11 @@ export function getCLIOptions() { help: `Enables outputting [200] and [404] responses in the server log, which are suppressed by default`, type: 'flag', }, + + 'skip-serving': { + help: `Causes the build to exit when it would start serving over HTTP instead\n\nMainly useful for testing performance`, + type: 'flag', + }, }; } @@ -78,6 +83,7 @@ export async function go({ const host = cliOptions['host'] ?? defaultHost; const port = parseInt(cliOptions['port'] ?? defaultPort); const loudResponses = cliOptions['loud-responses'] ?? false; + const skipServing = cliOptions['skip-serving'] ?? false; const contentDependenciesWatcher = await watchContentDependencies(); const {contentDependencies} = contentDependenciesWatcher; @@ -396,10 +402,14 @@ export async function go({ } }); - server.listen(port, host); + if (skipServing) { + logInfo`Ready to serve! But --skip-serving was passed, so all done.`; + } else { + server.listen(port, host); - // Just keep going... forever!!! - await new Promise(() => {}); + // Just keep going... forever!!! + await new Promise(() => {}); + } return true; } -- cgit 1.3.0-6-gf8a5 From d6672865a59a94a2acdd3ec7e827f96f8c3e67e4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 17:52:25 -0400 Subject: data: withAlwaysReferenceByDirectory: micro-optimizations --- .../things/track/withAlwaysReferenceByDirectory.js | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index d27f7b23..52d72124 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -53,19 +53,28 @@ export default templateCompositeFrom({ // logic on a completely unrelated context. { dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'], - compute: (continuation, { + compute(continuation, { [input.myself()]: thisTrack, ['trackData']: trackData, ['originalReleaseTrack']: ref, - }) => continuation({ - ['#originalRelease']: - (ref.startsWith('track:') - ? trackData.find(track => track.directory === ref.slice('track:'.length)) - : trackData.find(track => - track !== thisTrack && - !CacheableObject.getUpdateValue(track, 'originalReleaseTrack') && - track.name.toLowerCase() === ref.toLowerCase())), - }) + }) { + let originalRelease; + + if (ref.startsWith('track:')) { + const refDirectory = ref.slice('track:'.length); + originalRelease = + trackData.find(track => track.directory === refDirectory); + } else { + const refName = ref.toLowerCase(); + originalRelease = + trackData.find(track => + track.name.toLowerCase() === refName && + track !== thisTrack && + !CacheableObject.getUpdateValue(track, 'originalReleaseTrack')); + } + + return continuation({['#originalRelease']: originalRelease}); + }, }, exitWithoutDependency({ -- cgit 1.3.0-6-gf8a5 From 237cfda2fffef391d9a3bf0f05b9af39a170ca4d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 18:28:25 -0400 Subject: find: support filtering which things are included for matching --- src/find.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/find.js b/src/find.js index 8c9413b7..d8c08ec1 100644 --- a/src/find.js +++ b/src/find.js @@ -16,6 +16,8 @@ function warnOrThrow(mode, message) { } export function processAllAvailableMatches(data, { + include = thing => true, + getMatchableNames = thing => (Object.hasOwn(thing, 'name') ? [thing.name] @@ -26,6 +28,10 @@ export function processAllAvailableMatches(data, { const multipleNameMatches = Object.create(null); for (const thing of data) { + if (!include(thing)) continue; + + byDirectory[thing.directory] = thing; + for (const name of getMatchableNames(thing)) { if (typeof name !== 'string') { logWarn`Unexpected ${typeAppearance(name)} returned in names for ${inspect(thing)}`; @@ -33,6 +39,7 @@ export function processAllAvailableMatches(data, { } const normalizedName = name.toLowerCase(); + if (normalizedName in byName) { const alreadyMatchesByName = byName[normalizedName]; byName[normalizedName] = null; @@ -45,8 +52,6 @@ export function processAllAvailableMatches(data, { byName[normalizedName] = thing; } } - - byDirectory[thing.directory] = thing; } return {byName, byDirectory, multipleNameMatches}; @@ -55,6 +60,7 @@ export function processAllAvailableMatches(data, { function findHelper({ referenceTypes, + include = undefined, getMatchableNames = undefined, }) { const keyRefRegex = @@ -84,6 +90,7 @@ function findHelper({ if (!subcache) { subcache = processAllAvailableMatches(data, { + include, getMatchableNames, }); -- cgit 1.3.0-6-gf8a5 From 7841f97cde6182359bb3f2b0f6117c6379ef18dc Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 18:29:22 -0400 Subject: data, find: use clean-logic, cached find.trackOriginalReleasesOnly --- .../things/track/withAlwaysReferenceByDirectory.js | 60 +++++++--------------- src/find.js | 18 +++++++ 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index 52d72124..fac8e213 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -2,22 +2,15 @@ // just to the track's name, which means you don't have to always reference // some *other* (much more commonly referenced) track by directory instead // of more naturally by name. -// -// See the implementation for an important caveat about matching the original -// track against other tracks, which uses a custom implementation pulling (and -// duplicating) details from #find instead of using withOriginalRelease and the -// usual withResolvedReference / find.track() utilities. -// import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; import {isBoolean} from '#validators'; import {exitWithoutDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; - -// TODO: Kludge. (The usage of this, not so much the import.) -import CacheableObject from '../../../things/cacheable-object.js'; +import {withResolvedReference} from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, @@ -44,38 +37,23 @@ export default templateCompositeFrom({ value: input.value(false), }), - // "Slow" / uncached, manual search from trackData (with this track - // excluded). Otherwise there end up being pretty bad recursion issues - // (track1.alwaysReferencedByDirectory depends on searching through data - // including track2, which depends on evaluating track2.alwaysReferenced- - // ByDirectory, which depends on searcing through data including track1...) - // That said, this is 100% a kludge, since it involves duplicating find - // logic on a completely unrelated context. - { - dependencies: [input.myself(), 'trackData', 'originalReleaseTrack'], - compute(continuation, { - [input.myself()]: thisTrack, - ['trackData']: trackData, - ['originalReleaseTrack']: ref, - }) { - let originalRelease; - - if (ref.startsWith('track:')) { - const refDirectory = ref.slice('track:'.length); - originalRelease = - trackData.find(track => track.directory === refDirectory); - } else { - const refName = ref.toLowerCase(); - originalRelease = - trackData.find(track => - track.name.toLowerCase() === refName && - track !== thisTrack && - !CacheableObject.getUpdateValue(track, 'originalReleaseTrack')); - } - - return continuation({['#originalRelease']: originalRelease}); - }, - }, + // It's necessary to use the custom trackOriginalReleasesOnly find function + // here, so as to avoid recursion issues - the find.track() function depends + // on accessing each track's alwaysReferenceByDirectory, which means it'll + // hit *this track* - and thus this step - and end up recursing infinitely. + // By definition, find.trackOriginalReleasesOnly excludes tracks which have + // an originalReleaseTrack update value set, which means even though it does + // still access each of tracks' `alwaysReferenceByDirectory` property, it + // won't access that of *this* track - it will never proceed past the + // `exitWithoutDependency` step directly above, so there's no opportunity + // for recursion. + withResolvedReference({ + ref: 'originalReleaseTrack', + data: 'trackData', + find: input.value(find.trackOriginalReleasesOnly), + }).outputs({ + '#resolvedReference': '#originalRelease', + }), exitWithoutDependency({ dependency: '#originalRelease', diff --git a/src/find.js b/src/find.js index d8c08ec1..dfcaa9aa 100644 --- a/src/find.js +++ b/src/find.js @@ -2,6 +2,7 @@ import {inspect} from 'node:util'; import {colors, logWarn} from '#cli'; import {typeAppearance} from '#sugar'; +import {CacheableObject} from '#things'; function warnOrThrow(mode, message) { if (mode === 'error') { @@ -185,6 +186,22 @@ const find = { ? [] : [track.name]), }), + + trackOriginalReleasesOnly: findHelper({ + referenceTypes: ['track'], + + include: track => + !CacheableObject.getUpdateValue(track, 'originalReleaseTrack'), + + // It's still necessary to check alwaysReferenceByDirectory here, since it + // may be set manually (with the `Always Reference By Directory` field), and + // these shouldn't be matched by name (as per usual). See the definition for + // that property for more information. + getMatchableNames: track => + (track.alwaysReferenceByDirectory + ? [] + : [track.name]), + }), }; export default find; @@ -207,6 +224,7 @@ export function bindFind(wikiData, opts1) { newsEntry: 'newsData', staticPage: 'staticPageData', track: 'trackData', + trackOriginalReleasesOnly: 'trackData', }).map(([key, value]) => { const findFn = find[key]; const thingData = wikiData[value]; -- cgit 1.3.0-6-gf8a5 From 42908282b410077eb7b01325a5257f5441dd0873 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 18:22:51 -0400 Subject: upd8: separate precacheCommonData step for better perf isolation --- src/upd8.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 59be9473..15247878 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -132,6 +132,9 @@ async function main() { linkWikiDataArrays: {...defaultStepStatus, name: `link wiki data arrays`}, + precacheCommonData: + {...defaultStepStatus, name: `precache common data`}, + filterDuplicateDirectories: {...defaultStepStatus, name: `filter duplicate directories`}, @@ -141,8 +144,8 @@ async function main() { sortWikiDataArrays: {...defaultStepStatus, name: `sort wiki data arrays`}, - precacheData: - {...defaultStepStatus, name: `precache data`}, + precacheAllData: + {...defaultStepStatus, name: `precache nearly all data`}, loadInternalDefaultLanguage: {...defaultStepStatus, name: `load internal default language`}, @@ -462,7 +465,7 @@ async function main() { const showAggregateTraces = cliOptions['show-traces'] ?? false; - const precacheData = cliOptions['precache-data'] ?? false; + const precacheAllData = cliOptions['precache-data'] ?? false; const showInvalidPropertyAccesses = cliOptions['show-invalid-property-accesses'] ?? false; // Makes writing nicer on the CPU and file I/O parts of the OS, with a @@ -525,8 +528,8 @@ async function main() { }); } - if (!precacheData) { - Object.assign(stepStatusSummary.precacheData, { + if (!precacheAllData) { + Object.assign(stepStatusSummary.precacheAllData, { status: STATUS_NOT_APPLICABLE, annotation: `--precache-data not provided`, }); @@ -866,6 +869,72 @@ async function main() { timeEnd: Date.now(), }); + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); + + const commonDataMap = { + albumData: new Set([ + // Needed for sorting + 'date', 'tracks', + // Needed for computing page paths + 'commentary', + ]), + + artTagData: new Set([ + // Needed for computing page paths + 'isContentWarning', + ]), + + artistAliasData: new Set([ + // Needed for computing page paths + 'aliasedArtist', + ]), + + flashData: new Set([ + // Needed for sorting + 'act', 'date', + ]), + + flashActData: new Set([ + // Needed for sorting + 'flashes', + ]), + + groupData: new Set([ + // Needed for computing page paths + 'albums', + ]), + + listingSpec: new Set([ + // Needed for computing page paths + 'contentFunction', 'featureFlag', + ]), + + trackData: new Set([ + // Needed for sorting + 'album', 'date', + // Needed for computing page paths + 'commentary', + ]), + }; + + for (const [wikiDataKey, properties] of Object.entries(commonDataMap)) { + const thingData = wikiData[wikiDataKey]; + const allProperties = new Set(['name', 'directory', ...properties]); + for (const thing of thingData) { + for (const property of allProperties) { + void thing[property]; + } + } + } + + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + // Filter out any things with duplicate directories throughout the data, // warning about them too. @@ -952,8 +1021,8 @@ async function main() { timeEnd: Date.now(), }); - if (precacheData) { - Object.assign(stepStatusSummary.precacheData, { + if (precacheAllData) { + Object.assign(stepStatusSummary.precacheAllData, { status: STATUS_STARTED_NOT_DONE, timeStart: Date.now(), }); @@ -970,7 +1039,7 @@ async function main() { .flatMap(([_key, things]) => things) .map(thing => () => CacheableObject.cacheAllExposedProperties(thing))); - Object.assign(stepStatusSummary.precacheData, { + Object.assign(stepStatusSummary.precacheAllData, { status: STATUS_DONE_CLEAN, timeEnd: Date.now(), }); @@ -979,7 +1048,7 @@ async function main() { if (noBuild) { displayCompositeCacheAnalysis(); - if (precacheData) { + if (precacheAllData) { return true; } } -- cgit 1.3.0-6-gf8a5 From ea971431bb08641e26c399edd1bc74b838931b02 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 18:26:20 -0400 Subject: upd8: nicer uncaught error-with-cause logging --- src/upd8.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/upd8.js b/src/upd8.js index 15247878..7cb42027 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1428,6 +1428,9 @@ if (true || isMain(import.meta.url) || path.basename(process.argv[1]) === 'hsmus } catch (error) { if (error instanceof AggregateError) { showAggregate(error); + } else if (error.cause) { + console.error(error); + showAggregate(error); } else { console.error(error); } -- cgit 1.3.0-6-gf8a5 From 1704fa016ba81318e92ef59951f4a9b3ffc4d091 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 5 Nov 2023 18:48:08 -0400 Subject: upd8: replace --precache-data with --precache-mode --- src/upd8.js | 186 ++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 80 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 7cb42027..408ad884 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -335,18 +335,18 @@ async function main() { type: 'flag', }, - // Compute ALL data properties before moving on to building. This ensures - // writes are processed at a stable speed (since they don't have to perform - // any additional data computation besides what is done for the page - // itself), but it'll also take a long while for the initial caching to - // complete. This shouldn't have any overall difference on efficiency as - // it's the same amount of processing being done regardless; the option is - // mostly present for optimization testing (i.e. if you want to focus on - // efficiency of data calculation or write generation separately instead of - // mixed together). - 'precache-data': { - help: `Compute all runtime-cached values for wiki data objects before proceeding to site build (optimizes rate of content generation/serving, but waits a lot longer before build actually starts, and may compute data which is never required for this build)`, - type: 'flag', + 'precache-mode': { + help: + `Change the way certain runtime-computed values are preemptively evaluated and cached\n\n` + + `common: Preemptively compute certain properties which are needed for basic data loading and site generation\n\n` + + `all: Compute every visible data property, optimizing rate of content generation, but causing a long stall before the build actually starts\n\n` + + `none: Don't preemptively compute any values - strictly the most efficient, but may result in unpredictably "lopsided" performance for individual steps of loading data and building the site\n\n` + + `Defaults to 'common'`, + type: 'value', + validate(value) { + if (['common', 'all', 'none'].includes(value)) return true; + return 'common, all, or none'; + }, }, }; @@ -465,7 +465,7 @@ async function main() { const showAggregateTraces = cliOptions['show-traces'] ?? false; - const precacheAllData = cliOptions['precache-data'] ?? false; + const precacheMode = cliOptions['precache-mode'] ?? 'common'; const showInvalidPropertyAccesses = cliOptions['show-invalid-property-accesses'] ?? false; // Makes writing nicer on the CPU and file I/O parts of the OS, with a @@ -528,11 +528,35 @@ async function main() { }); } - if (!precacheAllData) { - Object.assign(stepStatusSummary.precacheAllData, { - status: STATUS_NOT_APPLICABLE, - annotation: `--precache-data not provided`, - }); + switch (precacheMode) { + case 'common': + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is common, not all`, + }); + + break; + + case 'all': + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is all, not common`, + }); + + break; + + case 'none': + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is none`, + }); + + Object.assign(stepStatusSummary.precacheAllData, { + status: STATUS_NOT_APPLICABLE, + annotation: `--precache-mode is none`, + }); + + break; } if (!langPath) { @@ -869,71 +893,73 @@ async function main() { timeEnd: Date.now(), }); - Object.assign(stepStatusSummary.precacheCommonData, { - status: STATUS_STARTED_NOT_DONE, - timeStart: Date.now(), - }); + if (precacheMode === 'common') { + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_STARTED_NOT_DONE, + timeStart: Date.now(), + }); - const commonDataMap = { - albumData: new Set([ - // Needed for sorting - 'date', 'tracks', - // Needed for computing page paths - 'commentary', - ]), - - artTagData: new Set([ - // Needed for computing page paths - 'isContentWarning', - ]), - - artistAliasData: new Set([ - // Needed for computing page paths - 'aliasedArtist', - ]), - - flashData: new Set([ - // Needed for sorting - 'act', 'date', - ]), - - flashActData: new Set([ - // Needed for sorting - 'flashes', - ]), - - groupData: new Set([ - // Needed for computing page paths - 'albums', - ]), - - listingSpec: new Set([ - // Needed for computing page paths - 'contentFunction', 'featureFlag', - ]), - - trackData: new Set([ - // Needed for sorting - 'album', 'date', - // Needed for computing page paths - 'commentary', - ]), - }; + const commonDataMap = { + albumData: new Set([ + // Needed for sorting + 'date', 'tracks', + // Needed for computing page paths + 'commentary', + ]), + + artTagData: new Set([ + // Needed for computing page paths + 'isContentWarning', + ]), + + artistAliasData: new Set([ + // Needed for computing page paths + 'aliasedArtist', + ]), + + flashData: new Set([ + // Needed for sorting + 'act', 'date', + ]), + + flashActData: new Set([ + // Needed for sorting + 'flashes', + ]), + + groupData: new Set([ + // Needed for computing page paths + 'albums', + ]), + + listingSpec: new Set([ + // Needed for computing page paths + 'contentFunction', 'featureFlag', + ]), + + trackData: new Set([ + // Needed for sorting + 'album', 'date', + // Needed for computing page paths + 'commentary', + ]), + }; - for (const [wikiDataKey, properties] of Object.entries(commonDataMap)) { - const thingData = wikiData[wikiDataKey]; - const allProperties = new Set(['name', 'directory', ...properties]); - for (const thing of thingData) { - for (const property of allProperties) { - void thing[property]; + for (const [wikiDataKey, properties] of Object.entries(commonDataMap)) { + const thingData = wikiData[wikiDataKey]; + const allProperties = new Set(['name', 'directory', ...properties]); + for (const thing of thingData) { + for (const property of allProperties) { + void thing[property]; + } } } - } - Object.assign(stepStatusSummary.precacheCommonData, { - status: STATUS_DONE_CLEAN, - timeEnd: Date.now(), - }); + Object.assign(stepStatusSummary.precacheCommonData, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + } // Filter out any things with duplicate directories throughout the data, // warning about them too. @@ -1021,7 +1047,7 @@ async function main() { timeEnd: Date.now(), }); - if (precacheAllData) { + if (precacheMode === 'all') { Object.assign(stepStatusSummary.precacheAllData, { status: STATUS_STARTED_NOT_DONE, timeStart: Date.now(), @@ -1048,7 +1074,7 @@ async function main() { if (noBuild) { displayCompositeCacheAnalysis(); - if (precacheAllData) { + if (precacheMode === 'all') { return true; } } -- cgit 1.3.0-6-gf8a5 From 9f37683838ffc9f04b4e705d382a101b0b422412 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 15:33:57 -0400 Subject: sugar: accept aggregateOpts and fn in either order --- src/util/sugar.js | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/util/sugar.js b/src/util/sugar.js index 3e39e98f..6718b697 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -411,6 +411,18 @@ export function aggregateThrows(errorClass) { return {[openAggregate.errorClassSymbol]: errorClass}; } +// Helper function for allowing both (fn, aggregateOpts) and (aggregateOpts, fn) +// in aggregate utilities. +function _reorganizeAggregateArguments(arg1, arg2) { + if (typeof arg1 === 'function') { + return {fn: arg1, opts: arg2 ?? {}}; + } else if (typeof arg2 === 'function') { + return {fn: arg2, opts: arg1 ?? {}}; + } else { + throw new Error(`Expected a function`); + } +} + // Performs an ordinary array map with the given function, collating into a // results array (with errored inputs filtered out) and an error aggregate. // @@ -420,15 +432,15 @@ export function aggregateThrows(errorClass) { // Note the aggregate property is the result of openAggregate(), still unclosed; // use aggregate.close() to throw the error. (This aggregate may be passed to a // parent aggregate: `parent.call(aggregate.close)`!) -export function mapAggregate(array, fn, aggregateOpts) { - return _mapAggregate('sync', null, array, fn, aggregateOpts); +export function mapAggregate(array, arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + return _mapAggregate('sync', null, array, fn, opts); } -export function mapAggregateAsync(array, fn, { - promiseAll = Promise.all.bind(Promise), - ...aggregateOpts -} = {}) { - return _mapAggregate('async', promiseAll, array, fn, aggregateOpts); +export function mapAggregateAsync(array, arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts; + return _mapAggregate('async', promiseAll, array, fn, remainingOpts); } // Helper function for mapAggregate which holds code common between sync and @@ -462,15 +474,15 @@ export function _mapAggregate(mode, promiseAll, array, fn, aggregateOpts) { // inputs to a particular output. // // As with mapAggregate, the returned aggregate property is not yet closed. -export function filterAggregate(array, fn, aggregateOpts) { - return _filterAggregate('sync', null, array, fn, aggregateOpts); +export function filterAggregate(array, arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + return _filterAggregate('sync', null, array, fn, opts); } -export async function filterAggregateAsync(array, fn, { - promiseAll = Promise.all.bind(Promise), - ...aggregateOpts -} = {}) { - return _filterAggregate('async', promiseAll, array, fn, aggregateOpts); +export async function filterAggregateAsync(array, arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + const {promiseAll = Promise.all.bind(Promise), ...remainingOpts} = opts; + return _filterAggregate('async', promiseAll, array, fn, remainingOpts); } // Helper function for filterAggregate which holds code common between sync and @@ -530,20 +542,17 @@ function _filterAggregate(mode, promiseAll, array, fn, aggregateOpts) { // Totally sugar function for opening an aggregate, running the provided // function with it, then closing the function and returning the result (if // there's no throw). -export function withAggregate(aggregateOpts, fn) { - return _withAggregate('sync', aggregateOpts, fn); +export function withAggregate(arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + return _withAggregate('sync', opts, fn); } -export function withAggregateAsync(aggregateOpts, fn) { - return _withAggregate('async', aggregateOpts, fn); +export function withAggregateAsync(arg1, arg2) { + const {fn, opts} = _reorganizeAggregateArguments(arg1, arg2); + return _withAggregate('async', opts, fn); } export function _withAggregate(mode, aggregateOpts, fn) { - if (typeof aggregateOpts === 'function') { - fn = aggregateOpts; - aggregateOpts = {}; - } - const aggregate = openAggregate(aggregateOpts); if (mode === 'sync') { -- cgit 1.3.0-6-gf8a5 From 32f5dfa4b3c12dec18d0655160f9d49ca93b16d9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 15:34:52 -0400 Subject: sugar: separate annotateError functions & utilities --- src/util/sugar.js | 34 ++++++++++++++++++++-- .../composite/data/withPropertiesFromObject.js | 4 +-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/util/sugar.js b/src/util/sugar.js index 6718b697..d5f0fbd3 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -637,13 +637,31 @@ export function showAggregate(topError, { } } +export function annotateError(error, ...callbacks) { + for (const callback of callbacks) { + error = callback(error) ?? error; + } + + return error; +} + +export function annotateErrorWithIndex(error, index) { + return Object.assign(error, { + [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: + index, + + message: + `(${colors.yellow(`#${index + 1}`)}) ` + + error.message, + }); +} + export function decorateErrorWithIndex(fn) { return (x, index, array) => { try { return fn(x, index, array); } catch (error) { - error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`; - error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index; + annotateErrorWithIndex(error, index); throw error; } }; @@ -660,6 +678,18 @@ export function decorateErrorWithCause(fn, cause) { }; } +export function annotateErrorWithFile(error, file) { + return Object.assign(error, { + [Symbol.for('hsmusic.annotateError.file')]: + file, + + message: + error.message + + (error.message.includes('\n') ? '\n' : ' ') + + `(file: ${colors.bright(colors.blue(file))})`, + }); +} + export function conditionallySuppressError(conditionFn, callbackFn) { return (...args) => { try { diff --git a/test/unit/data/composite/data/withPropertiesFromObject.js b/test/unit/data/composite/data/withPropertiesFromObject.js index ead1b9b2..cb1d8d21 100644 --- a/test/unit/data/composite/data/withPropertiesFromObject.js +++ b/test/unit/data/composite/data/withPropertiesFromObject.js @@ -207,7 +207,7 @@ t.test(`withPropertiesFromObject: validate static inputs`, t => { {message: `object: Expected an object, got array`}, {message: `properties: Errors validating array items`, errors: [ { - [Symbol.for('hsmusic.decorate.indexInSourceArray')]: 2, + [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2, message: /Expected a string, got number/, }, ]}, @@ -240,7 +240,7 @@ t.test(`withPropertiesFromObject: validate dynamic inputs`, t => { {message: `object: Expected an object, got array`}, {message: `properties: Errors validating array items`, errors: [ { - [Symbol.for('hsmusic.decorate.indexInSourceArray')]: 2, + [Symbol.for('hsmusic.annotateError.indexInSourceArray')]: 2, message: /Expected a string, got number/, }, ]}, -- cgit 1.3.0-6-gf8a5 From ab306affb06b9f94a2f6b8dc8607614949b3ab0e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 15:36:57 -0400 Subject: yaml: tidy aggregate nesting and error syntax --- src/data/yaml.js | 196 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 110 insertions(+), 86 deletions(-) diff --git a/src/data/yaml.js b/src/data/yaml.js index 0ffe9682..37f31800 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -18,11 +18,11 @@ import T, { } from '#things'; import { + annotateErrorWithFile, conditionallySuppressError, decorateErrorWithIndex, empty, filterProperties, - mapAggregate, openAggregate, showAggregate, withAggregate, @@ -1119,15 +1119,30 @@ export async function loadAndProcessDataDocuments({dataPath}) { }); const wikiDataResult = {}; + const _getFileFromArgument = arg => + (typeof arg === 'object' + ? arg.file + : arg); + function decorateErrorWithFile(fn) { - return (x, index, array) => { + return (...args) => { try { - return fn(x, index, array); - } catch (error) { - error.message += - (error.message.includes('\n') ? '\n' : ' ') + - `(file: ${colors.bright(colors.blue(path.relative(dataPath, x.file)))})`; - throw error; + return fn(...args); + } catch (caughtError) { + const file = _getFileFromArgument(args[0]); + throw annotateErrorWithFile(caughtError, path.relative(dataPath, file)); + } + }; + } + + // Certified gensync moment. + function asyncDecorateErrorWithFile(fn) { + return async (...args) => { + try { + return await fn(...args); + } catch (caughtError) { + const file = _getFileFromArgument(args[0]); + throw annotateErrorWithFile(caughtError, path.relative(dataPath, file)); } }; } @@ -1135,7 +1150,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { for (const dataStep of dataSteps) { await processDataAggregate.nestAsync( {message: `Errors during data step: ${colors.bright(dataStep.title)}`}, - async ({call, callAsync, map, mapAsync, push, nest}) => { + async ({call, callAsync, map, mapAsync, push}) => { const {documentMode} = dataStep; if (!Object.values(documentModes).includes(documentMode)) { @@ -1323,8 +1338,8 @@ export async function loadAndProcessDataDocuments({dataPath}) { throw new Error(`Expected 'files' property for ${documentMode.toString()}`); } - let files = ( - typeof dataStep.files === 'function' + const filesFromDataStep = + (typeof dataStep.files === 'function' ? await callAsync(() => dataStep.files(dataPath).then( files => files, @@ -1335,101 +1350,110 @@ export async function loadAndProcessDataDocuments({dataPath}) { throw error; } })) - : dataStep.files - ); + : dataStep.files); - if (!files) { - return; - } + const filesUnderDataPath = + filesFromDataStep + .map(file => path.join(dataPath, file)); - files = files.map((file) => path.join(dataPath, file)); - - const readResults = await mapAsync( - files, - (file) => readFile(file, 'utf-8').then((contents) => ({file, contents})), - {message: `Errors reading data files`}); - - let yamlResults = map( - readResults, - decorateErrorWithFile(({file, contents}) => ({ - file, - documents: yaml.loadAll(contents), - })), - {message: `Errors parsing data files as valid YAML`}); - - yamlResults = yamlResults.map(({file, documents}) => { - const {documents: filteredDocuments, aggregate} = filterBlankDocuments(documents); - call(decorateErrorWithFile(aggregate.close), {file}); - return {file, documents: filteredDocuments}; - }); + const yamlResults = []; + + await mapAsync(filesUnderDataPath, {message: `Errors loading data files`}, + asyncDecorateErrorWithFile(async file => { + let contents; + try { + contents = await readFile(file, 'utf-8'); + } catch (caughtError) { + throw new Error(`Failed to read data file`, {cause: caughtError}); + } + + let documents; + try { + documents = yaml.loadAll(contents); + } catch (caughtError) { + throw new Error(`Failed to parse valid YAML`, {cause: caughtError}); + } + + const {documents: filteredDocuments, aggregate: filterAggregate} = + filterBlankDocuments(documents); + + try { + filterAggregate.close(); + } catch (caughtError) { + // Blank documents aren't a critical error, they're just something + // that should be noted - the (filtered) documents still get pushed. + const pathToFile = path.relative(dataPath, file); + annotateErrorWithFile(caughtError, pathToFile); + push(caughtError); + } + + yamlResults.push({file, documents: filteredDocuments}); + })); const processResults = []; switch (documentMode) { case documentModes.headerAndEntries: - map(yamlResults, decorateErrorWithFile(({documents}) => { - const headerDocument = documents[0]; - const entryDocuments = documents.slice(1).filter(Boolean); - - if (!headerDocument) - throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - - // This'll be decorated with the file, and groups together any - // errors from processing the header and entry documents. - const fileAggregate = - openAggregate({message: `Errors processing documents`}); - - const {thing: headerObject, aggregate: headerAggregate} = - dataStep.processHeaderDocument(headerDocument); - - try { - headerAggregate.close() - } catch (caughtError) { - caughtError.message = `(${colors.yellow(`header`)}) ${caughtError.message}`; - fileAggregate.push(caughtError); - } + map(yamlResults, {message: `Errors processing documents in data files`}, + decorateErrorWithFile(({documents}) => { + const headerDocument = documents[0]; + const entryDocuments = documents.slice(1).filter(Boolean); - const entryObjects = []; + if (!headerDocument) + throw new Error(`Missing header document (empty file or erroneously starting with "---"?)`); - for (let index = 0; index < entryDocuments.length; index++) { - const entryDocument = entryDocuments[index]; + withAggregate({message: `Errors processing documents`}, ({push}) => { + const {thing: headerObject, aggregate: headerAggregate} = + dataStep.processHeaderDocument(headerDocument); - const {thing: entryObject, aggregate: entryAggregate} = - dataStep.processEntryDocument(entryDocument); + try { + headerAggregate.close(); + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`header`)}) ${caughtError.message}`; + push(caughtError); + } - entryObjects.push(entryObject); + const entryObjects = []; - try { - entryAggregate.close(); - } catch (caughtError) { - caughtError.message = `(${colors.yellow(`entry #${index + 1}`)}) ${caughtError.message}`; - fileAggregate.push(caughtError); - } - } + for (let index = 0; index < entryDocuments.length; index++) { + const entryDocument = entryDocuments[index]; - processResults.push({ - header: headerObject, - entries: entryObjects, - }); + const {thing: entryObject, aggregate: entryAggregate} = + dataStep.processEntryDocument(entryDocument); - fileAggregate.close(); - }), {message: `Errors processing documents in data files`}); + entryObjects.push(entryObject); + + try { + entryAggregate.close(); + } catch (caughtError) { + caughtError.message = `(${colors.yellow(`entry #${index + 1}`)}) ${caughtError.message}`; + push(caughtError); + } + } + + processResults.push({ + header: headerObject, + entries: entryObjects, + }); + }); + })); break; case documentModes.onePerFile: - map(yamlResults, decorateErrorWithFile(({documents}) => { - if (documents.length > 1) - throw new Error(`Only expected one document to be present per file, got ${documents.length} here`); + map(yamlResults, {message: `Errors processing data files as valid documents`}, + decorateErrorWithFile(({documents}) => { + if (documents.length > 1) + throw new Error(`Only expected one document to be present per file, got ${documents.length} here`); - if (empty(documents) || !documents[0]) - throw new Error(`Expected a document, this file is empty`); + if (empty(documents) || !documents[0]) + throw new Error(`Expected a document, this file is empty`); - const {thing, aggregate} = - dataStep.processDocument(documents[0]); + const {thing, aggregate} = + dataStep.processDocument(documents[0]); - processResults.push(thing); - aggregate.close(); - }), {message: `Errors processing data files as valid documents`}); + processResults.push(thing); + aggregate.close(); + })); break; } -- cgit 1.3.0-6-gf8a5 From 8d06ad886607e7acdf636e07719bd4fbae3de767 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 16:28:37 -0400 Subject: sugar: expose and integrate async-adaptive error decorators --- src/util/sugar.js | 88 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/util/sugar.js b/src/util/sugar.js index d5f0fbd3..9646be37 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -656,28 +656,6 @@ export function annotateErrorWithIndex(error, index) { }); } -export function decorateErrorWithIndex(fn) { - return (x, index, array) => { - try { - return fn(x, index, array); - } catch (error) { - annotateErrorWithIndex(error, index); - throw error; - } - }; -} - -export function decorateErrorWithCause(fn, cause) { - return (...args) => { - try { - return fn(...args); - } catch (error) { - error.cause = cause; - throw error; - } - }; -} - export function annotateErrorWithFile(error, file) { return Object.assign(error, { [Symbol.for('hsmusic.annotateError.file')]: @@ -690,6 +668,72 @@ export function annotateErrorWithFile(error, file) { }); } +export function asyncAdaptiveDecorateError(fn, callback) { + if (typeof callback !== 'function') { + throw new Error(`Expected callback to be a function, got ${typeAppearance(callback)}`); + } + + const syncDecorated = function (...args) { + try { + return fn(...args); + } catch (caughtError) { + throw callback(caughtError, ...args); + } + }; + + const asyncDecorated = async function(...args) { + try { + return await fn(...args); + } catch (caughtError) { + throw callback(caughtError); + } + }; + + syncDecorated.async = asyncDecorated; + + return syncDecorated; +} + +export function decorateError(fn, callback) { + return asyncAdaptiveDecorateError(fn, callback); +} + +export function asyncDecorateError(fn, callback) { + return asyncAdaptiveDecorateError(fn, callback).async; +} + +export function decorateErrorWithAnnotation(fn, ...annotationCallbacks) { + return asyncAdaptiveDecorateError(fn, + (caughtError, ...args) => + annotateError(caughtError, + ...annotationCallbacks + .map(callback => error => callback(error, ...args)))); +} + +export function decorateErrorWithIndex(fn) { + return decorateErrorWithAnnotation(fn, + (caughtError, _value, index) => + annotateErrorWithIndex(caughtError, index)); +} + +export function decorateErrorWithCause(fn, cause) { + return asyncAdaptiveDecorateError(fn, + (caughtError) => + Object.assign(caughtError, {cause})); +} + +export function asyncDecorateErrorWithAnnotation(fn, ...annotationCallbacks) { + return decorateErrorWithAnnotation(fn, ...annotationCallbacks).async; +} + +export function asyncDecorateErrorWithIndex(fn) { + return decorateErrorWithIndex(fn).async; +} + +export function asyncDecorateErrorWithCause(fn, cause) { + return decorateErrorWithCause(fn, cause).async; +} + export function conditionallySuppressError(conditionFn, callbackFn) { return (...args) => { try { -- cgit 1.3.0-6-gf8a5 From 682b62b33aa6e5a4c512343d0355d32cb1c67c17 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 16:29:16 -0400 Subject: yaml: consolidate logic in async-adaptive decorateErrorWithFile --- src/data/yaml.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/data/yaml.js b/src/data/yaml.js index 37f31800..1d35bae8 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -21,6 +21,7 @@ import { annotateErrorWithFile, conditionallySuppressError, decorateErrorWithIndex, + decorateErrorWithAnnotation, empty, filterProperties, openAggregate, @@ -1119,32 +1120,20 @@ export async function loadAndProcessDataDocuments({dataPath}) { }); const wikiDataResult = {}; - const _getFileFromArgument = arg => - (typeof arg === 'object' - ? arg.file - : arg); - function decorateErrorWithFile(fn) { - return (...args) => { - try { - return fn(...args); - } catch (caughtError) { - const file = _getFileFromArgument(args[0]); - throw annotateErrorWithFile(caughtError, path.relative(dataPath, file)); - } - }; + return decorateErrorWithAnnotation(fn, + (caughtError, firstArg) => + annotateErrorWithFile( + caughtError, + path.relative( + dataPath, + (typeof firstArg === 'object' + ? firstArg.file + : firstArg)))); } - // Certified gensync moment. function asyncDecorateErrorWithFile(fn) { - return async (...args) => { - try { - return await fn(...args); - } catch (caughtError) { - const file = _getFileFromArgument(args[0]); - throw annotateErrorWithFile(caughtError, path.relative(dataPath, file)); - } - }; + return decorateErrorWithFile(fn).async; } for (const dataStep of dataSteps) { -- cgit 1.3.0-6-gf8a5 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(-) 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(-) 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(-) 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 39a2f042cd6275f06977be6fe9335bd76fb94222 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 17:53:58 -0400 Subject: upd8: fix not defaulting to internal language properly --- src/upd8.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/upd8.js b/src/upd8.js index 408ad884..b1340989 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1162,7 +1162,10 @@ async function main() { }); const customDefaultLanguage = - languages[wikiData.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; + (wikiData.wikiInfo.defaultLanguage + ? languages[wikiData.wikiInfo.defaultLanguage] + : null); + let finalDefaultLanguage; if (customDefaultLanguage) { -- 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 +++ src/upd8.js | 43 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 9 deletions(-) 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; } diff --git a/src/upd8.js b/src/upd8.js index b1340989..9714d166 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -39,7 +39,7 @@ import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; import {displayCompositeCacheAnalysis} from '#composite'; -import {processLanguageFile} from '#language'; +import {processLanguageFile, watchLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; import {empty, showAggregate, withEntries} from '#sugar'; @@ -1086,17 +1086,34 @@ async function main() { let internalDefaultLanguage; - try { - internalDefaultLanguage = - await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); + const internalDefaultLanguageWatcher = + watchLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); - Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { - status: STATUS_DONE_CLEAN, - timeEnd: Date.now(), + try { + await new Promise((resolve, reject) => { + const watcher = internalDefaultLanguageWatcher; + + const onReady = () => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + resolve(); + }; + + const onError = error => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + watcher.close(); + reject(error); + }; + + watcher.on('ready', onReady); + watcher.on('error', onError); }); - } catch (error) { - console.error(error); + internalDefaultLanguage = internalDefaultLanguageWatcher.language; + } catch (_error) { + // No need to display the error here - it's already printed by + // watchLanguageFile. logError`There was an error reading the internal language file.`; fileIssue(); @@ -1109,6 +1126,14 @@ async function main() { return false; } + // Bypass node.js special-case handling for uncaught error events + internalDefaultLanguageWatcher.on('error', () => {}); + + Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { + status: STATUS_DONE_CLEAN, + timeEnd: Date.now(), + }); + let languages; if (langPath) { -- 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(-) 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 8197804433694865567c826ec0031e8ce73cbcd4 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 18:45:16 -0400 Subject: upd8: complete integration for reloading custom languages --- src/upd8.js | 160 +++++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 50 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 9714d166..c011b660 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1134,6 +1134,7 @@ async function main() { timeEnd: Date.now(), }); + let customLanguageWatchers; let languages; if (langPath) { @@ -1147,31 +1148,77 @@ async function main() { pathStyle: 'device', }); - let results; + customLanguageWatchers = + languageDataFiles.map(file => { + const watcher = watchLanguageFile(file); - // TODO: Aggregate errors (with Promise.allSettled). - try { - results = - await progressPromiseAll(`Reading & processing language files.`, - languageDataFiles.map((file) => processLanguageFile(file))); - } catch (error) { - console.error(error); + // Bypass node.js special-case handling for uncaught error events + watcher.on('error', () => {}); - logError`Failed to load language files. Please investigate these, or don't provide`; - logError`--lang-path (or HSMUSIC_LANG) and build again.`; - - Object.assign(stepStatusSummary.loadLanguageFiles, { - status: STATUS_FATAL_ERROR, - annotation: `see log for details`, - timeEnd: Date.now(), + return watcher; }); - return false; + const waitingOnWatchers = new Set(customLanguageWatchers); + + const initialResults = + await Promise.allSettled( + customLanguageWatchers.map(watcher => + new Promise((resolve, reject) => { + const onReady = () => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + waitingOnWatchers.delete(watcher); + resolve(); + }; + + const onError = error => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + reject(error); + }; + + watcher.on('ready', onReady); + watcher.on('error', onError); + }))); + + if (initialResults.some(({status}) => status === 'rejected')) { + logWarn`There were errors loading custom languages from the language path`; + logWarn`provided: ${langPath}`; + + if (noInput) { + logError`Failed to load language files. Please investigate these, or don't provide`; + logError`--lang-path (or HSMUSIC_LANG) and build again.`; + + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_FATAL_ERROR, + annotation: `see log for details`, + timeEnd: Date.now(), + }); + + return false; + } + + logWarn`The build should start automatically if you investigate these.`; + logWarn`Or, exit by pressing ^C here (control+C) and run again without`; + logWarn`providing ${'--lang-path'} (or ${'HSMUSIC_LANG'}) to build without custom`; + logWarn`languages.`; + + await new Promise(resolve => { + for (const watcher of waitingOnWatchers) { + watcher.once('ready', () => { + waitingOnWatchers.remove(watcher); + if (empty(waitingOnWatchers)) { + resolve(); + } + }); + } + }); } languages = Object.fromEntries( - results.map((language) => [language.code, language])); + customLanguageWatchers + .map(watcher => [watcher.language.code, watcher.language])); Object.assign(stepStatusSummary.loadLanguageFiles, { status: STATUS_DONE_CLEAN, @@ -1192,54 +1239,67 @@ async function main() { : null); let finalDefaultLanguage; + let finalDefaultLanguageWatcher; + let finalDefaultLanguageAnnotation; - if (customDefaultLanguage) { - logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; - customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; - finalDefaultLanguage = customDefaultLanguage; + if (wikiData.wikiInfo.defaultLanguage) { + const customDefaultLanguage = languages[wikiData.wikiInfo.defaultLanguage]; - Object.assign(stepStatusSummary.initializeDefaultLanguage, { - status: STATUS_DONE_CLEAN, - annotation: `using wiki-specified custom default language`, - timeEnd: Date.now(), - }); - } else if (wikiData.wikiInfo.defaultLanguage) { - logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; - if (langPath) { - logError`Check if an appropriate file exists in ${langPath}?`; - } else { - logError`Be sure to specify ${'--lang-path'} or ${'HSMUSIC_LANG'} with the path to language files.`; + if (!customDefaultLanguage) { + logError`Wiki info file specified default language is ${wikiData.wikiInfo.defaultLanguage}, but no such language file exists!`; + if (langPath) { + logError`Check if an appropriate file exists in ${langPath}?`; + } else { + logError`Be sure to specify ${'--lang-path'} or ${'HSMUSIC_LANG'} with the path to language files.`; + } + + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_FATAL_ERROR, + annotation: `wiki specifies default language whose file is not available`, + timeEnd: Date.now(), + }); + + return false; } - Object.assign(stepStatusSummary.initializeDefaultLanguage, { - status: STATUS_FATAL_ERROR, - annotation: `wiki specifies default language whose file is not available`, - timeEnd: Date.now(), - }); + customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; - return false; + logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; + + finalDefaultLanguage = customDefaultLanguage; + finalDefaultLanguageWatcher = + customLanguageWatchers.find(({language}) => language === customDefaultLanguage); + finalDefaultLanguageAnnotation = `using wiki-specified custom default language`; } else { languages[internalDefaultLanguage.code] = internalDefaultLanguage; - finalDefaultLanguage = internalDefaultLanguage; - stepStatusSummary.initializeDefaultLanguage.status = STATUS_DONE_CLEAN; - Object.assign(stepStatusSummary.initializeDefaultLanguage, { - status: STATUS_DONE_CLEAN, - annotation: `no custom default language specified`, - timeEnd: Date.now(), - }); + finalDefaultLanguage = internalDefaultLanguage; + finalDefaultLanguageWatcher = internalDefaultLanguageWatcher; + finalDefaultLanguageAnnotation = `no custom default language specified`; } - for (const language of Object.values(languages)) { - if (language === finalDefaultLanguage) { - continue; + const inheritStringsFromDefaultLanguage = () => { + const {strings: inheritedStrings} = finalDefaultLanguage; + for (const language of Object.values(languages)) { + if (language === finalDefaultLanguage) continue; + Object.assign(language, {inheritedStrings}); } + }; - language.inheritedStrings = finalDefaultLanguage.strings; - } + inheritStringsFromDefaultLanguage(); + + finalDefaultLanguageWatcher.on('update', () => { + inheritStringsFromDefaultLanguage(); + }); logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`; + Object.assign(stepStatusSummary.initializeDefaultLanguage, { + status: STATUS_DONE_CLEAN, + annotation: finalDefaultLanguageAnnotation, + timeEnd: Date.now(), + }); + const urls = generateURLs(urlSpec); Object.assign(stepStatusSummary.verifyImagePaths, { -- 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(-) 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 fade91f5f20a68ea8ffad6f151ff9c9bd8b19736 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 19:06:50 -0400 Subject: upd8: basic port strings-default.json -> strings-default.yaml --- src/strings-default.json | 513 ----------------------------------------- src/strings-default.yaml | 585 +++++++++++++++++++++++++++++++++++++++++++++++ src/upd8.js | 2 +- 3 files changed, 586 insertions(+), 514 deletions(-) delete mode 100644 src/strings-default.json create mode 100644 src/strings-default.yaml diff --git a/src/strings-default.json b/src/strings-default.json deleted file mode 100644 index b6471bdf..00000000 --- a/src/strings-default.json +++ /dev/null @@ -1,513 +0,0 @@ -{ - "meta.languageCode": "en", - "meta.languageName": "English", - "count.tracks": "{TRACKS}", - "count.tracks.withUnit.zero": "", - "count.tracks.withUnit.one": "{TRACKS} track", - "count.tracks.withUnit.two": "", - "count.tracks.withUnit.few": "", - "count.tracks.withUnit.many": "", - "count.tracks.withUnit.other": "{TRACKS} tracks", - "count.additionalFiles": "{FILES}", - "count.additionalFiles.withUnit.zero": "", - "count.additionalFiles.withUnit.one": "{FILES} file", - "count.additionalFiles.withUnit.two": "", - "count.additionalFiles.withUnit.few": "", - "count.additionalFiles.withUnit.many": "", - "count.additionalFiles.withUnit.other": "{FILES} files", - "count.albums": "{ALBUMS}", - "count.albums.withUnit.zero": "", - "count.albums.withUnit.one": "{ALBUMS} album", - "count.albums.withUnit.two": "", - "count.albums.withUnit.few": "", - "count.albums.withUnit.many": "", - "count.albums.withUnit.other": "{ALBUMS} albums", - "count.artworks": "{ARTWORKS}", - "count.artworks.withUnit.zero": "", - "count.artworks.withUnit.one": "{ARTWORKS} artwork", - "count.artworks.withUnit.two": "", - "count.artworks.withUnit.few": "", - "count.artworks.withUnit.many": "", - "count.artworks.withUnit.other": "{ARTWORKS} artworks", - "count.commentaryEntries": "{ENTRIES}", - "count.commentaryEntries.withUnit.zero": "", - "count.commentaryEntries.withUnit.one": "{ENTRIES} entry", - "count.commentaryEntries.withUnit.two": "", - "count.commentaryEntries.withUnit.few": "", - "count.commentaryEntries.withUnit.many": "", - "count.commentaryEntries.withUnit.other": "{ENTRIES} entries", - "count.contributions": "{CONTRIBUTIONS}", - "count.contributions.withUnit.zero": "", - "count.contributions.withUnit.one": "{CONTRIBUTIONS} contribution", - "count.contributions.withUnit.two": "", - "count.contributions.withUnit.few": "", - "count.contributions.withUnit.many": "", - "count.contributions.withUnit.other": "{CONTRIBUTIONS} contributions", - "count.coverArts": "{COVER_ARTS}", - "count.coverArts.withUnit.zero": "", - "count.coverArts.withUnit.one": "{COVER_ARTS} cover art", - "count.coverArts.withUnit.two": "", - "count.coverArts.withUnit.few": "", - "count.coverArts.withUnit.many": "", - "count.coverArts.withUnit.other": "{COVER_ARTS} cover arts", - "count.flashes": "{FLASHES}", - "count.flashes.withUnit.zero": "", - "count.flashes.withUnit.one": "{FLASHES} flashes & games", - "count.flashes.withUnit.two": "", - "count.flashes.withUnit.few": "", - "count.flashes.withUnit.many": "", - "count.flashes.withUnit.other": "{FLASHES} flashes & games", - "count.timesReferenced": "{TIMES_REFERENCED}", - "count.timesReferenced.withUnit.zero": "", - "count.timesReferenced.withUnit.one": "{TIMES_REFERENCED} time referenced", - "count.timesReferenced.withUnit.two": "", - "count.timesReferenced.withUnit.few": "", - "count.timesReferenced.withUnit.many": "", - "count.timesReferenced.withUnit.other": "{TIMES_REFERENCED} times referenced", - "count.words": "{WORDS}", - "count.words.thousand": "{WORDS}k", - "count.words.withUnit.zero": "", - "count.words.withUnit.one": "{WORDS} word", - "count.words.withUnit.two": "", - "count.words.withUnit.few": "", - "count.words.withUnit.many": "", - "count.words.withUnit.other": "{WORDS} words", - "count.timesUsed": "{TIMES_USED}", - "count.timesUsed.withUnit.zero": "", - "count.timesUsed.withUnit.one": "used {TIMES_USED} time", - "count.timesUsed.withUnit.two": "", - "count.timesUsed.withUnit.few": "", - "count.timesUsed.withUnit.many": "", - "count.timesUsed.withUnit.other": "used {TIMES_USED} times", - "count.index.zero": "", - "count.index.one": "{INDEX}st", - "count.index.two": "{INDEX}nd", - "count.index.few": "{INDEX}rd", - "count.index.many": "", - "count.index.other": "{INDEX}th", - "count.duration.hours": "{HOURS}:{MINUTES}:{SECONDS}", - "count.duration.hours.withUnit": "{HOURS}:{MINUTES}:{SECONDS} hours", - "count.duration.minutes": "{MINUTES}:{SECONDS}", - "count.duration.minutes.withUnit": "{MINUTES}:{SECONDS} minutes", - "count.duration.approximate": "~{DURATION}", - "count.duration.missing": "_:__", - "count.fileSize.terabytes": "{TERABYTES} TB", - "count.fileSize.gigabytes": "{GIGABYTES} GB", - "count.fileSize.megabytes": "{MEGABYTES} MB", - "count.fileSize.kilobytes": "{KILOBYTES} kB", - "count.fileSize.bytes": "{BYTES} bytes", - "releaseInfo.by": "By {ARTISTS}.", - "releaseInfo.from": "From {ALBUM}.", - "releaseInfo.coverArtBy": "Cover art by {ARTISTS}.", - "releaseInfo.wallpaperArtBy": "Wallpaper art by {ARTISTS}.", - "releaseInfo.bannerArtBy": "Banner art by {ARTISTS}.", - "releaseInfo.released": "Released {DATE}.", - "releaseInfo.artReleased": "Art released {DATE}.", - "releaseInfo.addedToWiki": "Added to wiki {DATE}.", - "releaseInfo.duration": "Duration: {DURATION}.", - "releaseInfo.viewCommentary": "View {LINK}!", - "releaseInfo.viewCommentary.link": "commentary page", - "releaseInfo.viewGallery": "View {LINK}!", - "releaseInfo.viewGallery.link": "gallery page", - "releaseInfo.viewGalleryOrCommentary": "View {GALLERY} or {COMMENTARY}!", - "releaseInfo.viewGalleryOrCommentary.gallery": "gallery page", - "releaseInfo.viewGalleryOrCommentary.commentary": "commentary page", - "releaseInfo.viewOriginalFile": "View {LINK}.", - "releaseInfo.viewOriginalFile.withSize": "View {LINK} ({SIZE}).", - "releaseInfo.viewOriginalFile.link": "original file", - "releaseInfo.viewOriginalFile.sizeWarning": "(Heads up! If you're on a mobile plan, this is a large download.)", - "releaseInfo.listenOn": "Listen on {LINKS}.", - "releaseInfo.listenOn.noLinks": "This wiki doesn't have any listening links for {NAME}.", - "releaseInfo.visitOn": "Visit on {LINKS}.", - "releaseInfo.playOn": "Play on {LINKS}.", - "releaseInfo.readCommentary": "Read {LINK}.", - "releaseInfo.readCommentary.link": "artist commentary", - "releaseInfo.alsoReleasedAs": "Also released as:", - "releaseInfo.alsoReleasedAs.item": "{TRACK} (on {ALBUM})", - "releaseInfo.contributors": "Contributors:", - "releaseInfo.tracksReferenced": "Tracks that {TRACK} references:", - "releaseInfo.tracksThatReference": "Tracks that reference {TRACK}:", - "releaseInfo.tracksSampled": "Tracks that {TRACK} samples:", - "releaseInfo.tracksThatSample": "Tracks that sample {TRACK}:", - "releaseInfo.flashesThatFeature": "Flashes & games that feature {TRACK}:", - "releaseInfo.flashesThatFeature.item": "{FLASH}", - "releaseInfo.flashesThatFeature.item.asDifferentRelease": "{FLASH} (as {TRACK})", - "releaseInfo.tracksFeatured": "Tracks that {FLASH} features:", - "releaseInfo.lyrics": "Lyrics:", - "releaseInfo.artistCommentary": "Artist commentary:", - "releaseInfo.artistCommentary.seeOriginalRelease": "See {ORIGINAL}!", - "releaseInfo.artTags": "Tags:", - "releaseInfo.artTags.inline": "Tags: {TAGS}", - "releaseInfo.additionalFiles.shortcut": "View {ANCHOR_LINK}: {TITLES}", - "releaseInfo.additionalFiles.shortcut.anchorLink": "additional files", - "releaseInfo.additionalFiles.heading": "View or download {ADDITIONAL_FILES}:", - "releaseInfo.additionalFiles.entry": "{TITLE}", - "releaseInfo.additionalFiles.entry.withDescription": "{TITLE}: {DESCRIPTION}", - "releaseInfo.additionalFiles.file": "{FILE}", - "releaseInfo.additionalFiles.file.withSize": "{FILE} ({SIZE})", - "releaseInfo.sheetMusicFiles.shortcut": "Download {LINK}.", - "releaseInfo.sheetMusicFiles.shortcut.link": "sheet music files", - "releaseInfo.sheetMusicFiles.heading": "Print or download sheet music files:", - "releaseInfo.midiProjectFiles.shortcut": "Download {LINK}.", - "releaseInfo.midiProjectFiles.shortcut.link": "MIDI/project files", - "releaseInfo.midiProjectFiles.heading": "Download MIDI/project files:", - "releaseInfo.note": "Context notes:", - "trackList.section.withDuration": "{SECTION} ({DURATION}):", - "trackList.group": "From {GROUP}:", - "trackList.group.fromOther": "From somewhere else:", - "trackList.item.withDuration": "({DURATION}) {TRACK}", - "trackList.item.withDuration.withArtists": "({DURATION}) {TRACK} {BY}", - "trackList.item.withArtists": "{TRACK} {BY}", - "trackList.item.withArtists.by": "by {ARTISTS}", - "trackList.item.rerelease": "{TRACK} (re-release)", - "misc.alt.albumCover": "album cover", - "misc.alt.albumBanner": "album banner", - "misc.alt.trackCover": "track cover", - "misc.alt.artistAvatar": "artist avatar", - "misc.alt.flashArt": "flash art", - "misc.artistLink": "{ARTIST}", - "misc.artistLink.withContribution": "{ARTIST} ({CONTRIB})", - "misc.artistLink.withExternalLinks": "{ARTIST} ({LINKS})", - "misc.artistLink.withContribution.withExternalLinks": "{ARTIST} ({CONTRIB}) ({LINKS})", - "misc.chronology.seeArtistPages": "(See artist pages for chronology info!)", - "misc.chronology.heading.coverArt": "{INDEX} cover art by {ARTIST}", - "misc.chronology.heading.flash": "{INDEX} flash/game by {ARTIST}", - "misc.chronology.heading.track": "{INDEX} track by {ARTIST}", - "misc.chronology.withNavigation": "{HEADING} ({NAVIGATION})", - "misc.external.domain": "External ({DOMAIN})", - "misc.external.local": "Wiki Archive (local upload)", - "misc.external.bandcamp": "Bandcamp", - "misc.external.bandcamp.domain": "Bandcamp ({DOMAIN})", - "misc.external.deviantart": "DeviantArt", - "misc.external.instagram": "Instagram", - "misc.external.mastodon": "Mastodon", - "misc.external.mastodon.domain": "Mastodon ({DOMAIN})", - "misc.external.newgrounds": "Newgrounds", - "misc.external.patreon": "Patreon", - "misc.external.poetryFoundation": "Poetry Foundation", - "misc.external.soundcloud": "SoundCloud", - "misc.external.spotify": "Spotify", - "misc.external.tumblr": "Tumblr", - "misc.external.twitter": "Twitter", - "misc.external.wikipedia": "Wikipedia", - "misc.external.youtube": "YouTube", - "misc.external.youtube.playlist": "YouTube (playlist)", - "misc.external.youtube.fullAlbum": "YouTube (full album)", - "misc.external.flash.bgreco": "{LINK} (HQ Audio)", - "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})", - "misc.external.flash.homestuck.secret": "{LINK} (secret page)", - "misc.external.flash.youtube": "{LINK} (on any device)", - "misc.missingImage": "(This image file is missing)", - "misc.missingLinkContent": "(Missing link content)", - "misc.nav.previous": "Previous", - "misc.nav.next": "Next", - "misc.nav.info": "Info", - "misc.nav.gallery": "Gallery", - "misc.pageTitle": "{TITLE}", - "misc.pageTitle.withWikiName": "{TITLE} | {WIKI_NAME}", - "misc.skippers.skipTo": "Skip to:", - "misc.skippers.content": "Content", - "misc.skippers.sidebar": "Sidebar", - "misc.skippers.sidebar.left": "Sidebar (left)", - "misc.skippers.sidebar.right": "Sidebar (right)", - "misc.skippers.header": "Header", - "misc.skippers.footer": "Footer", - "misc.skippers.tracks": "Tracks", - "misc.skippers.art": "Artworks", - "misc.skippers.flashes": "Flashes & Games", - "misc.skippers.contributors": "Contributors", - "misc.skippers.references": "References...", - "misc.skippers.referencedBy": "Referenced by...", - "misc.skippers.samples": "Samples...", - "misc.skippers.sampledBy": "Sampled by...", - "misc.skippers.features": "Features...", - "misc.skippers.featuredIn": "Featured in...", - "misc.skippers.lyrics": "Lyrics", - "misc.skippers.sheetMusicFiles": "Sheet music files", - "misc.skippers.midiProjectFiles": "MIDI/project files", - "misc.skippers.additionalFiles": "Additional files", - "misc.skippers.commentary": "Commentary", - "misc.skippers.artistCommentary": "Commentary", - "misc.socialEmbed.heading": "{WIKI_NAME} | {HEADING}", - "misc.jumpTo": "Jump to:", - "misc.jumpTo.withLinks": "Jump to: {LINKS}.", - "misc.contentWarnings": "cw: {WARNINGS}", - "misc.contentWarnings.reveal": "click to show", - "misc.albumGrid.details": "({TRACKS}, {TIME})", - "misc.albumGrid.details.coverArtists": "(Illust. {ARTISTS})", - "misc.albumGrid.details.otherCoverArtists": "(With {ARTISTS})", - "misc.albumGrid.noCoverArt": "{ALBUM}", - "misc.albumGalleryGrid.noCoverArt": "{NAME}", - "misc.uiLanguage": "UI Language: {LANGUAGES}", - "homepage.title": "{TITLE}", - "homepage.news.title": "News", - "homepage.news.entry.viewRest": "(View rest of entry!)", - "albumSidebar.trackList.fallbackSectionName": "Track list", - "albumSidebar.trackList.group": "{GROUP}", - "albumSidebar.trackList.group.withRange": "{GROUP} ({RANGE})", - "albumSidebar.trackList.item": "{TRACK}", - "albumSidebar.groupBox.title": "{GROUP}", - "albumSidebar.groupBox.next": "Next: {ALBUM}", - "albumSidebar.groupBox.previous": "Previous: {ALBUM}", - "albumPage.title": "{ALBUM}", - "albumPage.nav.album": "{ALBUM}", - "albumPage.nav.randomTrack": "Random Track", - "albumPage.nav.gallery": "Gallery", - "albumPage.nav.commentary": "Commentary", - "albumPage.socialEmbed.heading": "{GROUP}", - "albumPage.socialEmbed.title": "{ALBUM}", - "albumPage.socialEmbed.body.withDuration": "{DURATION}.", - "albumPage.socialEmbed.body.withTracks": "{TRACKS}.", - "albumPage.socialEmbed.body.withReleaseDate": "Released {DATE}.", - "albumPage.socialEmbed.body.withDuration.withTracks": "{DURATION}, {TRACKS}.", - "albumPage.socialEmbed.body.withDuration.withReleaseDate": "{DURATION}. Released {DATE}.", - "albumPage.socialEmbed.body.withTracks.withReleaseDate": "{TRACKS}. Released {DATE}.", - "albumPage.socialEmbed.body.withDuration.withTracks.withReleaseDate": "{DURATION}, {TRACKS}. Released {DATE}.", - "albumGalleryPage.title": "{ALBUM} - Gallery", - "albumGalleryPage.statsLine": "{TRACKS} totaling {DURATION}.", - "albumGalleryPage.statsLine.withDate": "{TRACKS} totaling {DURATION}. Released {DATE}.", - "albumGalleryPage.coverArtistsLine": "All track artwork by {ARTISTS}.", - "albumGalleryPage.noTrackArtworksLine": "This album doesn't have any track artwork.", - "albumCommentaryPage.title": "{ALBUM} - Commentary", - "albumCommentaryPage.infoLine": "{WORDS} across {ENTRIES}.", - "albumCommentaryPage.nav.album": "Album: {ALBUM}", - "albumCommentaryPage.entry.title.albumCommentary": "Album commentary", - "albumCommentaryPage.entry.title.trackCommentary": "{TRACK}", - "artistPage.title": "{ARTIST}", - "artistPage.creditList.album": "{ALBUM}", - "artistPage.creditList.album.withDate": "{ALBUM} ({DATE})", - "artistPage.creditList.album.withDuration": "{ALBUM} ({DURATION})", - "artistPage.creditList.album.withDate.withDuration": "{ALBUM} ({DATE}; {DURATION})", - "artistPage.creditList.flashAct": "{ACT}", - "artistPage.creditList.flashAct.withDate": "{ACT} ({DATE})", - "artistPage.creditList.flashAct.withDateRange": "{ACT} ({DATE_RANGE})", - "artistPage.creditList.entry.track": "{TRACK}", - "artistPage.creditList.entry.track.withDuration": "({DURATION}) {TRACK}", - "artistPage.creditList.entry.album.coverArt": "(cover art)", - "artistPage.creditList.entry.album.wallpaperArt": "(wallpaper art)", - "artistPage.creditList.entry.album.bannerArt": "(banner art)", - "artistPage.creditList.entry.album.commentary": "(album commentary)", - "artistPage.creditList.entry.flash": "{FLASH}", - "artistPage.creditList.entry.rerelease": "{ENTRY} (re-release)", - "artistPage.creditList.entry.withContribution": "{ENTRY} ({CONTRIBUTION})", - "artistPage.creditList.entry.withArtists": "{ENTRY} (with {ARTISTS})", - "artistPage.creditList.entry.withArtists.withContribution": "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})", - "artistPage.contributedDurationLine": "{ARTIST} has contributed {DURATION} of music shared on this wiki.", - "artistPage.musicGroupsLine": "Contributed music to groups: {GROUPS}", - "artistPage.artGroupsLine": "Contributed art to groups: {GROUPS}", - "artistPage.groupsLine.item.withCount": "{GROUP} ({COUNT})", - "artistPage.groupsLine.item.withDuration": "{GROUP} ({DURATION})", - "artistPage.groupContributions.title.music": "Contributed music to groups:", - "artistPage.groupContributions.title.artworks": "Contributed artworks to groups:", - "artistPage.groupContributions.title.withSortButton": "{TITLE} ({SORT})", - "artistPage.groupContributions.title.sorting.count": "Sorting by count.", - "artistPage.groupContributions.title.sorting.duration": "Sorting by duration.", - "artistPage.groupContributions.item.countAccent": "({COUNT})", - "artistPage.groupContributions.item.durationAccent": "({DURATION})", - "artistPage.groupContributions.item.countDurationAccent": "({COUNT} — {DURATION})", - "artistPage.groupContributions.item.durationCountAccent": "({DURATION} — {COUNT})", - "artistPage.trackList.title": "Tracks", - "artistPage.artList.title": "Artworks", - "artistPage.flashList.title": "Flashes & Games", - "artistPage.commentaryList.title": "Commentary", - "artistPage.viewArtGallery": "View {LINK}!", - "artistPage.viewArtGallery.orBrowseList": "View {LINK}! Or browse the list:", - "artistPage.viewArtGallery.link": "art gallery", - "artistPage.nav.artist": "Artist: {ARTIST}", - "artistGalleryPage.title": "{ARTIST} - Gallery", - "artistGalleryPage.infoLine": "Contributed to {COVER_ARTS}.", - "commentaryIndex.title": "Commentary", - "commentaryIndex.infoLine": "{WORDS} across {ENTRIES}, in all.", - "commentaryIndex.albumList.title": "Choose an album:", - "commentaryIndex.albumList.item": "{ALBUM} ({WORDS} across {ENTRIES})", - "flashIndex.title": "Flashes & Games", - "flashPage.title": "{FLASH}", - "flashPage.nav.flash": "{FLASH}", - "flashSidebar.flashList.flashesInThisAct": "Flashes in this act", - "flashSidebar.flashList.entriesInThisSection": "Entries in this section", - "groupSidebar.title": "Groups", - "groupSidebar.groupList.category": "{CATEGORY}", - "groupSidebar.groupList.item": "{GROUP}", - "groupPage.nav.group": "Group: {GROUP}", - "groupInfoPage.title": "{GROUP}", - "groupInfoPage.viewAlbumGallery": "View {LINK}! Or browse the list:", - "groupInfoPage.viewAlbumGallery.link": "album gallery", - "groupInfoPage.albumList.title": "Albums", - "groupInfoPage.albumList.item": "({YEAR}) {ALBUM}", - "groupInfoPage.albumList.item.withoutYear": "{ALBUM}", - "groupInfoPage.albumList.item.withAccent": "{ITEM} {ACCENT}", - "groupInfoPage.albumList.item.otherGroupAccent": "(from {GROUP})", - "groupGalleryPage.title": "{GROUP} - Gallery", - "groupGalleryPage.infoLine": "{TRACKS} across {ALBUMS}, totaling {TIME}.", - "listingIndex.title": "Listings", - "listingIndex.infoLine": "{WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}.", - "listingIndex.exploreList": "Feel free to explore any of the listings linked below and in the sidebar!", - "listingPage.target.album": "Albums", - "listingPage.target.artist": "Artists", - "listingPage.target.group": "Groups", - "listingPage.target.track": "Tracks", - "listingPage.target.tag": "Tags", - "listingPage.target.other": "Other", - "listingPage.listingsFor": "Listings for {TARGET}: {LISTINGS}", - "listingPage.seeAlso": "Also check out: {LISTINGS}", - "listingPage.listAlbums.byName.title": "Albums - by Name", - "listingPage.listAlbums.byName.title.short": "...by Name", - "listingPage.listAlbums.byName.item": "{ALBUM} ({TRACKS})", - "listingPage.listAlbums.byTracks.title": "Albums - by Tracks", - "listingPage.listAlbums.byTracks.title.short": "...by Tracks", - "listingPage.listAlbums.byTracks.item": "{ALBUM} ({TRACKS})", - "listingPage.listAlbums.byDuration.title": "Albums - by Duration", - "listingPage.listAlbums.byDuration.title.short": "...by Duration", - "listingPage.listAlbums.byDuration.item": "{ALBUM} ({DURATION})", - "listingPage.listAlbums.byDate.title": "Albums - by Date", - "listingPage.listAlbums.byDate.title.short": "...by Date", - "listingPage.listAlbums.byDate.item": "{ALBUM} ({DATE})", - "listingPage.listAlbums.byDateAdded.title.short": "...by Date Added to Wiki", - "listingPage.listAlbums.byDateAdded.title": "Albums - by Date Added to Wiki", - "listingPage.listAlbums.byDateAdded.chunk.title": "{DATE}", - "listingPage.listAlbums.byDateAdded.chunk.item": "{ALBUM}", - "listingPage.listArtists.byName.title": "Artists - by Name", - "listingPage.listArtists.byName.title.short": "...by Name", - "listingPage.listArtists.byName.item": "{ARTIST} ({CONTRIBUTIONS})", - "listingPage.listArtists.byContribs.title": "Artists - by Contributions", - "listingPage.listArtists.byContribs.title.short": "...by Contributions", - "listingPage.listArtists.byContribs.item": "{ARTIST} ({CONTRIBUTIONS})", - "listingPage.listArtists.byCommentary.title": "Artists - by Commentary Entries", - "listingPage.listArtists.byCommentary.title.short": "...by Commentary Entries", - "listingPage.listArtists.byCommentary.item": "{ARTIST} ({ENTRIES})", - "listingPage.listArtists.byDuration.title": "Artists - by Duration", - "listingPage.listArtists.byDuration.title.short": "...by Duration", - "listingPage.listArtists.byDuration.item": "{ARTIST} ({DURATION})", - "listingPage.listArtists.byLatest.title": "Artists - by Latest Contribution", - "listingPage.listArtists.byLatest.title.short": "...by Latest Contribution", - "listingPage.listArtists.byLatest.chunk.title.album": "{ALBUM} ({DATE})", - "listingPage.listArtists.byLatest.chunk.title.flash": "{FLASH} ({DATE})", - "listingPage.listArtists.byLatest.chunk.item": "{ARTIST}", - "listingPage.listArtists.byLatest.dateless.title": "These artists' contributions aren't dated:", - "listingPage.listArtists.byLatest.dateless.item": "{ARTIST}", - "listingPage.listGroups.byName.title": "Groups - by Name", - "listingPage.listGroups.byName.title.short": "...by Name", - "listingPage.listGroups.byName.item": "{GROUP} ({GALLERY})", - "listingPage.listGroups.byName.item.gallery": "Gallery", - "listingPage.listGroups.byCategory.title": "Groups - by Category", - "listingPage.listGroups.byCategory.title.short": "...by Category", - "listingPage.listGroups.byCategory.chunk.title": "{CATEGORY}", - "listingPage.listGroups.byCategory.chunk.item": "{GROUP} ({GALLERY})", - "listingPage.listGroups.byCategory.chunk.item.gallery": "Gallery", - "listingPage.listGroups.byAlbums.title": "Groups - by Albums", - "listingPage.listGroups.byAlbums.title.short": "...by Albums", - "listingPage.listGroups.byAlbums.item": "{GROUP} ({ALBUMS})", - "listingPage.listGroups.byTracks.title": "Groups - by Tracks", - "listingPage.listGroups.byTracks.title.short": "...by Tracks", - "listingPage.listGroups.byTracks.item": "{GROUP} ({TRACKS})", - "listingPage.listGroups.byDuration.title": "Groups - by Duration", - "listingPage.listGroups.byDuration.title.short": "...by Duration", - "listingPage.listGroups.byDuration.item": "{GROUP} ({DURATION})", - "listingPage.listGroups.byLatest.title": "Groups - by Latest Album", - "listingPage.listGroups.byLatest.title.short": "...by Latest Album", - "listingPage.listGroups.byLatest.item": "{GROUP} ({DATE})", - "listingPage.listTracks.byName.title": "Tracks - by Name", - "listingPage.listTracks.byName.title.short": "...by Name", - "listingPage.listTracks.byName.item": "{TRACK}", - "listingPage.listTracks.byAlbum.title": "Tracks - by Album", - "listingPage.listTracks.byAlbum.title.short": "...by Album", - "listingPage.listTracks.byAlbum.chunk.title": "{ALBUM}", - "listingPage.listTracks.byAlbum.chunk.item": "{TRACK}", - "listingPage.listTracks.byDate.title": "Tracks - by Date", - "listingPage.listTracks.byDate.title.short": "...by Date", - "listingPage.listTracks.byDate.chunk.title": "{ALBUM} ({DATE})", - "listingPage.listTracks.byDate.chunk.item": "{TRACK}", - "listingPage.listTracks.byDate.chunk.item.rerelease": "{TRACK} (re-release)", - "listingPage.listTracks.byDuration.title": "Tracks - by Duration", - "listingPage.listTracks.byDuration.title.short": "...by Duration", - "listingPage.listTracks.byDuration.item": "{TRACK} ({DURATION})", - "listingPage.listTracks.byDurationInAlbum.title": "Tracks - by Duration (in Album)", - "listingPage.listTracks.byDurationInAlbum.title.short": "...by Duration (in Album)", - "listingPage.listTracks.byDurationInAlbum.chunk.title": "{ALBUM}", - "listingPage.listTracks.byDurationInAlbum.chunk.item": "{TRACK} ({DURATION})", - "listingPage.listTracks.byTimesReferenced.title": "Tracks - by Times Referenced", - "listingPage.listTracks.byTimesReferenced.title.short": "...by Times Referenced", - "listingPage.listTracks.byTimesReferenced.item": "{TRACK} ({TIMES_REFERENCED})", - "listingPage.listTracks.inFlashes.byAlbum.title": "Tracks - in Flashes & Games (by Album)", - "listingPage.listTracks.inFlashes.byAlbum.title.short": "...in Flashes & Games (by Album)", - "listingPage.listTracks.inFlashes.byAlbum.chunk.title": "{ALBUM}", - "listingPage.listTracks.inFlashes.byAlbum.chunk.item": "{TRACK} (in {FLASHES})", - "listingPage.listTracks.inFlashes.byFlash.title": "Tracks - in Flashes & Games (by Flash)", - "listingPage.listTracks.inFlashes.byFlash.title.short": "...in Flashes & Games (by Flash)", - "listingPage.listTracks.inFlashes.byFlash.chunk.title": "{FLASH}", - "listingPage.listTracks.inFlashes.byFlash.chunk.item": "{TRACK} (from {ALBUM})", - "listingPage.listTracks.withLyrics.title": "Tracks - with Lyrics", - "listingPage.listTracks.withLyrics.title.short": "...with Lyrics", - "listingPage.listTracks.withLyrics.chunk.title": "{ALBUM}", - "listingPage.listTracks.withLyrics.chunk.title.withDate": "{ALBUM} ({DATE})", - "listingPage.listTracks.withLyrics.chunk.item": "{TRACK}", - "listingPage.listTracks.withSheetMusicFiles.title": "Tracks - with Sheet Music Files", - "listingPage.listTracks.withSheetMusicFiles.title.short": "...with Sheet Music Files", - "listingPage.listTracks.withSheetMusicFiles.chunk.title": "{ALBUM}", - "listingPage.listTracks.withSheetMusicFiles.chunk.title.withDate": "{ALBUM} ({DATE})", - "listingPage.listTracks.withSheetMusicFiles.chunk.item": "{TRACK}", - "listingPage.listTracks.withMidiProjectFiles.title": "Tracks - with MIDI & Project Files", - "listingPage.listTracks.withMidiProjectFiles.title.short": "...with MIDI & Project Files", - "listingPage.listTracks.withMidiProjectFiles.chunk.title": "{ALBUM}", - "listingPage.listTracks.withMidiProjectFiles.chunk.title.withDate": "{ALBUM} ({DATE})", - "listingPage.listTracks.withMidiProjectFiles.chunk.item": "{TRACK}", - "listingPage.listTags.byName.title": "Tags - by Name", - "listingPage.listTags.byName.title.short": "...by Name", - "listingPage.listTags.byName.item": "{TAG} ({TIMES_USED})", - "listingPage.listTags.byUses.title": "Tags - by Uses", - "listingPage.listTags.byUses.title.short": "...by Uses", - "listingPage.listTags.byUses.item": "{TAG} ({TIMES_USED})", - "listingPage.other.allSheetMusic.title": "All Sheet Music", - "listingPage.other.allSheetMusic.title.short": "All Sheet Music", - "listingPage.other.allSheetMusic.albumFiles": "Album sheet music:", - "listingPage.other.allSheetMusic.file": "{TITLE}", - "listingPage.other.allSheetMusic.file.withMultipleFiles": "{TITLE} ({FILES})", - "listingPage.other.allMidiProjectFiles.title": "All MIDI/Project Files", - "listingPage.other.allMidiProjectFiles.title.short": "All MIDI/Project Files", - "listingPage.other.allMidiProjectFiles.albumFiles": "Album MIDI/project files:", - "listingPage.other.allMidiProjectFiles.file": "{TITLE}", - "listingPage.other.allMidiProjectFiles.file.withMultipleFiles": "{TITLE} ({FILES})", - "listingPage.other.allAdditionalFiles.title": "All Additional Files", - "listingPage.other.allAdditionalFiles.title.short": "All Additional Files", - "listingPage.other.allAdditionalFiles.albumFiles": "Album additional files:", - "listingPage.other.allAdditionalFiles.file": "{TITLE}", - "listingPage.other.allAdditionalFiles.file.withMultipleFiles": "{TITLE} ({FILES})", - "listingPage.other.randomPages.title": "Random Pages", - "listingPage.other.randomPages.title.short": "Random Pages", - "listingPage.other.randomPages.chooseLinkLine": "Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry.", - "listingPage.other.randomPages.dataLoadingLine": "(Data files are downloading in the background! Please wait for data to load.)", - "listingPage.other.randomPages.dataLoadedLine": "(Data files have finished being downloaded. The links should work!)", - "listingPage.other.randomPages.misc": "Miscellaneous:", - "listingPage.other.randomPages.misc.randomArtist": "Random Artist", - "listingPage.other.randomPages.misc.atLeastTwoContributions": "at least 2 contributions", - "listingPage.other.randomPages.misc.randomAlbumWholeSite": "Random Album (whole site)", - "listingPage.other.randomPages.misc.randomTrackWholeSite": "Random Track (whole site)", - "listingPage.other.randomPages.group": "From {GROUP}: ({RANDOM_ALBUM}, {RANDOM_TRACK})", - "listingPage.other.randomPages.group.randomAlbum": "Random Album", - "listingPage.other.randomPages.group.randomTrack": "Random Track", - "listingPage.other.randomPages.album": "{ALBUM}", - "listingPage.misc.trackContributors": "Track Contributors", - "listingPage.misc.artContributors": "Art Contributors", - "listingPage.misc.flashContributors": "Flash & Game Contributors", - "listingPage.misc.artAndFlashContributors": "Art & Flash Contributors", - "newsIndex.title": "News", - "newsIndex.entry.viewRest": "(View rest of entry!)", - "newsEntryPage.title": "{ENTRY}", - "newsEntryPage.published": "(Published {DATE}.)", - "redirectPage.title": "Moved to {TITLE}", - "redirectPage.infoLine": "This page has been moved to {TARGET}.", - "tagPage.title": "{TAG}", - "tagPage.infoLine": "Appears in {COVER_ARTS}.", - "tagPage.nav.tag": "Tag: {TAG}", - "trackPage.title": "{TRACK}", - "trackPage.referenceList.fandom": "Fandom:", - "trackPage.referenceList.official": "Official:", - "trackPage.nav.track": "{TRACK}", - "trackPage.nav.track.withNumber": "{NUMBER}. {TRACK}", - "trackPage.nav.random": "Random", - "trackPage.socialEmbed.heading": "{ALBUM}", - "trackPage.socialEmbed.title": "{TRACK}", - "trackPage.socialEmbed.body.withArtists.withCoverArtists": "By {ARTISTS}; art by {COVER_ARTISTS}.", - "trackPage.socialEmbed.body.withArtists": "By {ARTISTS}.", - "trackPage.socialEmbed.body.withCoverArtists": "Art by {COVER_ARTISTS}." -} diff --git a/src/strings-default.yaml b/src/strings-default.yaml new file mode 100644 index 00000000..1f16de46 --- /dev/null +++ b/src/strings-default.yaml @@ -0,0 +1,585 @@ +meta.languageCode: en +meta.languageName: English + +count.tracks: "{TRACKS}" +count.tracks.withUnit.zero: "" +count.tracks.withUnit.one: "{TRACKS} track" +count.tracks.withUnit.two: "" +count.tracks.withUnit.few: "" +count.tracks.withUnit.many: "" +count.tracks.withUnit.other: "{TRACKS} tracks" + +count.additionalFiles: "{FILES}" +count.additionalFiles.withUnit.zero: "" +count.additionalFiles.withUnit.one: "{FILES} file" +count.additionalFiles.withUnit.two: "" +count.additionalFiles.withUnit.few: "" +count.additionalFiles.withUnit.many: "" +count.additionalFiles.withUnit.other: "{FILES} files" + +count.albums: "{ALBUMS}" +count.albums.withUnit.zero: "" +count.albums.withUnit.one: "{ALBUMS} album" +count.albums.withUnit.two: "" +count.albums.withUnit.few: "" +count.albums.withUnit.many: "" +count.albums.withUnit.other: "{ALBUMS} albums" + +count.artworks: "{ARTWORKS}" +count.artworks.withUnit.zero: "" +count.artworks.withUnit.one: "{ARTWORKS} artwork" +count.artworks.withUnit.two: "" +count.artworks.withUnit.few: "" +count.artworks.withUnit.many: "" +count.artworks.withUnit.other: "{ARTWORKS} artworks" + +count.commentaryEntries: "{ENTRIES}" +count.commentaryEntries.withUnit.zero: "" +count.commentaryEntries.withUnit.one: "{ENTRIES} entry" +count.commentaryEntries.withUnit.two: "" +count.commentaryEntries.withUnit.few: "" +count.commentaryEntries.withUnit.many: "" +count.commentaryEntries.withUnit.other: "{ENTRIES} entries" + +count.contributions: "{CONTRIBUTIONS}" +count.contributions.withUnit.zero: "" +count.contributions.withUnit.one: "{CONTRIBUTIONS} contribution" +count.contributions.withUnit.two: "" +count.contributions.withUnit.few: "" +count.contributions.withUnit.many: "" +count.contributions.withUnit.other: "{CONTRIBUTIONS} contributions" + +count.coverArts: "{COVER_ARTS}" +count.coverArts.withUnit.zero: "" +count.coverArts.withUnit.one: "{COVER_ARTS} cover art" +count.coverArts.withUnit.two: "" +count.coverArts.withUnit.few: "" +count.coverArts.withUnit.many: "" +count.coverArts.withUnit.other: "{COVER_ARTS} cover arts" + +count.flashes: "{FLASHES}" +count.flashes.withUnit.zero: "" +count.flashes.withUnit.one: "{FLASHES} flashes & games" +count.flashes.withUnit.two: "" +count.flashes.withUnit.few: "" +count.flashes.withUnit.many: "" +count.flashes.withUnit.other: "{FLASHES} flashes & games" + +count.timesReferenced: "{TIMES_REFERENCED}" +count.timesReferenced.withUnit.zero: "" +count.timesReferenced.withUnit.one: "{TIMES_REFERENCED} time referenced" +count.timesReferenced.withUnit.two: "" +count.timesReferenced.withUnit.few: "" +count.timesReferenced.withUnit.many: "" +count.timesReferenced.withUnit.other: "{TIMES_REFERENCED} times referenced" + +count.words: "{WORDS}" +count.words.thousand: "{WORDS}k" +count.words.withUnit.zero: "" +count.words.withUnit.one: "{WORDS} word" +count.words.withUnit.two: "" +count.words.withUnit.few: "" +count.words.withUnit.many: "" +count.words.withUnit.other: "{WORDS} words" + +count.timesUsed: "{TIMES_USED}" +count.timesUsed.withUnit.zero: "" +count.timesUsed.withUnit.one: "used {TIMES_USED} time" +count.timesUsed.withUnit.two: "" +count.timesUsed.withUnit.few: "" +count.timesUsed.withUnit.many: "" +count.timesUsed.withUnit.other: "used {TIMES_USED} times" + +count.index.zero: "" +count.index.one: "{INDEX}st" +count.index.two: "{INDEX}nd" +count.index.few: "{INDEX}rd" +count.index.many: "" +count.index.other: "{INDEX}th" + +count.duration.hours: "{HOURS}:{MINUTES}:{SECONDS}" +count.duration.hours.withUnit: "{HOURS}:{MINUTES}:{SECONDS} hours" +count.duration.minutes: "{MINUTES}:{SECONDS}" +count.duration.minutes.withUnit: "{MINUTES}:{SECONDS} minutes" +count.duration.approximate: "~{DURATION}" +count.duration.missing: "_:__" + +count.fileSize.terabytes: "{TERABYTES} TB" +count.fileSize.gigabytes: "{GIGABYTES} GB" +count.fileSize.megabytes: "{MEGABYTES} MB" +count.fileSize.kilobytes: "{KILOBYTES} kB" +count.fileSize.bytes: "{BYTES} bytes" + +releaseInfo.by: "By {ARTISTS}." +releaseInfo.from: "From {ALBUM}." +releaseInfo.coverArtBy: "Cover art by {ARTISTS}." +releaseInfo.wallpaperArtBy: "Wallpaper art by {ARTISTS}." +releaseInfo.bannerArtBy: "Banner art by {ARTISTS}." +releaseInfo.released: "Released {DATE}." +releaseInfo.artReleased: "Art released {DATE}." +releaseInfo.addedToWiki: "Added to wiki {DATE}." +releaseInfo.duration: "Duration: {DURATION}." +releaseInfo.viewCommentary: "View {LINK}!" +releaseInfo.viewCommentary.link: "commentary page" +releaseInfo.viewGallery: "View {LINK}!" +releaseInfo.viewGallery.link: "gallery page" +releaseInfo.viewGalleryOrCommentary: "View {GALLERY} or {COMMENTARY}!" +releaseInfo.viewGalleryOrCommentary.gallery: "gallery page" +releaseInfo.viewGalleryOrCommentary.commentary: "commentary page" +releaseInfo.viewOriginalFile: "View {LINK}." +releaseInfo.viewOriginalFile.withSize: "View {LINK} ({SIZE})." +releaseInfo.viewOriginalFile.link: "original file" +releaseInfo.viewOriginalFile.sizeWarning: >- + (Heads up! If you're on a mobile plan, this is a large download.) +releaseInfo.listenOn: "Listen on {LINKS}." +releaseInfo.listenOn.noLinks: >- + This wiki doesn't have any listening links for {NAME}. +releaseInfo.visitOn: "Visit on {LINKS}." +releaseInfo.playOn: "Play on {LINKS}." +releaseInfo.readCommentary: "Read {LINK}." +releaseInfo.readCommentary.link: "artist commentary" +releaseInfo.alsoReleasedAs: "Also released as:" +releaseInfo.alsoReleasedAs.item: "{TRACK} (on {ALBUM})" +releaseInfo.contributors: "Contributors:" +releaseInfo.tracksReferenced: "Tracks that {TRACK} references:" +releaseInfo.tracksThatReference: "Tracks that reference {TRACK}:" +releaseInfo.tracksSampled: "Tracks that {TRACK} samples:" +releaseInfo.tracksThatSample: "Tracks that sample {TRACK}:" +releaseInfo.flashesThatFeature: "Flashes & games that feature {TRACK}:" +releaseInfo.flashesThatFeature.item: "{FLASH}" +releaseInfo.flashesThatFeature.item.asDifferentRelease: "{FLASH} (as {TRACK})" +releaseInfo.tracksFeatured: "Tracks that {FLASH} features:" +releaseInfo.lyrics: "Lyrics:" +releaseInfo.artistCommentary: "Artist commentary:" +releaseInfo.artistCommentary.seeOriginalRelease: "See {ORIGINAL}!" +releaseInfo.artTags: "Tags:" +releaseInfo.artTags.inline: "Tags: {TAGS}" +releaseInfo.additionalFiles.shortcut: "View {ANCHOR_LINK}: {TITLES}" +releaseInfo.additionalFiles.shortcut.anchorLink: "additional files" +releaseInfo.additionalFiles.heading: "View or download {ADDITIONAL_FILES}:" +releaseInfo.additionalFiles.entry: "{TITLE}" +releaseInfo.additionalFiles.entry.withDescription: "{TITLE}: {DESCRIPTION}" +releaseInfo.additionalFiles.file: "{FILE}" +releaseInfo.additionalFiles.file.withSize: "{FILE} ({SIZE})" +releaseInfo.sheetMusicFiles.shortcut: "Download {LINK}." +releaseInfo.sheetMusicFiles.shortcut.link: "sheet music files" +releaseInfo.sheetMusicFiles.heading: "Print or download sheet music files:" +releaseInfo.midiProjectFiles.shortcut: "Download {LINK}." +releaseInfo.midiProjectFiles.shortcut.link: "MIDI/project files" +releaseInfo.midiProjectFiles.heading: "Download MIDI/project files:" +releaseInfo.note: "Context notes:" + +trackList.section.withDuration: "{SECTION} ({DURATION}):" +trackList.group: "From {GROUP}:" +trackList.group.fromOther: "From somewhere else:" +trackList.item.withDuration: "({DURATION}) {TRACK}" +trackList.item.withDuration.withArtists: "({DURATION}) {TRACK} {BY}" +trackList.item.withArtists: "{TRACK} {BY}" +trackList.item.withArtists.by: "by {ARTISTS}" +trackList.item.rerelease: "{TRACK} (re-release)" + +misc.alt.albumCover: "album cover" +misc.alt.albumBanner: "album banner" +misc.alt.trackCover: "track cover" +misc.alt.artistAvatar: "artist avatar" +misc.alt.flashArt: "flash art" + +misc.artistLink: "{ARTIST}" +misc.artistLink.withContribution: "{ARTIST} ({CONTRIB})" +misc.artistLink.withExternalLinks: "{ARTIST} ({LINKS})" +misc.artistLink.withContribution.withExternalLinks: "{ARTIST} ({CONTRIB}) ({LINKS})" + +misc.chronology.seeArtistPages: "(See artist pages for chronology info!)" +misc.chronology.heading.coverArt: "{INDEX} cover art by {ARTIST}" +misc.chronology.heading.flash: "{INDEX} flash/game by {ARTIST}" +misc.chronology.heading.track: "{INDEX} track by {ARTIST}" +misc.chronology.withNavigation: "{HEADING} ({NAVIGATION})" + +misc.external.domain: "External ({DOMAIN})" +misc.external.local: "Wiki Archive (local upload)" +misc.external.bandcamp: "Bandcamp" +misc.external.bandcamp.domain: "Bandcamp ({DOMAIN})" +misc.external.deviantart: "DeviantArt" +misc.external.instagram: "Instagram" +misc.external.mastodon: "Mastodon" +misc.external.mastodon.domain: "Mastodon ({DOMAIN})" +misc.external.newgrounds: "Newgrounds" +misc.external.patreon: "Patreon" +misc.external.poetryFoundation: "Poetry Foundation" +misc.external.soundcloud: "SoundCloud" +misc.external.spotify: "Spotify" +misc.external.tumblr: "Tumblr" +misc.external.twitter: "Twitter" +misc.external.wikipedia: "Wikipedia" +misc.external.youtube: "YouTube" +misc.external.youtube.playlist: "YouTube (playlist)" +misc.external.youtube.fullAlbum: "YouTube (full album)" +misc.external.flash.bgreco: "{LINK} (HQ Audio)" +misc.external.flash.homestuck.page: "{LINK} (page {PAGE})" +misc.external.flash.homestuck.secret: "{LINK} (secret page)" +misc.external.flash.youtube: "{LINK} (on any device)" + +misc.missingImage: "(This image file is missing)" +misc.missingLinkContent: "(Missing link content)" + +misc.nav.previous: "Previous" +misc.nav.next: "Next" +misc.nav.info: "Info" +misc.nav.gallery: "Gallery" + +misc.pageTitle: "{TITLE}" +misc.pageTitle.withWikiName: "{TITLE} | {WIKI_NAME}" + +misc.skippers.skipTo: "Skip to:" +misc.skippers.content: "Content" +misc.skippers.sidebar: "Sidebar" +misc.skippers.sidebar.left: "Sidebar (left)" +misc.skippers.sidebar.right: "Sidebar (right)" +misc.skippers.header: "Header" +misc.skippers.footer: "Footer" +misc.skippers.tracks: "Tracks" +misc.skippers.art: "Artworks" +misc.skippers.flashes: "Flashes & Games" +misc.skippers.contributors: "Contributors" +misc.skippers.references: "References..." +misc.skippers.referencedBy: "Referenced by..." +misc.skippers.samples: "Samples..." +misc.skippers.sampledBy: "Sampled by..." +misc.skippers.features: "Features..." +misc.skippers.featuredIn: "Featured in..." +misc.skippers.lyrics: "Lyrics" +misc.skippers.sheetMusicFiles: "Sheet music files" +misc.skippers.midiProjectFiles: "MIDI/project files" +misc.skippers.additionalFiles: "Additional files" +misc.skippers.commentary: "Commentary" +misc.skippers.artistCommentary: "Commentary" + +misc.socialEmbed.heading: "{WIKI_NAME} | {HEADING}" + +misc.jumpTo: "Jump to:" +misc.jumpTo.withLinks: "Jump to: {LINKS}." + +misc.contentWarnings: "cw: {WARNINGS}" +misc.contentWarnings.reveal: "click to show" + +misc.albumGrid.details: "({TRACKS}, {TIME})" +misc.albumGrid.details.coverArtists: "(Illust. {ARTISTS})" +misc.albumGrid.details.otherCoverArtists: "(With {ARTISTS})" +misc.albumGrid.noCoverArt: "{ALBUM}" +misc.albumGalleryGrid.noCoverArt: "{NAME}" + +misc.uiLanguage: "UI Language: {LANGUAGES" + +homepage.title: "{TITLE}" +homepage.news.title: News +homepage.news.entry.viewRest: "(View rest of entry!)" + +albumSidebar.trackList.fallbackSectionName: "Track list" +albumSidebar.trackList.group: "{GROUP}" +albumSidebar.trackList.group.withRange: "{GROUP} ({RANGE})" +albumSidebar.trackList.item: "{TRACK}" +albumSidebar.groupBox.title: "{GROUP}" +albumSidebar.groupBox.next: "Next: {ALBUM}" +albumSidebar.groupBox.previous: "Previous: {ALBUM}" + +albumPage.title: "{ALBUM}" +albumPage.nav.album: "{ALBUM}" +albumPage.nav.randomTrack: "Random Track" +albumPage.nav.gallery: "Gallery" +albumPage.nav.commentary: "Commentary" +albumPage.socialEmbed.heading: "{GROUP}" +albumPage.socialEmbed.title: "{ALBUM}" +albumPage.socialEmbed.body.withDuration: "{DURATION}." +albumPage.socialEmbed.body.withTracks: "{TRACKS}." +albumPage.socialEmbed.body.withReleaseDate: Released {DATE}. +albumPage.socialEmbed.body.withDuration.withTracks: "{DURATION}, {TRACKS}." +albumPage.socialEmbed.body.withDuration.withReleaseDate: "{DURATION}. Released {DATE}." +albumPage.socialEmbed.body.withTracks.withReleaseDate: "{TRACKS}. Released {DATE}." +albumPage.socialEmbed.body.withDuration.withTracks.withReleaseDate: "{DURATION}, {TRACKS}. + Released {DATE}." + +albumGalleryPage.title: "{ALBUM} - Gallery" +albumGalleryPage.statsLine: >- + {TRACKS} totaling {DURATION}. +albumGalleryPage.statsLine.withDate: >- + {TRACKS} totaling {DURATION}. Released {DATE}. +albumGalleryPage.coverArtistsLine: >- + All track artwork by {ARTISTS}. +albumGalleryPage.noTrackArtworksLine: >- + This album doesn't have any track artwork. + +albumCommentaryPage.title: "{ALBUM} - Commentary" +albumCommentaryPage.infoLine: "{WORDS} across {ENTRIES}." +albumCommentaryPage.nav.album: "Album: {ALBUM}" +albumCommentaryPage.entry.title.albumCommentary: "Album commentary" +albumCommentaryPage.entry.title.trackCommentary: "{TRACK}" + +artistPage.title: "{ARTIST}" +artistPage.creditList.album: "{ALBUM}" +artistPage.creditList.album.withDate: "{ALBUM} ({DATE})" +artistPage.creditList.album.withDuration: "{ALBUM} ({DURATION})" +artistPage.creditList.album.withDate.withDuration: "{ALBUM} ({DATE}; {DURATION})" +artistPage.creditList.flashAct: "{ACT}" +artistPage.creditList.flashAct.withDate: "{ACT} ({DATE})" +artistPage.creditList.flashAct.withDateRange: "{ACT} ({DATE_RANGE})" +artistPage.creditList.entry.track: "{TRACK}" +artistPage.creditList.entry.track.withDuration: "({DURATION}) {TRACK}" +artistPage.creditList.entry.album.coverArt: "(cover art)" +artistPage.creditList.entry.album.wallpaperArt: "(wallpaper art)" +artistPage.creditList.entry.album.bannerArt: "(banner art)" +artistPage.creditList.entry.album.commentary: "(album commentary)" +artistPage.creditList.entry.flash: "{FLASH}" +artistPage.creditList.entry.rerelease: "{ENTRY} (re-release)" +artistPage.creditList.entry.withContribution: "{ENTRY} ({CONTRIBUTION})" +artistPage.creditList.entry.withArtists: "{ENTRY} (with {ARTISTS})" +artistPage.creditList.entry.withArtists.withContribution: "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})" +artistPage.contributedDurationLine: >- + {ARTIST} has contributed {DURATION} of music shared on this wiki. +artistPage.musicGroupsLine: "Contributed music to groups: {GROUPS}" +artistPage.artGroupsLine: "Contributed art to groups: {GROUPS}" +artistPage.groupsLine.item.withCount: "{GROUP} ({COUNT})" +artistPage.groupsLine.item.withDuration: "{GROUP} ({DURATION})" +artistPage.groupContributions.title.music: "Contributed music to groups:" +artistPage.groupContributions.title.artworks: "Contributed artworks to groups:" +artistPage.groupContributions.title.withSortButton: "{TITLE} ({SORT})" +artistPage.groupContributions.title.sorting.count: "Sorting by count." +artistPage.groupContributions.title.sorting.duration: "Sorting by duration." +artistPage.groupContributions.item.countAccent: "({COUNT})" +artistPage.groupContributions.item.durationAccent: "({DURATION})" +artistPage.groupContributions.item.countDurationAccent: "({COUNT} — {DURATION})" +artistPage.groupContributions.item.durationCountAccent: "({DURATION} — {COUNT})" +artistPage.trackList.title: "Tracks" +artistPage.artList.title: "Artworks" +artistPage.flashList.title: "Flashes & Games" +artistPage.commentaryList.title: "Commentary" +artistPage.viewArtGallery: "View {LINK}!" +artistPage.viewArtGallery.orBrowseList: "View {LINK}! Or browse the list:" +artistPage.viewArtGallery.link: "art gallery" +artistPage.nav.artist: "Artist: {ARTIST}" +artistGalleryPage.title: "{ARTIST} - Gallery" +artistGalleryPage.infoLine: "Contributed to {COVER_ARTS}." + +commentaryIndex.title: "Commentary" +commentaryIndex.infoLine: "{WORDS} across {ENTRIES}, in all." +commentaryIndex.albumList.title: "Choose an album:" +commentaryIndex.albumList.item: "{ALBUM} ({WORDS} across {ENTRIES})" + +flashIndex.title: "Flashes & Games" + +flashPage.title: "{FLASH}" +flashPage.nav.flash: "{FLASH}" + +flashSidebar.flashList.flashesInThisAct: "Flashes in this act" +flashSidebar.flashList.entriesInThisSection: "Entries in this section" + +groupSidebar.title: "Groups" +groupSidebar.groupList.category: "{CATEGORY}" +groupSidebar.groupList.item: "{GROUP}" + +groupPage.nav.group: "Group: {GROUP}" + +groupInfoPage.title: "{GROUP}" +groupInfoPage.viewAlbumGallery: "View {LINK}! Or browse the list:" +groupInfoPage.viewAlbumGallery.link: "album gallery" +groupInfoPage.albumList.title: "Albums" +groupInfoPage.albumList.item: "({YEAR}) {ALBUM}" +groupInfoPage.albumList.item.withoutYear: "{ALBUM}" +groupInfoPage.albumList.item.withAccent: "{ITEM} {ACCENT}" +groupInfoPage.albumList.item.otherGroupAccent: "(from {GROUP})" + +groupGalleryPage.title: "{GROUP} - Gallery" +groupGalleryPage.infoLine: "{TRACKS} across {ALBUMS}, totaling {TIME}." + +listingIndex.title: "Listings" +listingIndex.infoLine: >- + {WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}. +listingIndex.exploreList: >- + Feel free to explore any of the listings linked below and in the sidebar! + +listingPage.target.album: "Albums" +listingPage.target.artist: "Artists" +listingPage.target.group: "Groups" +listingPage.target.track: "Tracks" +listingPage.target.tag: "Tags" +listingPage.target.other: "Other" + +listingPage.listingsFor: "Listings for {TARGET}: {LISTINGS}" +listingPage.seeAlso: "Also check out: {LISTINGS}" + +listingPage.listAlbums.byName.title: "Albums - by Name" +listingPage.listAlbums.byName.title.short: "...by Name" +listingPage.listAlbums.byName.item: "{ALBUM} ({TRACKS})" +listingPage.listAlbums.byTracks.title: "Albums - by Tracks" +listingPage.listAlbums.byTracks.title.short: "...by Tracks" +listingPage.listAlbums.byTracks.item: "{ALBUM} ({TRACKS})" +listingPage.listAlbums.byDuration.title: "Albums - by Duration" +listingPage.listAlbums.byDuration.title.short: "...by Duration" +listingPage.listAlbums.byDuration.item: "{ALBUM} ({DURATION})" +listingPage.listAlbums.byDate.title: "Albums - by Date" +listingPage.listAlbums.byDate.title.short: "...by Date" +listingPage.listAlbums.byDate.item: "{ALBUM} ({DATE})" +listingPage.listAlbums.byDateAdded.title.short: "...by Date Added to Wiki" +listingPage.listAlbums.byDateAdded.title: "Albums - by Date Added to Wiki" +listingPage.listAlbums.byDateAdded.chunk.title: "{DATE}" +listingPage.listAlbums.byDateAdded.chunk.item: "{ALBUM}" + +listingPage.listArtists.byName.title: "Artists - by Name" +listingPage.listArtists.byName.title.short: "...by Name" +listingPage.listArtists.byName.item: "{ARTIST} ({CONTRIBUTIONS})" +listingPage.listArtists.byContribs.title: "Artists - by Contributions" +listingPage.listArtists.byContribs.title.short: "...by Contributions" +listingPage.listArtists.byContribs.item: "{ARTIST} ({CONTRIBUTIONS})" +listingPage.listArtists.byCommentary.title: "Artists - by Commentary Entries" +listingPage.listArtists.byCommentary.title.short: "...by Commentary Entries" +listingPage.listArtists.byCommentary.item: "{ARTIST} ({ENTRIES})" +listingPage.listArtists.byDuration.title: "Artists - by Duration" +listingPage.listArtists.byDuration.title.short: "...by Duration" +listingPage.listArtists.byDuration.item: "{ARTIST} ({DURATION})" +listingPage.listArtists.byLatest.title: "Artists - by Latest Contribution" +listingPage.listArtists.byLatest.title.short: "...by Latest Contribution" +listingPage.listArtists.byLatest.chunk.title.album: "{ALBUM} ({DATE})" +listingPage.listArtists.byLatest.chunk.title.flash: "{FLASH} ({DATE})" +listingPage.listArtists.byLatest.chunk.item: "{ARTIST}" +listingPage.listArtists.byLatest.dateless.title: "These artists' contributions aren't dated:" +listingPage.listArtists.byLatest.dateless.item: "{ARTIST}" + +listingPage.listGroups.byName.title: "Groups - by Name" +listingPage.listGroups.byName.title.short: "...by Name" +listingPage.listGroups.byName.item: "{GROUP} ({GALLERY})" +listingPage.listGroups.byName.item.gallery: "Gallery" +listingPage.listGroups.byCategory.title: "Groups - by Category" +listingPage.listGroups.byCategory.title.short: "...by Category" +listingPage.listGroups.byCategory.chunk.title: "{CATEGORY}" +listingPage.listGroups.byCategory.chunk.item: "{GROUP} ({GALLERY})" +listingPage.listGroups.byCategory.chunk.item.gallery: "Gallery" +listingPage.listGroups.byAlbums.title: "Groups - by Albums" +listingPage.listGroups.byAlbums.title.short: "...by Albums" +listingPage.listGroups.byAlbums.item: "{GROUP} ({ALBUMS})" +listingPage.listGroups.byTracks.title: "Groups - by Tracks" +listingPage.listGroups.byTracks.title.short: "...by Tracks" +listingPage.listGroups.byTracks.item: "{GROUP} ({TRACKS})" +listingPage.listGroups.byDuration.title: "Groups - by Duration" +listingPage.listGroups.byDuration.title.short: "...by Duration" +listingPage.listGroups.byDuration.item: "{GROUP} ({DURATION})" +listingPage.listGroups.byLatest.title: "Groups - by Latest Album" +listingPage.listGroups.byLatest.title.short: "...by Latest Album" +listingPage.listGroups.byLatest.item: "{GROUP} ({DATE})" + +listingPage.listTracks.byName.title: "Tracks - by Name" +listingPage.listTracks.byName.title.short: "...by Name" +listingPage.listTracks.byName.item: "{TRACK}" +listingPage.listTracks.byAlbum.title: "Tracks - by Album" +listingPage.listTracks.byAlbum.title.short: "...by Album" +listingPage.listTracks.byAlbum.chunk.title: "{ALBUM}" +listingPage.listTracks.byAlbum.chunk.item: "{TRACK}" +listingPage.listTracks.byDate.title: "Tracks - by Date" +listingPage.listTracks.byDate.title.short: "...by Date" +listingPage.listTracks.byDate.chunk.title: "{ALBUM} ({DATE})" +listingPage.listTracks.byDate.chunk.item: "{TRACK}" +listingPage.listTracks.byDate.chunk.item.rerelease: "{TRACK} (re-release)" +listingPage.listTracks.byDuration.title: "Tracks - by Duration" +listingPage.listTracks.byDuration.title.short: "...by Duration" +listingPage.listTracks.byDuration.item: "{TRACK} ({DURATION})" +listingPage.listTracks.byDurationInAlbum.title: "Tracks - by Duration (in Album)" +listingPage.listTracks.byDurationInAlbum.title.short: "...by Duration (in Album)" +listingPage.listTracks.byDurationInAlbum.chunk.title: "{ALBUM}" +listingPage.listTracks.byDurationInAlbum.chunk.item: "{TRACK} ({DURATION})" +listingPage.listTracks.byTimesReferenced.title: "Tracks - by Times Referenced" +listingPage.listTracks.byTimesReferenced.title.short: "...by Times Referenced" +listingPage.listTracks.byTimesReferenced.item: "{TRACK} ({TIMES_REFERENCED})" +listingPage.listTracks.inFlashes.byAlbum.title: "Tracks - in Flashes & Games (by Album)" +listingPage.listTracks.inFlashes.byAlbum.title.short: "...in Flashes & Games (by Album)" +listingPage.listTracks.inFlashes.byAlbum.chunk.title: "{ALBUM}" +listingPage.listTracks.inFlashes.byAlbum.chunk.item: "{TRACK} (in {FLASHES})" +listingPage.listTracks.inFlashes.byFlash.title: "Tracks - in Flashes & Games (by Flash)" +listingPage.listTracks.inFlashes.byFlash.title.short: "...in Flashes & Games (by Flash)" +listingPage.listTracks.inFlashes.byFlash.chunk.title: "{FLASH}" +listingPage.listTracks.inFlashes.byFlash.chunk.item: "{TRACK} (from {ALBUM})" +listingPage.listTracks.withLyrics.title: "Tracks - with Lyrics" +listingPage.listTracks.withLyrics.title.short: "...with Lyrics" +listingPage.listTracks.withLyrics.chunk.title: "{ALBUM}" +listingPage.listTracks.withLyrics.chunk.title.withDate: "{ALBUM} ({DATE})" +listingPage.listTracks.withLyrics.chunk.item: "{TRACK}" +listingPage.listTracks.withSheetMusicFiles.title: "Tracks - with Sheet Music Files" +listingPage.listTracks.withSheetMusicFiles.title.short: "...with Sheet Music Files" +listingPage.listTracks.withSheetMusicFiles.chunk.title: "{ALBUM}" +listingPage.listTracks.withSheetMusicFiles.chunk.title.withDate: "{ALBUM} ({DATE})" +listingPage.listTracks.withSheetMusicFiles.chunk.item: "{TRACK}" +listingPage.listTracks.withMidiProjectFiles.title: "Tracks - with MIDI & Project Files" +listingPage.listTracks.withMidiProjectFiles.title.short: "...with MIDI & Project Files" +listingPage.listTracks.withMidiProjectFiles.chunk.title: "{ALBUM}" +listingPage.listTracks.withMidiProjectFiles.chunk.title.withDate: "{ALBUM} ({DATE})" +listingPage.listTracks.withMidiProjectFiles.chunk.item: "{TRACK}" + +listingPage.listTags.byName.title: "Tags - by Name" +listingPage.listTags.byName.title.short: "...by Name" +listingPage.listTags.byName.item: "{TAG} ({TIMES_USED})" +listingPage.listTags.byUses.title: "Tags - by Uses" +listingPage.listTags.byUses.title.short: "...by Uses" +listingPage.listTags.byUses.item: "{TAG} ({TIMES_USED})" + +listingPage.other.allSheetMusic.title: "All Sheet Music" +listingPage.other.allSheetMusic.title.short: "All Sheet Music" +listingPage.other.allSheetMusic.albumFiles: "Album sheet music:" +listingPage.other.allSheetMusic.file: "{TITLE}" +listingPage.other.allSheetMusic.file.withMultipleFiles: "{TITLE} ({FILES})" +listingPage.other.allMidiProjectFiles.title: "All MIDI/Project Files" +listingPage.other.allMidiProjectFiles.title.short: "All MIDI/Project Files" +listingPage.other.allMidiProjectFiles.albumFiles: "Album MIDI/project files:" +listingPage.other.allMidiProjectFiles.file: "{TITLE}" +listingPage.other.allMidiProjectFiles.file.withMultipleFiles: "{TITLE} ({FILES})" +listingPage.other.allAdditionalFiles.title: "All Additional Files" +listingPage.other.allAdditionalFiles.title.short: "All Additional Files" +listingPage.other.allAdditionalFiles.albumFiles: "Album additional files:" +listingPage.other.allAdditionalFiles.file: "{TITLE}" +listingPage.other.allAdditionalFiles.file.withMultipleFiles: "{TITLE} ({FILES})" + +listingPage.other.randomPages.title: "Random Pages" +listingPage.other.randomPages.title.short: "Random Pages" +listingPage.other.randomPages.chooseLinkLine: >- + Choose a link to go to a random page in that category or album! + If your browser doesn't support relatively modern JavaScript + or you've disabled it, these links won't work - sorry. +listingPage.other.randomPages.dataLoadingLine: >- + (Data files are downloading in the background! Please wait for data to load.) +listingPage.other.randomPages.dataLoadedLine: >- + (Data files have finished being downloaded. The links should work!) +listingPage.other.randomPages.misc: "Miscellaneous:" +listingPage.other.randomPages.misc.randomArtist: "Random Artist" +listingPage.other.randomPages.misc.atLeastTwoContributions: "at least 2 contributions" +listingPage.other.randomPages.misc.randomAlbumWholeSite: "Random Album (whole site)" +listingPage.other.randomPages.misc.randomTrackWholeSite: "Random Track (whole site)" +listingPage.other.randomPages.group: "From {GROUP}: ({RANDOM_ALBUM}, {RANDOM_TRACK})" +listingPage.other.randomPages.group.randomAlbum: "Random Album" +listingPage.other.randomPages.group.randomTrack: "Random Track" +listingPage.other.randomPages.album: "{ALBUM}" + +listingPage.misc.trackContributors: "Track Contributors" +listingPage.misc.artContributors: "Art Contributors" +listingPage.misc.flashContributors: "Flash & Game Contributors" +listingPage.misc.artAndFlashContributors: "Art & Flash Contributors" + +newsIndex.title: "News" +newsIndex.entry.viewRest: "(View rest of entry!)" + +newsEntryPage.title: "{ENTRY}" +newsEntryPage.published: "(Published {DATE}.)" + +redirectPage.title: "Moved to {TITLE}" +redirectPage.infoLine: "This page has been moved to {TARGET}." + +tagPage.title: "{TAG}" +tagPage.infoLine: "Appears in {COVER_ARTS}." +tagPage.nav.tag: "Tag: {TAG}" + +trackPage.title: "{TRACK}" +trackPage.referenceList.fandom: "Fandom:" +trackPage.referenceList.official: "Official:" +trackPage.nav.track: "{TRACK}" +trackPage.nav.track.withNumber: "{NUMBER}. {TRACK}" +trackPage.nav.random: "Random" +trackPage.socialEmbed.heading: "{ALBUM}" +trackPage.socialEmbed.title: "{TRACK}" +trackPage.socialEmbed.body.withArtists.withCoverArtists: "By {ARTISTS}; art by {COVER_ARTISTS}." +trackPage.socialEmbed.body.withArtists: "By {ARTISTS}." +trackPage.socialEmbed.body.withCoverArtists: "Art by {COVER_ARTISTS}." diff --git a/src/upd8.js b/src/upd8.js index c011b660..eae9d217 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -94,7 +94,7 @@ try { const BUILD_TIME = new Date(); -const DEFAULT_STRINGS_FILE = 'strings-default.json'; +const DEFAULT_STRINGS_FILE = 'strings-default.yaml'; const STATUS_NOT_STARTED = `not started`; const STATUS_NOT_APPLICABLE = `not applicable`; -- 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(-) 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 143e4fbaed9e58387147b8fec5e1697bf1079f31 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 6 Nov 2023 20:08:54 -0400 Subject: content: organize strings-default.yaml into tree structure --- src/strings-default.yaml | 1509 ++++++++++++++++++++++++++++------------------ 1 file changed, 927 insertions(+), 582 deletions(-) diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 1f16de46..2fd905d1 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1,585 +1,930 @@ meta.languageCode: en meta.languageName: English -count.tracks: "{TRACKS}" -count.tracks.withUnit.zero: "" -count.tracks.withUnit.one: "{TRACKS} track" -count.tracks.withUnit.two: "" -count.tracks.withUnit.few: "" -count.tracks.withUnit.many: "" -count.tracks.withUnit.other: "{TRACKS} tracks" - -count.additionalFiles: "{FILES}" -count.additionalFiles.withUnit.zero: "" -count.additionalFiles.withUnit.one: "{FILES} file" -count.additionalFiles.withUnit.two: "" -count.additionalFiles.withUnit.few: "" -count.additionalFiles.withUnit.many: "" -count.additionalFiles.withUnit.other: "{FILES} files" - -count.albums: "{ALBUMS}" -count.albums.withUnit.zero: "" -count.albums.withUnit.one: "{ALBUMS} album" -count.albums.withUnit.two: "" -count.albums.withUnit.few: "" -count.albums.withUnit.many: "" -count.albums.withUnit.other: "{ALBUMS} albums" - -count.artworks: "{ARTWORKS}" -count.artworks.withUnit.zero: "" -count.artworks.withUnit.one: "{ARTWORKS} artwork" -count.artworks.withUnit.two: "" -count.artworks.withUnit.few: "" -count.artworks.withUnit.many: "" -count.artworks.withUnit.other: "{ARTWORKS} artworks" - -count.commentaryEntries: "{ENTRIES}" -count.commentaryEntries.withUnit.zero: "" -count.commentaryEntries.withUnit.one: "{ENTRIES} entry" -count.commentaryEntries.withUnit.two: "" -count.commentaryEntries.withUnit.few: "" -count.commentaryEntries.withUnit.many: "" -count.commentaryEntries.withUnit.other: "{ENTRIES} entries" - -count.contributions: "{CONTRIBUTIONS}" -count.contributions.withUnit.zero: "" -count.contributions.withUnit.one: "{CONTRIBUTIONS} contribution" -count.contributions.withUnit.two: "" -count.contributions.withUnit.few: "" -count.contributions.withUnit.many: "" -count.contributions.withUnit.other: "{CONTRIBUTIONS} contributions" - -count.coverArts: "{COVER_ARTS}" -count.coverArts.withUnit.zero: "" -count.coverArts.withUnit.one: "{COVER_ARTS} cover art" -count.coverArts.withUnit.two: "" -count.coverArts.withUnit.few: "" -count.coverArts.withUnit.many: "" -count.coverArts.withUnit.other: "{COVER_ARTS} cover arts" - -count.flashes: "{FLASHES}" -count.flashes.withUnit.zero: "" -count.flashes.withUnit.one: "{FLASHES} flashes & games" -count.flashes.withUnit.two: "" -count.flashes.withUnit.few: "" -count.flashes.withUnit.many: "" -count.flashes.withUnit.other: "{FLASHES} flashes & games" - -count.timesReferenced: "{TIMES_REFERENCED}" -count.timesReferenced.withUnit.zero: "" -count.timesReferenced.withUnit.one: "{TIMES_REFERENCED} time referenced" -count.timesReferenced.withUnit.two: "" -count.timesReferenced.withUnit.few: "" -count.timesReferenced.withUnit.many: "" -count.timesReferenced.withUnit.other: "{TIMES_REFERENCED} times referenced" - -count.words: "{WORDS}" -count.words.thousand: "{WORDS}k" -count.words.withUnit.zero: "" -count.words.withUnit.one: "{WORDS} word" -count.words.withUnit.two: "" -count.words.withUnit.few: "" -count.words.withUnit.many: "" -count.words.withUnit.other: "{WORDS} words" - -count.timesUsed: "{TIMES_USED}" -count.timesUsed.withUnit.zero: "" -count.timesUsed.withUnit.one: "used {TIMES_USED} time" -count.timesUsed.withUnit.two: "" -count.timesUsed.withUnit.few: "" -count.timesUsed.withUnit.many: "" -count.timesUsed.withUnit.other: "used {TIMES_USED} times" - -count.index.zero: "" -count.index.one: "{INDEX}st" -count.index.two: "{INDEX}nd" -count.index.few: "{INDEX}rd" -count.index.many: "" -count.index.other: "{INDEX}th" - -count.duration.hours: "{HOURS}:{MINUTES}:{SECONDS}" -count.duration.hours.withUnit: "{HOURS}:{MINUTES}:{SECONDS} hours" -count.duration.minutes: "{MINUTES}:{SECONDS}" -count.duration.minutes.withUnit: "{MINUTES}:{SECONDS} minutes" -count.duration.approximate: "~{DURATION}" -count.duration.missing: "_:__" - -count.fileSize.terabytes: "{TERABYTES} TB" -count.fileSize.gigabytes: "{GIGABYTES} GB" -count.fileSize.megabytes: "{MEGABYTES} MB" -count.fileSize.kilobytes: "{KILOBYTES} kB" -count.fileSize.bytes: "{BYTES} bytes" - -releaseInfo.by: "By {ARTISTS}." -releaseInfo.from: "From {ALBUM}." -releaseInfo.coverArtBy: "Cover art by {ARTISTS}." -releaseInfo.wallpaperArtBy: "Wallpaper art by {ARTISTS}." -releaseInfo.bannerArtBy: "Banner art by {ARTISTS}." -releaseInfo.released: "Released {DATE}." -releaseInfo.artReleased: "Art released {DATE}." -releaseInfo.addedToWiki: "Added to wiki {DATE}." -releaseInfo.duration: "Duration: {DURATION}." -releaseInfo.viewCommentary: "View {LINK}!" -releaseInfo.viewCommentary.link: "commentary page" -releaseInfo.viewGallery: "View {LINK}!" -releaseInfo.viewGallery.link: "gallery page" -releaseInfo.viewGalleryOrCommentary: "View {GALLERY} or {COMMENTARY}!" -releaseInfo.viewGalleryOrCommentary.gallery: "gallery page" -releaseInfo.viewGalleryOrCommentary.commentary: "commentary page" -releaseInfo.viewOriginalFile: "View {LINK}." -releaseInfo.viewOriginalFile.withSize: "View {LINK} ({SIZE})." -releaseInfo.viewOriginalFile.link: "original file" -releaseInfo.viewOriginalFile.sizeWarning: >- - (Heads up! If you're on a mobile plan, this is a large download.) -releaseInfo.listenOn: "Listen on {LINKS}." -releaseInfo.listenOn.noLinks: >- - This wiki doesn't have any listening links for {NAME}. -releaseInfo.visitOn: "Visit on {LINKS}." -releaseInfo.playOn: "Play on {LINKS}." -releaseInfo.readCommentary: "Read {LINK}." -releaseInfo.readCommentary.link: "artist commentary" -releaseInfo.alsoReleasedAs: "Also released as:" -releaseInfo.alsoReleasedAs.item: "{TRACK} (on {ALBUM})" -releaseInfo.contributors: "Contributors:" -releaseInfo.tracksReferenced: "Tracks that {TRACK} references:" -releaseInfo.tracksThatReference: "Tracks that reference {TRACK}:" -releaseInfo.tracksSampled: "Tracks that {TRACK} samples:" -releaseInfo.tracksThatSample: "Tracks that sample {TRACK}:" -releaseInfo.flashesThatFeature: "Flashes & games that feature {TRACK}:" -releaseInfo.flashesThatFeature.item: "{FLASH}" -releaseInfo.flashesThatFeature.item.asDifferentRelease: "{FLASH} (as {TRACK})" -releaseInfo.tracksFeatured: "Tracks that {FLASH} features:" -releaseInfo.lyrics: "Lyrics:" -releaseInfo.artistCommentary: "Artist commentary:" -releaseInfo.artistCommentary.seeOriginalRelease: "See {ORIGINAL}!" -releaseInfo.artTags: "Tags:" -releaseInfo.artTags.inline: "Tags: {TAGS}" -releaseInfo.additionalFiles.shortcut: "View {ANCHOR_LINK}: {TITLES}" -releaseInfo.additionalFiles.shortcut.anchorLink: "additional files" -releaseInfo.additionalFiles.heading: "View or download {ADDITIONAL_FILES}:" -releaseInfo.additionalFiles.entry: "{TITLE}" -releaseInfo.additionalFiles.entry.withDescription: "{TITLE}: {DESCRIPTION}" -releaseInfo.additionalFiles.file: "{FILE}" -releaseInfo.additionalFiles.file.withSize: "{FILE} ({SIZE})" -releaseInfo.sheetMusicFiles.shortcut: "Download {LINK}." -releaseInfo.sheetMusicFiles.shortcut.link: "sheet music files" -releaseInfo.sheetMusicFiles.heading: "Print or download sheet music files:" -releaseInfo.midiProjectFiles.shortcut: "Download {LINK}." -releaseInfo.midiProjectFiles.shortcut.link: "MIDI/project files" -releaseInfo.midiProjectFiles.heading: "Download MIDI/project files:" -releaseInfo.note: "Context notes:" - -trackList.section.withDuration: "{SECTION} ({DURATION}):" -trackList.group: "From {GROUP}:" -trackList.group.fromOther: "From somewhere else:" -trackList.item.withDuration: "({DURATION}) {TRACK}" -trackList.item.withDuration.withArtists: "({DURATION}) {TRACK} {BY}" -trackList.item.withArtists: "{TRACK} {BY}" -trackList.item.withArtists.by: "by {ARTISTS}" -trackList.item.rerelease: "{TRACK} (re-release)" - -misc.alt.albumCover: "album cover" -misc.alt.albumBanner: "album banner" -misc.alt.trackCover: "track cover" -misc.alt.artistAvatar: "artist avatar" -misc.alt.flashArt: "flash art" - -misc.artistLink: "{ARTIST}" -misc.artistLink.withContribution: "{ARTIST} ({CONTRIB})" -misc.artistLink.withExternalLinks: "{ARTIST} ({LINKS})" -misc.artistLink.withContribution.withExternalLinks: "{ARTIST} ({CONTRIB}) ({LINKS})" - -misc.chronology.seeArtistPages: "(See artist pages for chronology info!)" -misc.chronology.heading.coverArt: "{INDEX} cover art by {ARTIST}" -misc.chronology.heading.flash: "{INDEX} flash/game by {ARTIST}" -misc.chronology.heading.track: "{INDEX} track by {ARTIST}" -misc.chronology.withNavigation: "{HEADING} ({NAVIGATION})" - -misc.external.domain: "External ({DOMAIN})" -misc.external.local: "Wiki Archive (local upload)" -misc.external.bandcamp: "Bandcamp" -misc.external.bandcamp.domain: "Bandcamp ({DOMAIN})" -misc.external.deviantart: "DeviantArt" -misc.external.instagram: "Instagram" -misc.external.mastodon: "Mastodon" -misc.external.mastodon.domain: "Mastodon ({DOMAIN})" -misc.external.newgrounds: "Newgrounds" -misc.external.patreon: "Patreon" -misc.external.poetryFoundation: "Poetry Foundation" -misc.external.soundcloud: "SoundCloud" -misc.external.spotify: "Spotify" -misc.external.tumblr: "Tumblr" -misc.external.twitter: "Twitter" -misc.external.wikipedia: "Wikipedia" -misc.external.youtube: "YouTube" -misc.external.youtube.playlist: "YouTube (playlist)" -misc.external.youtube.fullAlbum: "YouTube (full album)" -misc.external.flash.bgreco: "{LINK} (HQ Audio)" -misc.external.flash.homestuck.page: "{LINK} (page {PAGE})" -misc.external.flash.homestuck.secret: "{LINK} (secret page)" -misc.external.flash.youtube: "{LINK} (on any device)" - -misc.missingImage: "(This image file is missing)" -misc.missingLinkContent: "(Missing link content)" - -misc.nav.previous: "Previous" -misc.nav.next: "Next" -misc.nav.info: "Info" -misc.nav.gallery: "Gallery" - -misc.pageTitle: "{TITLE}" -misc.pageTitle.withWikiName: "{TITLE} | {WIKI_NAME}" - -misc.skippers.skipTo: "Skip to:" -misc.skippers.content: "Content" -misc.skippers.sidebar: "Sidebar" -misc.skippers.sidebar.left: "Sidebar (left)" -misc.skippers.sidebar.right: "Sidebar (right)" -misc.skippers.header: "Header" -misc.skippers.footer: "Footer" -misc.skippers.tracks: "Tracks" -misc.skippers.art: "Artworks" -misc.skippers.flashes: "Flashes & Games" -misc.skippers.contributors: "Contributors" -misc.skippers.references: "References..." -misc.skippers.referencedBy: "Referenced by..." -misc.skippers.samples: "Samples..." -misc.skippers.sampledBy: "Sampled by..." -misc.skippers.features: "Features..." -misc.skippers.featuredIn: "Featured in..." -misc.skippers.lyrics: "Lyrics" -misc.skippers.sheetMusicFiles: "Sheet music files" -misc.skippers.midiProjectFiles: "MIDI/project files" -misc.skippers.additionalFiles: "Additional files" -misc.skippers.commentary: "Commentary" -misc.skippers.artistCommentary: "Commentary" - -misc.socialEmbed.heading: "{WIKI_NAME} | {HEADING}" - -misc.jumpTo: "Jump to:" -misc.jumpTo.withLinks: "Jump to: {LINKS}." - -misc.contentWarnings: "cw: {WARNINGS}" -misc.contentWarnings.reveal: "click to show" - -misc.albumGrid.details: "({TRACKS}, {TIME})" -misc.albumGrid.details.coverArtists: "(Illust. {ARTISTS})" -misc.albumGrid.details.otherCoverArtists: "(With {ARTISTS})" -misc.albumGrid.noCoverArt: "{ALBUM}" -misc.albumGalleryGrid.noCoverArt: "{NAME}" - -misc.uiLanguage: "UI Language: {LANGUAGES" - -homepage.title: "{TITLE}" -homepage.news.title: News -homepage.news.entry.viewRest: "(View rest of entry!)" - -albumSidebar.trackList.fallbackSectionName: "Track list" -albumSidebar.trackList.group: "{GROUP}" -albumSidebar.trackList.group.withRange: "{GROUP} ({RANGE})" -albumSidebar.trackList.item: "{TRACK}" -albumSidebar.groupBox.title: "{GROUP}" -albumSidebar.groupBox.next: "Next: {ALBUM}" -albumSidebar.groupBox.previous: "Previous: {ALBUM}" - -albumPage.title: "{ALBUM}" -albumPage.nav.album: "{ALBUM}" -albumPage.nav.randomTrack: "Random Track" -albumPage.nav.gallery: "Gallery" -albumPage.nav.commentary: "Commentary" -albumPage.socialEmbed.heading: "{GROUP}" -albumPage.socialEmbed.title: "{ALBUM}" -albumPage.socialEmbed.body.withDuration: "{DURATION}." -albumPage.socialEmbed.body.withTracks: "{TRACKS}." -albumPage.socialEmbed.body.withReleaseDate: Released {DATE}. -albumPage.socialEmbed.body.withDuration.withTracks: "{DURATION}, {TRACKS}." -albumPage.socialEmbed.body.withDuration.withReleaseDate: "{DURATION}. Released {DATE}." -albumPage.socialEmbed.body.withTracks.withReleaseDate: "{TRACKS}. Released {DATE}." -albumPage.socialEmbed.body.withDuration.withTracks.withReleaseDate: "{DURATION}, {TRACKS}. - Released {DATE}." - -albumGalleryPage.title: "{ALBUM} - Gallery" -albumGalleryPage.statsLine: >- - {TRACKS} totaling {DURATION}. -albumGalleryPage.statsLine.withDate: >- - {TRACKS} totaling {DURATION}. Released {DATE}. -albumGalleryPage.coverArtistsLine: >- - All track artwork by {ARTISTS}. -albumGalleryPage.noTrackArtworksLine: >- - This album doesn't have any track artwork. - -albumCommentaryPage.title: "{ALBUM} - Commentary" -albumCommentaryPage.infoLine: "{WORDS} across {ENTRIES}." -albumCommentaryPage.nav.album: "Album: {ALBUM}" -albumCommentaryPage.entry.title.albumCommentary: "Album commentary" -albumCommentaryPage.entry.title.trackCommentary: "{TRACK}" - -artistPage.title: "{ARTIST}" -artistPage.creditList.album: "{ALBUM}" -artistPage.creditList.album.withDate: "{ALBUM} ({DATE})" -artistPage.creditList.album.withDuration: "{ALBUM} ({DURATION})" -artistPage.creditList.album.withDate.withDuration: "{ALBUM} ({DATE}; {DURATION})" -artistPage.creditList.flashAct: "{ACT}" -artistPage.creditList.flashAct.withDate: "{ACT} ({DATE})" -artistPage.creditList.flashAct.withDateRange: "{ACT} ({DATE_RANGE})" -artistPage.creditList.entry.track: "{TRACK}" -artistPage.creditList.entry.track.withDuration: "({DURATION}) {TRACK}" -artistPage.creditList.entry.album.coverArt: "(cover art)" -artistPage.creditList.entry.album.wallpaperArt: "(wallpaper art)" -artistPage.creditList.entry.album.bannerArt: "(banner art)" -artistPage.creditList.entry.album.commentary: "(album commentary)" -artistPage.creditList.entry.flash: "{FLASH}" -artistPage.creditList.entry.rerelease: "{ENTRY} (re-release)" -artistPage.creditList.entry.withContribution: "{ENTRY} ({CONTRIBUTION})" -artistPage.creditList.entry.withArtists: "{ENTRY} (with {ARTISTS})" -artistPage.creditList.entry.withArtists.withContribution: "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})" -artistPage.contributedDurationLine: >- - {ARTIST} has contributed {DURATION} of music shared on this wiki. -artistPage.musicGroupsLine: "Contributed music to groups: {GROUPS}" -artistPage.artGroupsLine: "Contributed art to groups: {GROUPS}" -artistPage.groupsLine.item.withCount: "{GROUP} ({COUNT})" -artistPage.groupsLine.item.withDuration: "{GROUP} ({DURATION})" -artistPage.groupContributions.title.music: "Contributed music to groups:" -artistPage.groupContributions.title.artworks: "Contributed artworks to groups:" -artistPage.groupContributions.title.withSortButton: "{TITLE} ({SORT})" -artistPage.groupContributions.title.sorting.count: "Sorting by count." -artistPage.groupContributions.title.sorting.duration: "Sorting by duration." -artistPage.groupContributions.item.countAccent: "({COUNT})" -artistPage.groupContributions.item.durationAccent: "({DURATION})" -artistPage.groupContributions.item.countDurationAccent: "({COUNT} — {DURATION})" -artistPage.groupContributions.item.durationCountAccent: "({DURATION} — {COUNT})" -artistPage.trackList.title: "Tracks" -artistPage.artList.title: "Artworks" -artistPage.flashList.title: "Flashes & Games" -artistPage.commentaryList.title: "Commentary" -artistPage.viewArtGallery: "View {LINK}!" -artistPage.viewArtGallery.orBrowseList: "View {LINK}! Or browse the list:" -artistPage.viewArtGallery.link: "art gallery" -artistPage.nav.artist: "Artist: {ARTIST}" -artistGalleryPage.title: "{ARTIST} - Gallery" -artistGalleryPage.infoLine: "Contributed to {COVER_ARTS}." - -commentaryIndex.title: "Commentary" -commentaryIndex.infoLine: "{WORDS} across {ENTRIES}, in all." -commentaryIndex.albumList.title: "Choose an album:" -commentaryIndex.albumList.item: "{ALBUM} ({WORDS} across {ENTRIES})" - -flashIndex.title: "Flashes & Games" - -flashPage.title: "{FLASH}" -flashPage.nav.flash: "{FLASH}" - -flashSidebar.flashList.flashesInThisAct: "Flashes in this act" -flashSidebar.flashList.entriesInThisSection: "Entries in this section" - -groupSidebar.title: "Groups" -groupSidebar.groupList.category: "{CATEGORY}" -groupSidebar.groupList.item: "{GROUP}" - -groupPage.nav.group: "Group: {GROUP}" - -groupInfoPage.title: "{GROUP}" -groupInfoPage.viewAlbumGallery: "View {LINK}! Or browse the list:" -groupInfoPage.viewAlbumGallery.link: "album gallery" -groupInfoPage.albumList.title: "Albums" -groupInfoPage.albumList.item: "({YEAR}) {ALBUM}" -groupInfoPage.albumList.item.withoutYear: "{ALBUM}" -groupInfoPage.albumList.item.withAccent: "{ITEM} {ACCENT}" -groupInfoPage.albumList.item.otherGroupAccent: "(from {GROUP})" - -groupGalleryPage.title: "{GROUP} - Gallery" -groupGalleryPage.infoLine: "{TRACKS} across {ALBUMS}, totaling {TIME}." - -listingIndex.title: "Listings" -listingIndex.infoLine: >- - {WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}. -listingIndex.exploreList: >- - Feel free to explore any of the listings linked below and in the sidebar! - -listingPage.target.album: "Albums" -listingPage.target.artist: "Artists" -listingPage.target.group: "Groups" -listingPage.target.track: "Tracks" -listingPage.target.tag: "Tags" -listingPage.target.other: "Other" - -listingPage.listingsFor: "Listings for {TARGET}: {LISTINGS}" -listingPage.seeAlso: "Also check out: {LISTINGS}" - -listingPage.listAlbums.byName.title: "Albums - by Name" -listingPage.listAlbums.byName.title.short: "...by Name" -listingPage.listAlbums.byName.item: "{ALBUM} ({TRACKS})" -listingPage.listAlbums.byTracks.title: "Albums - by Tracks" -listingPage.listAlbums.byTracks.title.short: "...by Tracks" -listingPage.listAlbums.byTracks.item: "{ALBUM} ({TRACKS})" -listingPage.listAlbums.byDuration.title: "Albums - by Duration" -listingPage.listAlbums.byDuration.title.short: "...by Duration" -listingPage.listAlbums.byDuration.item: "{ALBUM} ({DURATION})" -listingPage.listAlbums.byDate.title: "Albums - by Date" -listingPage.listAlbums.byDate.title.short: "...by Date" -listingPage.listAlbums.byDate.item: "{ALBUM} ({DATE})" -listingPage.listAlbums.byDateAdded.title.short: "...by Date Added to Wiki" -listingPage.listAlbums.byDateAdded.title: "Albums - by Date Added to Wiki" -listingPage.listAlbums.byDateAdded.chunk.title: "{DATE}" -listingPage.listAlbums.byDateAdded.chunk.item: "{ALBUM}" - -listingPage.listArtists.byName.title: "Artists - by Name" -listingPage.listArtists.byName.title.short: "...by Name" -listingPage.listArtists.byName.item: "{ARTIST} ({CONTRIBUTIONS})" -listingPage.listArtists.byContribs.title: "Artists - by Contributions" -listingPage.listArtists.byContribs.title.short: "...by Contributions" -listingPage.listArtists.byContribs.item: "{ARTIST} ({CONTRIBUTIONS})" -listingPage.listArtists.byCommentary.title: "Artists - by Commentary Entries" -listingPage.listArtists.byCommentary.title.short: "...by Commentary Entries" -listingPage.listArtists.byCommentary.item: "{ARTIST} ({ENTRIES})" -listingPage.listArtists.byDuration.title: "Artists - by Duration" -listingPage.listArtists.byDuration.title.short: "...by Duration" -listingPage.listArtists.byDuration.item: "{ARTIST} ({DURATION})" -listingPage.listArtists.byLatest.title: "Artists - by Latest Contribution" -listingPage.listArtists.byLatest.title.short: "...by Latest Contribution" -listingPage.listArtists.byLatest.chunk.title.album: "{ALBUM} ({DATE})" -listingPage.listArtists.byLatest.chunk.title.flash: "{FLASH} ({DATE})" -listingPage.listArtists.byLatest.chunk.item: "{ARTIST}" -listingPage.listArtists.byLatest.dateless.title: "These artists' contributions aren't dated:" -listingPage.listArtists.byLatest.dateless.item: "{ARTIST}" - -listingPage.listGroups.byName.title: "Groups - by Name" -listingPage.listGroups.byName.title.short: "...by Name" -listingPage.listGroups.byName.item: "{GROUP} ({GALLERY})" -listingPage.listGroups.byName.item.gallery: "Gallery" -listingPage.listGroups.byCategory.title: "Groups - by Category" -listingPage.listGroups.byCategory.title.short: "...by Category" -listingPage.listGroups.byCategory.chunk.title: "{CATEGORY}" -listingPage.listGroups.byCategory.chunk.item: "{GROUP} ({GALLERY})" -listingPage.listGroups.byCategory.chunk.item.gallery: "Gallery" -listingPage.listGroups.byAlbums.title: "Groups - by Albums" -listingPage.listGroups.byAlbums.title.short: "...by Albums" -listingPage.listGroups.byAlbums.item: "{GROUP} ({ALBUMS})" -listingPage.listGroups.byTracks.title: "Groups - by Tracks" -listingPage.listGroups.byTracks.title.short: "...by Tracks" -listingPage.listGroups.byTracks.item: "{GROUP} ({TRACKS})" -listingPage.listGroups.byDuration.title: "Groups - by Duration" -listingPage.listGroups.byDuration.title.short: "...by Duration" -listingPage.listGroups.byDuration.item: "{GROUP} ({DURATION})" -listingPage.listGroups.byLatest.title: "Groups - by Latest Album" -listingPage.listGroups.byLatest.title.short: "...by Latest Album" -listingPage.listGroups.byLatest.item: "{GROUP} ({DATE})" - -listingPage.listTracks.byName.title: "Tracks - by Name" -listingPage.listTracks.byName.title.short: "...by Name" -listingPage.listTracks.byName.item: "{TRACK}" -listingPage.listTracks.byAlbum.title: "Tracks - by Album" -listingPage.listTracks.byAlbum.title.short: "...by Album" -listingPage.listTracks.byAlbum.chunk.title: "{ALBUM}" -listingPage.listTracks.byAlbum.chunk.item: "{TRACK}" -listingPage.listTracks.byDate.title: "Tracks - by Date" -listingPage.listTracks.byDate.title.short: "...by Date" -listingPage.listTracks.byDate.chunk.title: "{ALBUM} ({DATE})" -listingPage.listTracks.byDate.chunk.item: "{TRACK}" -listingPage.listTracks.byDate.chunk.item.rerelease: "{TRACK} (re-release)" -listingPage.listTracks.byDuration.title: "Tracks - by Duration" -listingPage.listTracks.byDuration.title.short: "...by Duration" -listingPage.listTracks.byDuration.item: "{TRACK} ({DURATION})" -listingPage.listTracks.byDurationInAlbum.title: "Tracks - by Duration (in Album)" -listingPage.listTracks.byDurationInAlbum.title.short: "...by Duration (in Album)" -listingPage.listTracks.byDurationInAlbum.chunk.title: "{ALBUM}" -listingPage.listTracks.byDurationInAlbum.chunk.item: "{TRACK} ({DURATION})" -listingPage.listTracks.byTimesReferenced.title: "Tracks - by Times Referenced" -listingPage.listTracks.byTimesReferenced.title.short: "...by Times Referenced" -listingPage.listTracks.byTimesReferenced.item: "{TRACK} ({TIMES_REFERENCED})" -listingPage.listTracks.inFlashes.byAlbum.title: "Tracks - in Flashes & Games (by Album)" -listingPage.listTracks.inFlashes.byAlbum.title.short: "...in Flashes & Games (by Album)" -listingPage.listTracks.inFlashes.byAlbum.chunk.title: "{ALBUM}" -listingPage.listTracks.inFlashes.byAlbum.chunk.item: "{TRACK} (in {FLASHES})" -listingPage.listTracks.inFlashes.byFlash.title: "Tracks - in Flashes & Games (by Flash)" -listingPage.listTracks.inFlashes.byFlash.title.short: "...in Flashes & Games (by Flash)" -listingPage.listTracks.inFlashes.byFlash.chunk.title: "{FLASH}" -listingPage.listTracks.inFlashes.byFlash.chunk.item: "{TRACK} (from {ALBUM})" -listingPage.listTracks.withLyrics.title: "Tracks - with Lyrics" -listingPage.listTracks.withLyrics.title.short: "...with Lyrics" -listingPage.listTracks.withLyrics.chunk.title: "{ALBUM}" -listingPage.listTracks.withLyrics.chunk.title.withDate: "{ALBUM} ({DATE})" -listingPage.listTracks.withLyrics.chunk.item: "{TRACK}" -listingPage.listTracks.withSheetMusicFiles.title: "Tracks - with Sheet Music Files" -listingPage.listTracks.withSheetMusicFiles.title.short: "...with Sheet Music Files" -listingPage.listTracks.withSheetMusicFiles.chunk.title: "{ALBUM}" -listingPage.listTracks.withSheetMusicFiles.chunk.title.withDate: "{ALBUM} ({DATE})" -listingPage.listTracks.withSheetMusicFiles.chunk.item: "{TRACK}" -listingPage.listTracks.withMidiProjectFiles.title: "Tracks - with MIDI & Project Files" -listingPage.listTracks.withMidiProjectFiles.title.short: "...with MIDI & Project Files" -listingPage.listTracks.withMidiProjectFiles.chunk.title: "{ALBUM}" -listingPage.listTracks.withMidiProjectFiles.chunk.title.withDate: "{ALBUM} ({DATE})" -listingPage.listTracks.withMidiProjectFiles.chunk.item: "{TRACK}" - -listingPage.listTags.byName.title: "Tags - by Name" -listingPage.listTags.byName.title.short: "...by Name" -listingPage.listTags.byName.item: "{TAG} ({TIMES_USED})" -listingPage.listTags.byUses.title: "Tags - by Uses" -listingPage.listTags.byUses.title.short: "...by Uses" -listingPage.listTags.byUses.item: "{TAG} ({TIMES_USED})" - -listingPage.other.allSheetMusic.title: "All Sheet Music" -listingPage.other.allSheetMusic.title.short: "All Sheet Music" -listingPage.other.allSheetMusic.albumFiles: "Album sheet music:" -listingPage.other.allSheetMusic.file: "{TITLE}" -listingPage.other.allSheetMusic.file.withMultipleFiles: "{TITLE} ({FILES})" -listingPage.other.allMidiProjectFiles.title: "All MIDI/Project Files" -listingPage.other.allMidiProjectFiles.title.short: "All MIDI/Project Files" -listingPage.other.allMidiProjectFiles.albumFiles: "Album MIDI/project files:" -listingPage.other.allMidiProjectFiles.file: "{TITLE}" -listingPage.other.allMidiProjectFiles.file.withMultipleFiles: "{TITLE} ({FILES})" -listingPage.other.allAdditionalFiles.title: "All Additional Files" -listingPage.other.allAdditionalFiles.title.short: "All Additional Files" -listingPage.other.allAdditionalFiles.albumFiles: "Album additional files:" -listingPage.other.allAdditionalFiles.file: "{TITLE}" -listingPage.other.allAdditionalFiles.file.withMultipleFiles: "{TITLE} ({FILES})" - -listingPage.other.randomPages.title: "Random Pages" -listingPage.other.randomPages.title.short: "Random Pages" -listingPage.other.randomPages.chooseLinkLine: >- - Choose a link to go to a random page in that category or album! - If your browser doesn't support relatively modern JavaScript - or you've disabled it, these links won't work - sorry. -listingPage.other.randomPages.dataLoadingLine: >- - (Data files are downloading in the background! Please wait for data to load.) -listingPage.other.randomPages.dataLoadedLine: >- - (Data files have finished being downloaded. The links should work!) -listingPage.other.randomPages.misc: "Miscellaneous:" -listingPage.other.randomPages.misc.randomArtist: "Random Artist" -listingPage.other.randomPages.misc.atLeastTwoContributions: "at least 2 contributions" -listingPage.other.randomPages.misc.randomAlbumWholeSite: "Random Album (whole site)" -listingPage.other.randomPages.misc.randomTrackWholeSite: "Random Track (whole site)" -listingPage.other.randomPages.group: "From {GROUP}: ({RANDOM_ALBUM}, {RANDOM_TRACK})" -listingPage.other.randomPages.group.randomAlbum: "Random Album" -listingPage.other.randomPages.group.randomTrack: "Random Track" -listingPage.other.randomPages.album: "{ALBUM}" - -listingPage.misc.trackContributors: "Track Contributors" -listingPage.misc.artContributors: "Art Contributors" -listingPage.misc.flashContributors: "Flash & Game Contributors" -listingPage.misc.artAndFlashContributors: "Art & Flash Contributors" - -newsIndex.title: "News" -newsIndex.entry.viewRest: "(View rest of entry!)" - -newsEntryPage.title: "{ENTRY}" -newsEntryPage.published: "(Published {DATE}.)" - -redirectPage.title: "Moved to {TITLE}" -redirectPage.infoLine: "This page has been moved to {TARGET}." - -tagPage.title: "{TAG}" -tagPage.infoLine: "Appears in {COVER_ARTS}." -tagPage.nav.tag: "Tag: {TAG}" - -trackPage.title: "{TRACK}" -trackPage.referenceList.fandom: "Fandom:" -trackPage.referenceList.official: "Official:" -trackPage.nav.track: "{TRACK}" -trackPage.nav.track.withNumber: "{NUMBER}. {TRACK}" -trackPage.nav.random: "Random" -trackPage.socialEmbed.heading: "{ALBUM}" -trackPage.socialEmbed.title: "{TRACK}" -trackPage.socialEmbed.body.withArtists.withCoverArtists: "By {ARTISTS}; art by {COVER_ARTISTS}." -trackPage.socialEmbed.body.withArtists: "By {ARTISTS}." -trackPage.socialEmbed.body.withCoverArtists: "Art by {COVER_ARTISTS}." +count: + tracks: + _: "{TRACKS}" + withUnit: + zero: "" + one: "{TRACKS} track" + two: "" + few: "" + many: "" + other: "{TRACKS} tracks" + + additionalFiles: + _: "{FILES}" + withUnit: + zero: "" + one: "{FILES} file" + two: "" + few: "" + many: "" + other: "{FILES} files" + + albums: + _: "{ALBUMS}" + withUnit: + zero: "" + one: "{ALBUMS} album" + two: "" + few: "" + many: "" + other: "{ALBUMS} albums" + + artworks: + _: "{ARTWORKS}" + withUnit: + zero: "" + one: "{ARTWORKS} artwork" + two: "" + few: "" + many: "" + other: "{ARTWORKS} artworks" + + commentaryEntries: + _: "{ENTRIES}" + withUnit: + zero: "" + one: "{ENTRIES} entry" + two: "" + few: "" + many: "" + other: "{ENTRIES} entries" + + contributions: + _: "{CONTRIBUTIONS}" + withUnit: + zero: "" + one: "{CONTRIBUTIONS} contribution" + two: "" + few: "" + many: "" + other: "{CONTRIBUTIONS} contributions" + + coverArts: + _: "{COVER_ARTS}" + withUnit: + zero: "" + one: "{COVER_ARTS} cover art" + two: "" + few: "" + many: "" + other: "{COVER_ARTS} cover arts" + + flashes: + _: "{FLASHES}" + withUnit: + zero: "" + one: "{FLASHES} flashes & games" + two: "" + few: "" + many: "" + other: "{FLASHES} flashes & games" + + timesReferenced: + _: "{TIMES_REFERENCED}" + withUnit: + zero: "" + one: "{TIMES_REFERENCED} time referenced" + two: "" + few: "" + many: "" + other: "{TIMES_REFERENCED} times referenced" + + words: + _: "{WORDS}" + thousand: "{WORDS}k" + withUnit: + zero: "" + one: "{WORDS} word" + two: "" + few: "" + many: "" + other: "{WORDS} words" + + timesUsed: + _: "{TIMES_USED}" + withUnit: + zero: "" + one: "used {TIMES_USED} time" + two: "" + few: "" + many: "" + other: "used {TIMES_USED} times" + + index: + zero: "" + one: "{INDEX}st" + two: "{INDEX}nd" + few: "{INDEX}rd" + many: "" + other: "{INDEX}th" + + duration: + missing: "_:__" + approximate: "~{DURATION}" + hours: + _: "{HOURS}:{MINUTES}:{SECONDS}" + withUnit: "{HOURS}:{MINUTES}:{SECONDS} hours" + minutes: + _: "{MINUTES}:{SECONDS}" + withUnit: "{MINUTES}:{SECONDS} minutes" + + fileSize: + terabytes: "{TERABYTES} TB" + gigabytes: "{GIGABYTES} GB" + megabytes: "{MEGABYTES} MB" + kilobytes: "{KILOBYTES} kB" + bytes: "{BYTES} bytes" + +releaseInfo: + by: "By {ARTISTS}." + from: "From {ALBUM}." + + coverArtBy: "Cover art by {ARTISTS}." + wallpaperArtBy: "Wallpaper art by {ARTISTS}." + bannerArtBy: "Banner art by {ARTISTS}." + + released: "Released {DATE}." + artReleased: "Art released {DATE}." + addedToWiki: "Added to wiki {DATE}." + + duration: "Duration: {DURATION}." + + viewCommentary: + _: "View {LINK}!" + link: "commentary page" + + viewGallery: + _: "View {LINK}!" + link: "gallery page" + + viewGalleryOrCommentary: + _: "View {GALLERY} or {COMMENTARY}!" + gallery: "gallery page" + commentary: "commentary page" + + viewOriginalFile: + _: "View {LINK}." + withSize: "View {LINK} ({SIZE})." + link: "original file" + sizeWarning: >- + (Heads up! If you're on a mobile plan, this is a large download.) + + listenOn: + _: "Listen on {LINKS}." + noLinks: >- + This wiki doesn't have any listening links for {NAME}. + + visitOn: "Visit on {LINKS}." + playOn: "Play on {LINKS}." + + readCommentary: + _: "Read {LINK}." + link: "artist commentary" + + alsoReleasedAs: + _: "Also released as:" + item: "{TRACK} (on {ALBUM})" + + contributors: "Contributors:" + + tracksReferenced: "Tracks that {TRACK} references:" + tracksThatReference: "Tracks that reference {TRACK}:" + tracksSampled: "Tracks that {TRACK} samples:" + tracksThatSample: "Tracks that sample {TRACK}:" + + flashesThatFeature: + _: "Flashes & games that feature {TRACK}:" + item: + _: "{FLASH}" + asDifferentRelease: "{FLASH} (as {TRACK})" + + tracksFeatured: "Tracks that {FLASH} features:" + + lyrics: "Lyrics:" + note: "Context notes:" + + artistCommentary: + _: "Artist commentary:" + seeOriginalRelease: "See {ORIGINAL}!" + + artTags: + _: "Tags:" + inline: "Tags: {TAGS}" + + additionalFiles: + heading: "View or download {ADDITIONAL_FILES}:" + + entry: + _: "{TITLE}" + withDescription: "{TITLE}: {DESCRIPTION}" + + file: + _: "{FILE}" + withSize: "{FILE} ({SIZE})" + + shortcut: + _: "View {ANCHOR_LINK}: {TITLES}" + anchorLink: "additional files" + + sheetMusicFiles: + heading: "Print or download sheet music files:" + + shortcut: + _: "Download {LINK}." + link: "sheet music files" + + midiProjectFiles: + heading: "Download MIDI/project files:" + + shortcut: + _: "Download {LINK}." + link: "MIDI/project files" + +trackList: + section: + withDuration: "{SECTION} ({DURATION}):" + + group: + _: "From {GROUP}:" + fromOther: "From somewhere else:" + + item: + withDuration: "({DURATION}) {TRACK}" + withDuration.withArtists: "({DURATION}) {TRACK} {BY}" + withArtists: "{TRACK} {BY}" + withArtists.by: "by {ARTISTS}" + rerelease: "{TRACK} (re-release)" + +misc: + alt: + albumCover: "album cover" + albumBanner: "album banner" + trackCover: "track cover" + artistAvatar: "artist avatar" + flashArt: "flash art" + + artistLink: + _: "{ARTIST}" + withContribution: "{ARTIST} ({CONTRIB})" + withExternalLinks: "{ARTIST} ({LINKS})" + withContribution.withExternalLinks: "{ARTIST} ({CONTRIB}) ({LINKS})" + + chronology: + seeArtistPages: "(See artist pages for chronology info!)" + withNavigation: "{HEADING} ({NAVIGATION})" + + heading: + coverArt: "{INDEX} cover art by {ARTIST}" + flash: "{INDEX} flash/game by {ARTIST}" + track: "{INDEX} track by {ARTIST}" + + external: + domain: "External ({DOMAIN})" + local: "Wiki Archive (local upload)" + deviantart: "DeviantArt" + instagram: "Instagram" + newgrounds: "Newgrounds" + patreon: "Patreon" + poetryFoundation: "Poetry Foundation" + soundcloud: "SoundCloud" + spotify: "Spotify" + tumblr: "Tumblr" + twitter: "Twitter" + wikipedia: "Wikipedia" + + bandcamp: + _: "Bandcamp" + domain: "Bandcamp ({DOMAIN})" + + mastodon: + _: "Mastodon" + domain: "Mastodon ({DOMAIN})" + + youtube: + _: "YouTube" + playlist: "YouTube (playlist)" + fullAlbum: "YouTube (full album)" + + flash: + bgreco: "{LINK} (HQ Audio)" + youtube: "{LINK} (on any device)" + homestuck: + page: "{LINK} (page {PAGE})" + secret: "{LINK} (secret page)" + + missingImage: "(This image file is missing)" + missingLinkContent: "(Missing link content)" + + nav: + previous: "Previous" + next: "Next" + info: "Info" + gallery: "Gallery" + + pageTitle: + _: "{TITLE}" + withWikiName: "{TITLE} | {WIKI_NAME}" + + skippers: + skipTo: "Skip to:" + + content: "Content" + header: "Header" + footer: "Footer" + + sidebar: + _: "Sidebar" + left: "Sidebar (left)" + right: "Sidebar (right)" + + tracks: "Tracks" + art: "Artworks" + flashes: "Flashes & Games" + contributors: "Contributors" + + references: "References..." + referencedBy: "Referenced by..." + samples: "Samples..." + sampledBy: "Sampled by..." + features: "Features..." + featuredIn: "Featured in..." + + lyrics: "Lyrics" + sheetMusicFiles: "Sheet music files" + midiProjectFiles: "MIDI/project files" + additionalFiles: "Additional files" + commentary: "Commentary" + artistCommentary: "Commentary" + + socialEmbed: + heading: "{WIKI_NAME} | {HEADING}" + + jumpTo: + _: "Jump to:" + withLinks: "Jump to: {LINKS}." + + contentWarnings: + _: "cw: {WARNINGS}" + reveal: "click to show" + + albumGrid: + noCoverArt: "{ALBUM}" + + details: + _: "({TRACKS}, {TIME})" + coverArtists: "(Illust. {ARTISTS})" + otherCoverArtists: "(With {ARTISTS})" + + albumGalleryGrid: + noCoverArt: "{NAME}" + + uiLanguage: "UI Language: {LANGUAGES" + +homepage: + title: "{TITLE}" + + news: + title: "News" + + entry: + viewRest: "(View rest of entry!)" + +albumSidebar: + trackList: + fallbackSectionName: "Track list" + item: "{TRACK}" + + group: + _: "{GROUP}" + withRange: "{GROUP} ({RANGE})" + + groupBox: + title: "{GROUP}" + next: "Next: {ALBUM}" + previous: "Previous: {ALBUM}" + +albumPage: + title: "{ALBUM}" + + nav: + album: "{ALBUM}" + randomTrack: "Random Track" + gallery: "Gallery" + commentary: "Commentary" + + socialEmbed: + heading: "{GROUP}" + title: "{ALBUM}" + + body: + withDuration: "{DURATION}." + withTracks: "{TRACKS}." + withReleaseDate: Released {DATE}. + withDuration.withTracks: "{DURATION}, {TRACKS}." + withDuration.withReleaseDate: "{DURATION}. Released {DATE}." + withTracks.withReleaseDate: "{TRACKS}. Released {DATE}." + withDuration.withTracks.withReleaseDate: "{DURATION}, {TRACKS}. Released {DATE}." + +albumGalleryPage: + title: "{ALBUM} - Gallery" + + statsLine: >- + {TRACKS} totaling {DURATION}. + + statsLine.withDate: >- + {TRACKS} totaling {DURATION}. Released {DATE}. + + coverArtistsLine: >- + All track artwork by {ARTISTS}. + + noTrackArtworksLine: >- + This album doesn't have any track artwork. + +albumCommentaryPage: + title: "{ALBUM} - Commentary" + + nav: + album: "Album: {ALBUM}" + + infoLine: >- + {WORDS} across {ENTRIES}. + + entry: + title: + albumCommentary: "Album commentary" + trackCommentary: "{TRACK}" + +artistPage: + title: "{ARTIST}" + + nav: + artist: "Artist: {ARTIST}" + + creditList: + album: + _: "{ALBUM}" + withDate: "{ALBUM} ({DATE})" + withDuration: "{ALBUM} ({DURATION})" + withDate.withDuration: "{ALBUM} ({DATE}; {DURATION})" + + flashAct: + _: "{ACT}" + withDate: "{ACT} ({DATE})" + withDateRange: "{ACT} ({DATE_RANGE})" + + entry: + rerelease: "{ENTRY} (re-release)" + withContribution: "{ENTRY} ({CONTRIBUTION})" + withArtists: "{ENTRY} (with {ARTISTS})" + withArtists.withContribution: "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})" + + track: + _: "{TRACK}" + withDuration: "({DURATION}) {TRACK}" + + album: + coverArt: "(cover art)" + wallpaperArt: "(wallpaper art)" + bannerArt: "(banner art)" + commentary: "(album commentary)" + + flash: + _: "{FLASH}" + + contributedDurationLine: >- + {ARTIST} has contributed {DURATION} of music shared on this wiki. + + musicGroupsLine: >- + Contributed music to groups: {GROUPS} + + artGroupsLine: >- + Contributed art to groups: {GROUPS} + + groupsLine: + item: + withCount: "{GROUP} ({COUNT})" + withDuration: "{GROUP} ({DURATION})" + + groupContributions: + title: + music: "Contributed music to groups:" + artworks: "Contributed artworks to groups:" + withSortButton: "{TITLE} ({SORT})" + + sorting: + count: "Sorting by count." + duration: "Sorting by duration." + + item: + countAccent: "({COUNT})" + durationAccent: "({DURATION})" + countDurationAccent: "({COUNT} — {DURATION})" + durationCountAccent: "({DURATION} — {COUNT})" + + trackList: + title: "Tracks" + + artList: + title: "Artworks" + + flashList: + title: "Flashes & Games" + + commentaryList: + title: "Commentary" + + viewArtGallery: + _: "View {LINK}!" + orBrowseList: "View {LINK}! Or browse the list:" + link: "art gallery" + +artistGalleryPage: + title: "{ARTIST} - Gallery" + + infoLine: >- + Contributed to {COVER_ARTS}. + +commentaryIndex: + title: "Commentary" + + infoLine: >- + {WORDS} across {ENTRIES}, in all. + + albumList: + title: "Choose an album:" + item: "{ALBUM} ({WORDS} across {ENTRIES})" + +flashIndex: + title: "Flashes & Games" + +flashPage: + title: "{FLASH}" + + nav: + flash: "{FLASH}" + +flashSidebar: + flashList: + flashesInThisAct: "Flashes in this act" + entriesInThisSection: "Entries in this section" + +groupSidebar: + title: "Groups" + + groupList: + category: "{CATEGORY}" + item: "{GROUP}" + +groupPage: + nav: + group: "Group: {GROUP}" + +groupInfoPage: + title: "{GROUP}" + + viewAlbumGallery: + _: "View {LINK}! Or browse the list:" + link: "album gallery" + + albumList: + title: "Albums" + + item: + _: "({YEAR}) {ALBUM}" + withoutYear: "{ALBUM}" + withAccent: "{ITEM} {ACCENT}" + otherGroupAccent: "(from {GROUP})" + +groupGalleryPage: + title: "{GROUP} - Gallery" + + infoLine: >- + {TRACKS} across {ALBUMS}, totaling {TIME}. + +listingIndex: + title: "Listings" + + infoLine: >- + {WIKI}: {TRACKS} across {ALBUMS}, totaling {DURATION}. + + exploreList: >- + Feel free to explore any of the listings linked below and in the sidebar! + +listingPage: + target: + album: "Albums" + artist: "Artists" + group: "Groups" + track: "Tracks" + tag: "Tags" + other: "Other" + + listingsFor: "Listings for {TARGET}: {LISTINGS}" + seeAlso: "Also check out: {LISTINGS}" + + listAlbums: + byName: + title: "Albums - by Name" + title.short: "...by Name" + item: "{ALBUM} ({TRACKS})" + + byTracks: + title: "Albums - by Tracks" + title.short: "...by Tracks" + item: "{ALBUM} ({TRACKS})" + + byDuration: + title: "Albums - by Duration" + title.short: "...by Duration" + item: "{ALBUM} ({DURATION})" + + byDate: + title: "Albums - by Date" + title.short: "...by Date" + item: "{ALBUM} ({DATE})" + + byDateAdded: + title: "Albums - by Date Added to Wiki" + title.short: "...by Date Added to Wiki" + chunk: + title: "{DATE}" + item: "{ALBUM}" + + listArtists: + byName: + title: "Artists - by Name" + title.short: "...by Name" + item: "{ARTIST} ({CONTRIBUTIONS})" + + byContribs: + title: "Artists - by Contributions" + title.short: "...by Contributions" + item: "{ARTIST} ({CONTRIBUTIONS})" + + byCommentary: + title: "Artists - by Commentary Entries" + title.short: "...by Commentary Entries" + item: "{ARTIST} ({ENTRIES})" + + byDuration: + title: "Artists - by Duration" + title.short: "...by Duration" + item: "{ARTIST} ({DURATION})" + + byLatest: + title: "Artists - by Latest Contribution" + title.short: "...by Latest Contribution" + + chunk: + title.album: "{ALBUM} ({DATE})" + title.flash: "{FLASH} ({DATE})" + item: "{ARTIST}" + + dateless: + title: "These artists' contributions aren't dated:" + item: "{ARTIST}" + + listGroups: + byName: + title: "Groups - by Name" + title.short: "...by Name" + item: "{GROUP} ({GALLERY})" + item.gallery: "Gallery" + + byCategory: + title: "Groups - by Category" + title.short: "...by Category" + + chunk: + title: "{CATEGORY}" + item: "{GROUP} ({GALLERY})" + item.gallery: "Gallery" + + byAlbums: + title: "Groups - by Albums" + title.short: "...by Albums" + item: "{GROUP} ({ALBUMS})" + + byTracks: + title: "Groups - by Tracks" + title.short: "...by Tracks" + item: "{GROUP} ({TRACKS})" + + byDuration: + title: "Groups - by Duration" + title.short: "...by Duration" + item: "{GROUP} ({DURATION})" + + byLatest: + title: "Groups - by Latest Album" + title.short: "...by Latest Album" + item: "{GROUP} ({DATE})" + + listTracks: + byName: + title: "Tracks - by Name" + title.short: "...by Name" + item: "{TRACK}" + + byAlbum: + title: "Tracks - by Album" + title.short: "...by Album" + + chunk: + title: "{ALBUM}" + item: "{TRACK}" + + byDate: + title: "Tracks - by Date" + title.short: "...by Date" + + chunk: + title: "{ALBUM} ({DATE})" + item: "{TRACK}" + item.rerelease: "{TRACK} (re-release)" + + byDuration: + title: "Tracks - by Duration" + title.short: "...by Duration" + item: "{TRACK} ({DURATION})" + + byDurationInAlbum: + title: "Tracks - by Duration (in Album)" + title.short: "...by Duration (in Album)" + + chunk: + title: "{ALBUM}" + item: "{TRACK} ({DURATION})" + + byTimesReferenced: + title: "Tracks - by Times Referenced" + title.short: "...by Times Referenced" + item: "{TRACK} ({TIMES_REFERENCED})" + + inFlashes.byAlbum: + title: "Tracks - in Flashes & Games (by Album)" + title.short: "...in Flashes & Games (by Album)" + + chunk: + title: "{ALBUM}" + item: "{TRACK} (in {FLASHES})" + + inFlashes.byFlash: + title: "Tracks - in Flashes & Games (by Flash)" + title.short: "...in Flashes & Games (by Flash)" + + chunk: + title: "{FLASH}" + item: "{TRACK} (from {ALBUM})" + + withLyrics: + title: "Tracks - with Lyrics" + title.short: "...with Lyrics" + + chunk: + title: "{ALBUM}" + title.withDate: "{ALBUM} ({DATE})" + item: "{TRACK}" + + withSheetMusicFiles: + title: "Tracks - with Sheet Music Files" + title.short: "...with Sheet Music Files" + + chunk: + title: "{ALBUM}" + title.withDate: "{ALBUM} ({DATE})" + item: "{TRACK}" + + withMidiProjectFiles: + title: "Tracks - with MIDI & Project Files" + title.short: "...with MIDI & Project Files" + + chunk: + title: "{ALBUM}" + title.withDate: "{ALBUM} ({DATE})" + item: "{TRACK}" + + listTags: + byName: + title: "Tags - by Name" + title.short: "...by Name" + item: "{TAG} ({TIMES_USED})" + + byUses: + title: "Tags - by Uses" + title.short: "...by Uses" + item: "{TAG} ({TIMES_USED})" + + other: + allSheetMusic: + title: "All Sheet Music" + title.short: "All Sheet Music" + albumFiles: "Album sheet music:" + + file: + _: "{TITLE}" + withMultipleFiles: "{TITLE} ({FILES})" + + allMidiProjectFiles: + title: "All MIDI/Project Files" + title.short: "All MIDI/Project Files" + albumFiles: "Album MIDI/project files:" + + file: + _: "{TITLE}" + withMultipleFiles: "{TITLE} ({FILES})" + + allAdditionalFiles: + title: "All Additional Files" + title.short: "All Additional Files" + albumFiles: "Album additional files:" + + file: + _: "{TITLE}" + withMultipleFiles: "{TITLE} ({FILES})" + + randomPages: + title: "Random Pages" + title.short: "Random Pages" + + chooseLinkLine: >- + Choose a link to go to a random page in that category or album! + If your browser doesn't support relatively modern JavaScript + or you've disabled it, these links won't work - sorry. + + dataLoadingLine: >- + (Data files are downloading in the background! Please wait for data to load.) + + dataLoadedLine: >- + (Data files have finished being downloaded. The links should work!) + + misc: + _: "Miscellaneous:" + randomArtist: "Random Artist" + atLeastTwoContributions: "at least 2 contributions" + randomAlbumWholeSite: "Random Album (whole site)" + randomTrackWholeSite: "Random Track (whole site)" + + group: + _: "From {GROUP}: ({RANDOM_ALBUM}, {RANDOM_TRACK})" + randomAlbum: "Random Album" + randomTrack: "Random Track" + + album: "{ALBUM}" + + misc: + trackContributors: "Track Contributors" + artContributors: "Art Contributors" + flashContributors: "Flash & Game Contributors" + artAndFlashContributors: "Art & Flash Contributors" + +newsIndex: + title: "News" + + entry: + viewRest: "(View rest of entry!)" + +newsEntryPage: + title: "{ENTRY}" + published: "(Published {DATE}.)" + +redirectPage: + title: "Moved to {TITLE}" + + infoLine: >- + This page has been moved to {TARGET}. + +tagPage: + title: "{TAG}" + + nav: + tag: "Tag: {TAG}" + + infoLine: >- + Appears in {COVER_ARTS}. + +trackPage: + title: "{TRACK}" + + nav: + random: "Random" + + track: + _: "{TRACK}" + withNumber: "{NUMBER}. {TRACK}" + + referenceList: + fandom: "Fandom:" + official: "Official:" + + socialEmbed: + heading: "{ALBUM}" + title: "{TRACK}" + + body: + withArtists.withCoverArtists: "By {ARTISTS}; art by {COVER_ARTISTS}." + withArtists: "By {ARTISTS}." + withCoverArtists: "Art by {COVER_ARTISTS}." -- cgit 1.3.0-6-gf8a5 From a10e27f93c8e7965c51b2e0372a7f4b19640452e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 7 Nov 2023 08:09:14 -0400 Subject: upd8: infer custom default language from internal default code --- src/upd8.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index c011b660..7d7e48b9 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1233,11 +1233,6 @@ async function main() { timeStart: Date.now(), }); - const customDefaultLanguage = - (wikiData.wikiInfo.defaultLanguage - ? languages[wikiData.wikiInfo.defaultLanguage] - : null); - let finalDefaultLanguage; let finalDefaultLanguageWatcher; let finalDefaultLanguageAnnotation; @@ -1262,14 +1257,25 @@ async function main() { return false; } - customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; - logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; + // The custom default language will be the new one providing fallback strings + // for other languages, but on its own, it still might not be a complete list + // of strings. So it falls back to the internal default language - which won't + // otherwise be presented on the site. + customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; + finalDefaultLanguage = customDefaultLanguage; finalDefaultLanguageWatcher = customLanguageWatchers.find(({language}) => language === customDefaultLanguage); finalDefaultLanguageAnnotation = `using wiki-specified custom default language`; + } else if (languages[internalDefaultLanguage.code]) { + const customDefaultLanguage = languages[internalDefaultLanguage.code]; + customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; + finalDefaultLanguage = customDefaultLanguage; + finalDefaultLanguageWatcher = + customLanguageWatchers.find(({language}) => language === customDefaultLanguage); + finalDefaultLanguageAnnotation = `using inferred custom default language`; } else { languages[internalDefaultLanguage.code] = internalDefaultLanguage; -- cgit 1.3.0-6-gf8a5 From 4001e3c16c0acacc6c6d89589e57996701058dc0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 7 Nov 2023 08:17:01 -0400 Subject: upd8: handle internal language updates in custom default language --- src/upd8.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 7d7e48b9..91f9a090 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -1259,19 +1259,12 @@ async function main() { logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; - // The custom default language will be the new one providing fallback strings - // for other languages, but on its own, it still might not be a complete list - // of strings. So it falls back to the internal default language - which won't - // otherwise be presented on the site. - customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; - finalDefaultLanguage = customDefaultLanguage; finalDefaultLanguageWatcher = customLanguageWatchers.find(({language}) => language === customDefaultLanguage); finalDefaultLanguageAnnotation = `using wiki-specified custom default language`; } else if (languages[internalDefaultLanguage.code]) { const customDefaultLanguage = languages[internalDefaultLanguage.code]; - customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; finalDefaultLanguage = customDefaultLanguage; finalDefaultLanguageWatcher = customLanguageWatchers.find(({language}) => language === customDefaultLanguage); @@ -1284,6 +1277,12 @@ async function main() { finalDefaultLanguageAnnotation = `no custom default language specified`; } + const inheritStringsFromInternalLanguage = () => { + if (finalDefaultLanguage === internalDefaultLanguage) return; + const {strings: inheritedStrings} = internalDefaultLanguage; + Object.assign(finalDefaultLanguage, {inheritedStrings}); + }; + const inheritStringsFromDefaultLanguage = () => { const {strings: inheritedStrings} = finalDefaultLanguage; for (const language of Object.values(languages)) { @@ -1292,8 +1291,19 @@ async function main() { } }; - inheritStringsFromDefaultLanguage(); + // The custom default language, if set, will be the new one providing fallback + // strings for other languages. But on its own, it still might not be a complete + // list of strings - so it falls back to the internal default language, which + // won't otherwise be presented in the build. + if (finalDefaultLanguage !== internalDefaultLanguage) { + inheritStringsFromInternalLanguage(); + internalDefaultLanguageWatcher.on('update', () => { + inheritStringsFromInternalLanguage(); + inheritStringsFromDefaultLanguage(); + }); + } + inheritStringsFromDefaultLanguage(); finalDefaultLanguageWatcher.on('update', () => { inheritStringsFromDefaultLanguage(); }); -- cgit 1.3.0-6-gf8a5 From 8901b8bb4c9966945519de9a0b7115fb9c5a9564 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 7 Nov 2023 08:19:09 -0400 Subject: upd8: quick eslint fixes --- src/upd8.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/upd8.js b/src/upd8.js index 91f9a090..ea4629ee 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -39,7 +39,7 @@ import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; import {displayCompositeCacheAnalysis} from '#composite'; -import {processLanguageFile, watchLanguageFile} from '#language'; +import {watchLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; import {empty, showAggregate, withEntries} from '#sugar'; @@ -56,7 +56,6 @@ import { logError, parseOptions, progressCallAll, - progressPromiseAll, } from '#cli'; import genThumbs, { -- cgit 1.3.0-6-gf8a5 From 505a2bf05216de928d667655ee2670ad6c3ff46d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 7 Nov 2023 08:27:19 -0400 Subject: upd8: stub --no-input option --- src/upd8.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/upd8.js b/src/upd8.js index ea4629ee..40683744 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -290,6 +290,11 @@ async function main() { type: 'flag', }, + 'no-input': { + help: `Don't wait on input from stdin - assume the device is headless`, + type: 'flag', + }, + // Want sweet, sweet trace8ack info in aggreg8te error messages? This // will print all the juicy details (or at least the first relevant // line) right to your output, 8ut also pro8a8ly give you a headache @@ -456,6 +461,7 @@ async function main() { const thumbsOnly = cliOptions['thumbs-only'] ?? false; const skipReferenceValidation = cliOptions['skip-reference-validation'] ?? false; const noBuild = cliOptions['no-build'] ?? false; + const noInput = cliOptions['no-input'] ?? false; showStepStatusSummary = cliOptions['show-step-summary'] ?? false; -- 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 +- src/upd8.js | 318 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 204 insertions(+), 116 deletions(-) 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, diff --git a/src/upd8.js b/src/upd8.js index 40683744..764ee0c3 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -39,7 +39,7 @@ import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; import {displayCompositeCacheAnalysis} from '#composite'; -import {watchLanguageFile} from '#language'; +import {processLanguageFile, watchLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; import {empty, showAggregate, withEntries} from '#sugar'; @@ -295,6 +295,13 @@ async function main() { type: 'flag', }, + 'no-language-reloading': { + help: `Don't reload language files while the build is running\n\nApplied by default for --static-build`, + type: 'flag', + }, + + 'no-language-reload': {alias: 'no-language-reloading'}, + // Want sweet, sweet trace8ack info in aggreg8te error messages? This // will print all the juicy details (or at least the first relevant // line) right to your output, 8ut also pro8a8ly give you a headache @@ -462,6 +469,7 @@ async function main() { const skipReferenceValidation = cliOptions['skip-reference-validation'] ?? false; const noBuild = cliOptions['no-build'] ?? false; const noInput = cliOptions['no-input'] ?? false; + let noLanguageReloading = cliOptions['no-language-reloading'] ?? null; // Will get default later. showStepStatusSummary = cliOptions['show-step-summary'] ?? false; @@ -572,12 +580,24 @@ async function main() { } if (noBuild) { + logInfo`Won't generate any site or page files this run (--no-build passed).`; + Object.assign(stepStatusSummary.performBuild, { status: STATUS_NOT_APPLICABLE, annotation: `--no-build provided`, }); + } else if (usingDefaultBuildMode) { + logInfo`No build mode specified, will use default: ${selectedBuildModeFlag}`; + } else { + logInfo`Will use specified build mode: ${selectedBuildModeFlag}`; } + noLanguageReloading ??= + ({ + 'static-build': true, + 'live-dev-server': false, + })[selectedBuildModeFlag]; + if (skipThumbs && thumbsOnly) { logInfo`Well, you've put yourself rather between a roc and a hard place, hmmmm?`; return false; @@ -771,14 +791,6 @@ async function main() { thumbsCache = result.cache; } - if (noBuild) { - logInfo`Not generating any site or page files this run (--no-build passed).`; - } else if (usingDefaultBuildMode) { - logInfo`No build mode specified, using default: ${selectedBuildModeFlag}`; - } else { - logInfo`Using specified build mode: ${selectedBuildModeFlag}`; - } - if (showInvalidPropertyAccesses) { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } @@ -1090,35 +1102,54 @@ async function main() { }); let internalDefaultLanguage; + let internalDefaultLanguageWatcher; - const internalDefaultLanguageWatcher = - watchLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); + const internalDefaultStringsFile = path.join(__dirname, DEFAULT_STRINGS_FILE); - try { - await new Promise((resolve, reject) => { - const watcher = internalDefaultLanguageWatcher; - - const onReady = () => { - watcher.removeListener('ready', onReady); - watcher.removeListener('error', onError); - resolve(); - }; - - const onError = error => { - watcher.removeListener('ready', onReady); - watcher.removeListener('error', onError); - watcher.close(); - reject(error); - }; - - watcher.on('ready', onReady); - watcher.on('error', onError); - }); + let errorLoadingInternalDefaultLanguage = false; - internalDefaultLanguage = internalDefaultLanguageWatcher.language; - } catch (_error) { - // No need to display the error here - it's already printed by - // watchLanguageFile. + if (noLanguageReloading) { + internalDefaultLanguageWatcher = null; + + try { + internalDefaultLanguage = await processLanguageFile(internalDefaultStringsFile); + } catch (error) { + niceShowAggregate(error); + errorLoadingInternalDefaultLanguage = true; + } + } else { + internalDefaultLanguageWatcher = watchLanguageFile(internalDefaultStringsFile); + + try { + await new Promise((resolve, reject) => { + const watcher = internalDefaultLanguageWatcher; + + const onReady = () => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + resolve(); + }; + + const onError = error => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + watcher.close(); + reject(error); + }; + + watcher.on('ready', onReady); + watcher.on('error', onError); + }); + + internalDefaultLanguage = internalDefaultLanguageWatcher.language; + } catch (_error) { + // No need to display the error here - it's already printed by + // watchLanguageFile. + errorLoadingInternalDefaultLanguage = true; + } + } + + if (errorLoadingInternalDefaultLanguage) { logError`There was an error reading the internal language file.`; fileIssue(); @@ -1131,8 +1162,10 @@ async function main() { return false; } - // Bypass node.js special-case handling for uncaught error events - internalDefaultLanguageWatcher.on('error', () => {}); + if (!noLanguageReloading) { + // Bypass node.js special-case handling for uncaught error events + internalDefaultLanguageWatcher.on('error', () => {}); + } Object.assign(stepStatusSummary.loadInternalDefaultLanguage, { status: STATUS_DONE_CLEAN, @@ -1153,81 +1186,118 @@ async function main() { pathStyle: 'device', }); - customLanguageWatchers = - languageDataFiles.map(file => { - const watcher = watchLanguageFile(file); + let errorLoadingCustomLanguages = false; - // Bypass node.js special-case handling for uncaught error events - watcher.on('error', () => {}); + if (noLanguageReloading) { + languages = {}; - return watcher; - }); + const results = + await Promise.allSettled( + languageDataFiles + .map(file => processLanguageFile(file))); - const waitingOnWatchers = new Set(customLanguageWatchers); - - const initialResults = - await Promise.allSettled( - customLanguageWatchers.map(watcher => - new Promise((resolve, reject) => { - const onReady = () => { - watcher.removeListener('ready', onReady); - watcher.removeListener('error', onError); - waitingOnWatchers.delete(watcher); - resolve(); - }; - - const onError = error => { - watcher.removeListener('ready', onReady); - watcher.removeListener('error', onError); - reject(error); - }; - - watcher.on('ready', onReady); - watcher.on('error', onError); - }))); - - if (initialResults.some(({status}) => status === 'rejected')) { - logWarn`There were errors loading custom languages from the language path`; - logWarn`provided: ${langPath}`; - - if (noInput) { - logError`Failed to load language files. Please investigate these, or don't provide`; - logError`--lang-path (or HSMUSIC_LANG) and build again.`; - - Object.assign(stepStatusSummary.loadLanguageFiles, { - status: STATUS_FATAL_ERROR, - annotation: `see log for details`, - timeEnd: Date.now(), + for (const {status, value: language, reason: error} of results) { + if (status === 'rejected') { + errorLoadingCustomLanguages = true; + niceShowAggregate(error); + } else { + languages[language.code] = language; + } + } + } else watchCustomLanguages: { + customLanguageWatchers = + languageDataFiles.map(file => { + const watcher = watchLanguageFile(file); + + // Bypass node.js special-case handling for uncaught error events + watcher.on('error', () => {}); + + return watcher; }); - return false; - } + const waitingOnWatchers = new Set(customLanguageWatchers); + + const initialResults = + await Promise.allSettled( + customLanguageWatchers + .map(watcher => new Promise((resolve, reject) => { + const onReady = () => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + waitingOnWatchers.delete(watcher); + resolve(); + }; + + const onError = error => { + watcher.removeListener('ready', onReady); + watcher.removeListener('error', onError); + reject(error); + }; + + watcher.on('ready', onReady); + watcher.on('error', onError); + }))); + + if (initialResults.some(({status}) => status === 'rejected')) { + logWarn`There were errors loading custom languages from the language path`; + logWarn`provided: ${langPath}`; + + if (noInput) { + internalDefaultLanguageWatcher.close(); + + for (const watcher of Object.values(customLanguageWatchers)) { + watcher.close(); + } - logWarn`The build should start automatically if you investigate these.`; - logWarn`Or, exit by pressing ^C here (control+C) and run again without`; - logWarn`providing ${'--lang-path'} (or ${'HSMUSIC_LANG'}) to build without custom`; - logWarn`languages.`; - - await new Promise(resolve => { - for (const watcher of waitingOnWatchers) { - watcher.once('ready', () => { - waitingOnWatchers.remove(watcher); - if (empty(waitingOnWatchers)) { - resolve(); - } - }); + errorLoadingCustomLanguages = true; + break watchCustomLanguages; } - }); + + logWarn`The build should start automatically if you investigate these.`; + logWarn`Or, exit by pressing ^C here (control+C) and run again without`; + logWarn`providing ${'--lang-path'} (or ${'HSMUSIC_LANG'}) to build without custom`; + logWarn`languages.`; + + await new Promise(resolve => { + for (const watcher of waitingOnWatchers) { + watcher.once('ready', () => { + waitingOnWatchers.remove(watcher); + if (empty(waitingOnWatchers)) { + resolve(); + } + }); + } + }); + } + + languages = + Object.fromEntries( + customLanguageWatchers + .map(({language}) => [language.code, language])); } - languages = - Object.fromEntries( - customLanguageWatchers - .map(watcher => [watcher.language.code, watcher.language])); + if (errorLoadingCustomLanguages) { + logError`Failed to load language files. Please investigate these, or don't provide`; + logError`--lang-path (or HSMUSIC_LANG) and build again.`; + + Object.assign(stepStatusSummary.loadLanguageFiles, { + status: STATUS_FATAL_ERROR, + annotation: `see log for details`, + timeEnd: Date.now(), + }); + + return false; + } Object.assign(stepStatusSummary.loadLanguageFiles, { status: STATUS_DONE_CLEAN, timeEnd: Date.now(), + annotation: + (noLanguageReloading + ? (selectedBuildModeFlag === 'static-build' + ? `loaded statically, default for --static-build` + : `loaded statically, --no-language-reloading provided`) + : `watching for changes`), }); } else { languages = {}; @@ -1265,24 +1335,40 @@ async function main() { logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; finalDefaultLanguage = customDefaultLanguage; - finalDefaultLanguageWatcher = - customLanguageWatchers.find(({language}) => language === customDefaultLanguage); finalDefaultLanguageAnnotation = `using wiki-specified custom default language`; + + if (!noLanguageReloading) { + finalDefaultLanguageWatcher = + customLanguageWatchers + .find(({language}) => language === customDefaultLanguage); + } } else if (languages[internalDefaultLanguage.code]) { const customDefaultLanguage = languages[internalDefaultLanguage.code]; + finalDefaultLanguage = customDefaultLanguage; - finalDefaultLanguageWatcher = - customLanguageWatchers.find(({language}) => language === customDefaultLanguage); finalDefaultLanguageAnnotation = `using inferred custom default language`; + + if (!noLanguageReloading) { + finalDefaultLanguageWatcher = + customLanguageWatchers + .find(({language}) => language === customDefaultLanguage); + } } else { languages[internalDefaultLanguage.code] = internalDefaultLanguage; finalDefaultLanguage = internalDefaultLanguage; - finalDefaultLanguageWatcher = internalDefaultLanguageWatcher; finalDefaultLanguageAnnotation = `no custom default language specified`; + + if (!noLanguageReloading) { + finalDefaultLanguageWatcher = internalDefaultLanguageWatcher; + } } const inheritStringsFromInternalLanguage = () => { + // The custom default language, if set, will be the new one providing fallback + // strings for other languages. But on its own, it still might not be a complete + // list of strings - so it falls back to the internal default language, which + // won't otherwise be presented in the build. if (finalDefaultLanguage === internalDefaultLanguage) return; const {strings: inheritedStrings} = internalDefaultLanguage; Object.assign(finalDefaultLanguage, {inheritedStrings}); @@ -1296,22 +1382,24 @@ async function main() { } }; - // The custom default language, if set, will be the new one providing fallback - // strings for other languages. But on its own, it still might not be a complete - // list of strings - so it falls back to the internal default language, which - // won't otherwise be presented in the build. if (finalDefaultLanguage !== internalDefaultLanguage) { inheritStringsFromInternalLanguage(); - internalDefaultLanguageWatcher.on('update', () => { - inheritStringsFromInternalLanguage(); - inheritStringsFromDefaultLanguage(); - }); } inheritStringsFromDefaultLanguage(); - finalDefaultLanguageWatcher.on('update', () => { - inheritStringsFromDefaultLanguage(); - }); + + if (!noLanguageReloading) { + if (finalDefaultLanguage !== internalDefaultLanguage) { + internalDefaultLanguageWatcher.on('update', () => { + inheritStringsFromInternalLanguage(); + inheritStringsFromDefaultLanguage(); + }); + } + + finalDefaultLanguageWatcher.on('update', () => { + inheritStringsFromDefaultLanguage(); + }); + } logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`; -- cgit 1.3.0-6-gf8a5 From 9bf4b59f608022ab0ca2cadc4acff7bb6760ebd2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 8 Nov 2023 19:54:01 -0400 Subject: content, repl: minor fixes and tweaks --- src/content/dependencies/generatePageLayout.js | 3 ++- src/content/dependencies/linkExternal.js | 25 ++++++++++++++++++++++--- src/repl.js | 4 +++- src/upd8.js | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index cd831ba7..72dfbae5 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -449,7 +449,8 @@ export default { {[html.onlyIfContent]: true, class: 'skipper-list'}, processSkippers([ {id: 'tracks', string: 'tracks'}, - {id: 'art', string: 'flashes'}, + {id: 'art', string: 'artworks'}, + {id: 'flashes', string: 'flashes'}, {id: 'contributors', string: 'contributors'}, {id: 'references', string: 'references'}, {id: 'referenced-by', string: 'referencedBy'}, diff --git a/src/content/dependencies/linkExternal.js b/src/content/dependencies/linkExternal.js index 73c656e3..5de612e2 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -3,10 +3,20 @@ const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com']; const MASTODON_DOMAINS = ['types.pl']; export default { - extraDependencies: ['html', 'language'], + extraDependencies: ['html', 'language', 'wikiData'], - data(url) { - return {url}; + sprawl: ({wikiInfo}) => ({wikiInfo}), + + data(sprawl, url) { + const data = {url}; + + const {canonicalBase} = sprawl.wikiInfo; + if (canonicalBase) { + const {hostname: canonicalDomain} = new URL(canonicalBase); + Object.assign(data, {canonicalDomain}); + } + + return data; }, slots: { @@ -20,6 +30,7 @@ export default { let isLocal; let domain; let pathname; + try { const url = new URL(data.url); domain = url.hostname; @@ -28,6 +39,14 @@ export default { // No support for relative local URLs yet, sorry! (I.e, local URLs must // be absolute relative to the domain name in order to work.) isLocal = true; + domain = null; + pathname = null; + } + + // isLocal also applies for URLs which match the 'Canonical Base' under + // wiki-info.yaml, if present. + if (data.canonicalDomain && domain === data.canonicalDomain) { + isLocal = true; } const link = html.tag('a', diff --git a/src/repl.js b/src/repl.js index ead01567..7a6f5c45 100644 --- a/src/repl.js +++ b/src/repl.js @@ -16,6 +16,8 @@ import * as serialize from '#serialize'; import * as sugar from '#sugar'; import * as wikiDataUtils from '#wiki-data'; +import {DEFAULT_STRINGS_FILE} from './upd8.js'; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); export async function getContextAssignments({ @@ -46,7 +48,7 @@ export async function getContextAssignments({ language = await processLanguageFile( path.join( path.dirname(fileURLToPath(import.meta.url)), - 'strings-default.json')); + DEFAULT_STRINGS_FILE)); } catch (error) { console.error(error); logWarn`Failed to create Language object`; diff --git a/src/upd8.js b/src/upd8.js index 868bfee4..24d0b92b 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -93,7 +93,7 @@ try { const BUILD_TIME = new Date(); -const DEFAULT_STRINGS_FILE = 'strings-default.yaml'; +export const DEFAULT_STRINGS_FILE = 'strings-default.yaml'; const STATUS_NOT_STARTED = `not started`; const STATUS_NOT_APPLICABLE = `not applicable`; -- cgit 1.3.0-6-gf8a5 From b62622d3cd8ffe1ed517ceb873d9352943c4a601 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 8 Nov 2023 19:55:37 -0400 Subject: content: strings-default.yaml documentation and clean-up --- src/strings-default.yaml | 880 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 799 insertions(+), 81 deletions(-) diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 2fd905d1..a5a09280 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1,16 +1,41 @@ meta.languageCode: en meta.languageName: English +# +# count: +# +# This covers pretty much any time that a specific number of things +# is represented! It's sectioned... like an alignment chart meme... +# +# First counting specific wiki objects, then more abstract stuff, +# and finally numerical representations of kinds of quantities that +# aren't really "counting", per se. +# +# These must be filled out according to the Unicode Common Locale +# Data Repository (Unicode CLDR). Check out info on their site: +# https://cldr.unicode.org +# +# Specifically, you'll want to look into the Plural Rules for your +# language. Here's a summary on what those even are: +# https://cldr.unicode.org/index/cldr-spec/plural-rules +# +# CLDR's charts are available online! This should bring you to the +# most recent table of plural rules: +# https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html +# +# Counting is generally done with the "Type: cardinal" section on +# that chart - for example, if the chart lists "one", "many", and +# "other" under the cardinal plural rules for your language, then +# your job is to fill in the correct pluralizations of the specific +# term for each of those. +# +# If you adore technical details or want to better understand the +# "Rules" column, you'll want to check out the syntax outline here: +# https://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules +# count: - tracks: - _: "{TRACKS}" - withUnit: - zero: "" - one: "{TRACKS} track" - two: "" - few: "" - many: "" - other: "{TRACKS} tracks" + + # Count things and objects additionalFiles: _: "{FILES}" @@ -82,26 +107,37 @@ count: many: "" other: "{FLASHES} flashes & games" - timesReferenced: - _: "{TIMES_REFERENCED}" + tracks: + _: "{TRACKS}" withUnit: zero: "" - one: "{TIMES_REFERENCED} time referenced" + one: "{TRACKS} track" two: "" few: "" many: "" - other: "{TIMES_REFERENCED} times referenced" + other: "{TRACKS} tracks" - words: - _: "{WORDS}" - thousand: "{WORDS}k" + # Count more abstract stuff + + days: + _: "{DAYS}" withUnit: zero: "" - one: "{WORDS} word" + one: "{DAYS} day" two: "" few: "" many: "" - other: "{WORDS} words" + other: "{DAYS} days" + + timesReferenced: + _: "{TIMES_REFERENCED}" + withUnit: + zero: "" + one: "{TIMES_REFERENCED} time referenced" + two: "" + few: "" + many: "" + other: "{TIMES_REFERENCED} times referenced" timesUsed: _: "{TIMES_USED}" @@ -113,13 +149,18 @@ count: many: "" other: "used {TIMES_USED} times" - index: - zero: "" - one: "{INDEX}st" - two: "{INDEX}nd" - few: "{INDEX}rd" - many: "" - other: "{INDEX}th" + words: + _: "{WORDS}" + thousand: "{WORDS}k" + withUnit: + zero: "" + one: "{WORDS} word" + two: "" + few: "" + many: "" + other: "{WORDS} words" + + # Numerical things that aren't exactly counting, per se duration: missing: "_:__" @@ -138,7 +179,28 @@ count: kilobytes: "{KILOBYTES} kB" bytes: "{BYTES} bytes" + # Indexes in a list + # These use "Type: ordinal" on CLDR's chart of plural rules. + + index: + zero: "" + one: "{INDEX}st" + two: "{INDEX}nd" + few: "{INDEX}rd" + many: "" + other: "{INDEX}th" + +# +# releaseInfo: +# +# This covers a lot of generic strings - they're used in a variety +# of contexts. They're sorted below with descriptions first, then +# actions further down. +# releaseInfo: + + # Descriptions + by: "By {ARTISTS}." from: "From {ALBUM}." @@ -152,6 +214,33 @@ releaseInfo: duration: "Duration: {DURATION}." + contributors: "Contributors:" + lyrics: "Lyrics:" + note: "Context notes:" + + alsoReleasedAs: + _: "Also released as:" + item: "{TRACK} (on {ALBUM})" + + tracksReferenced: "Tracks that {TRACK} references:" + tracksThatReference: "Tracks that reference {TRACK}:" + tracksSampled: "Tracks that {TRACK} samples:" + tracksThatSample: "Tracks that sample {TRACK}:" + + flashesThatFeature: + _: "Flashes & games that feature {TRACK}:" + item: + _: "{FLASH}" + asDifferentRelease: "{FLASH} (as {TRACK})" + + tracksFeatured: "Tracks that {FLASH} features:" + + artTags: + _: "Tags:" + inline: "Tags: {TAGS}" + + # Actions + viewCommentary: _: "View {LINK}!" link: "commentary page" @@ -184,36 +273,10 @@ releaseInfo: _: "Read {LINK}." link: "artist commentary" - alsoReleasedAs: - _: "Also released as:" - item: "{TRACK} (on {ALBUM})" - - contributors: "Contributors:" - - tracksReferenced: "Tracks that {TRACK} references:" - tracksThatReference: "Tracks that reference {TRACK}:" - tracksSampled: "Tracks that {TRACK} samples:" - tracksThatSample: "Tracks that sample {TRACK}:" - - flashesThatFeature: - _: "Flashes & games that feature {TRACK}:" - item: - _: "{FLASH}" - asDifferentRelease: "{FLASH} (as {TRACK})" - - tracksFeatured: "Tracks that {FLASH} features:" - - lyrics: "Lyrics:" - note: "Context notes:" - artistCommentary: _: "Artist commentary:" seeOriginalRelease: "See {ORIGINAL}!" - artTags: - _: "Tags:" - inline: "Tags: {TAGS}" - additionalFiles: heading: "View or download {ADDITIONAL_FILES}:" @@ -243,6 +306,15 @@ releaseInfo: _: "Download {LINK}." link: "MIDI/project files" +# +# trackList: +# +# A list of tracks! These are used pretty much across the wiki. +# Track lists can be split into sections, groups, or not split at +# all. "Track sections" are divisions in the list which suit the +# album as a whole, like if it has multiple discs or bonus tracks. +# "Groups" are actual group objects (see ex. groupInfoPage). +# trackList: section: withDuration: "{SECTION} ({DURATION}):" @@ -258,7 +330,18 @@ trackList: withArtists.by: "by {ARTISTS}" rerelease: "{TRACK} (re-release)" +# +# misc: +# +# These cover a whole host of general things across the wiki, and +# aren't specially organized. Sorry! See each entry for details. +# misc: + + # alt: + # Fallback text for the alt text of images and artworks - these + # are read aloud by screen readers. + alt: albumCover: "album cover" albumBanner: "album banner" @@ -266,14 +349,47 @@ misc: artistAvatar: "artist avatar" flashArt: "flash art" + # artistLink: + # Artist links have special accents which are made conditionally + # present in a variety of places across the wiki. + artistLink: _: "{ARTIST}" + + # Contribution to a track, artwork, or other thing. withContribution: "{ARTIST} ({CONTRIB})" + + # External links to visit the artist's own websites or profiles. withExternalLinks: "{ARTIST} ({LINKS})" + + # Combination of above. withContribution.withExternalLinks: "{ARTIST} ({CONTRIB}) ({LINKS})" + # chronology: + # + # "Chronology links" are a section that appear in the nav bar for + # most things with individual contributors across the wiki! These + # allow for quick navigation between older and newer releases of + # a given artist, or seeing at a glance how many contributions an + # artist made before the one you're currently viewing. + # + # Chronology information is described for each artist and shows + # the kind of thing which is being contributed to, since all of + # the entries are displayed together in one list. + # + chronology: + + # seeArtistPages: + # If the thing you're viewing has a lot of contributors, their + # chronology info will be exempt from the nav bar, which'll + # show this message instead. + seeArtistPages: "(See artist pages for chronology info!)" + + # withNavigation: + # Navigation refers to previous/next links. + withNavigation: "{HEADING} ({NAVIGATION})" heading: @@ -281,9 +397,26 @@ misc: flash: "{INDEX} flash/game by {ARTIST}" track: "{INDEX} track by {ARTIST}" + # external: + # Links which will generally bring you somewhere off of the wiki. + # The list of sites is hard-coded into the wiki software, so it + # may be out of date or missing ones that are relevant to another + # wiki - sorry! + external: + + # domain: + # General domain when one the URL doesn't match one of the + # sites below. + domain: "External ({DOMAIN})" + + # local: + # Files which are locally available on the wiki (under its media + # directory). + local: "Wiki Archive (local upload)" + deviantart: "DeviantArt" instagram: "Instagram" newgrounds: "Newgrounds" @@ -315,19 +448,53 @@ misc: page: "{LINK} (page {PAGE})" secret: "{LINK} (secret page)" + # missingImage: + # Fallback text displayed in an image when it's sourced to a file + # that isn't available under the wiki's media directory. While it + # shouldn't display on a correct build of the site, it may be + # displayed when working on data locally (for example adding a + # track before you've brought in its cover art). + missingImage: "(This image file is missing)" + + # misingLinkContent: + # Generic fallback when a link is completely missing its content. + # This is only to make those links visible in the first place - + # it should never appear on the website and is only intended for + # debugging. + missingLinkContent: "(Missing link content)" + # nav: + # Generic navigational elements. These usually only appear in the + # wiki's nav bar, at the top of the page. + nav: previous: "Previous" next: "Next" info: "Info" gallery: "Gallery" + # pageTitle: + # Title set under the page's HTML element, which is + # displayed in the browser tab bar, bookmarks list, etc. + pageTitle: _: "{TITLE}" withWikiName: "{TITLE} | {WIKI_NAME}" + # skippers: + # + # These are navigational links that only show up when you're + # navigating the wiki using the Tab key (or some other method of + # "tabbing" between links and interactive elements). They move + # the browser's nav focus to the selected element when pressed. + # + # There are a lot of definitions here, and they're mostly shown + # conditionally, based on the elements that are actually apparent + # on the current page. + # + skippers: skipTo: "Skip to:" @@ -340,11 +507,18 @@ misc: left: "Sidebar (left)" right: "Sidebar (right)" + # Displayed on artist info page. + tracks: "Tracks" - art: "Artworks" + artworks: "Artworks" flashes: "Flashes & Games" + + # Displayed on track and flash info pages. + contributors: "Contributors" + # Displayed on track info page. + references: "References..." referencedBy: "Referenced by..." samples: "Samples..." @@ -353,23 +527,48 @@ misc: featuredIn: "Featured in..." lyrics: "Lyrics" + sheetMusicFiles: "Sheet music files" midiProjectFiles: "MIDI/project files" - additionalFiles: "Additional files" + + # Displayed on track and album info pages. + commentary: "Commentary" + artistCommentary: "Commentary" + additionalFiles: "Additional files" + + # socialEmbed: + # Social embeds describe how the page should be represented on + # social platforms, chat messaging apps, and so on. socialEmbed: heading: "{WIKI_NAME} | {HEADING}" + # jumpTo: + # Generic action displayed at the top of some longer pages, for + # quickly scrolling down to a particular section. + jumpTo: _: "Jump to:" withLinks: "Jump to: {LINKS}." + # contentWarnings: + # Displayed for some artworks, informing of possibly sensitive + # content and giving the viewer a chance to consider before + # clicking through. + contentWarnings: _: "cw: {WARNINGS}" reveal: "click to show" + # albumGrid: + # Generic strings for various sorts of gallery grids, displayed + # on the homepage, album galleries, artist artwork galleries, and + # so on. These get the name of the thing being represented and, + # often, a bit of text providing pertinent extra details about + # that thing. + albumGrid: noCoverArt: "{ALBUM}" @@ -381,31 +580,83 @@ misc: albumGalleryGrid: noCoverArt: "{NAME}" - uiLanguage: "UI Language: {LANGUAGES" + # uiLanguage: + # Displayed in the footer, for switching between languages. + + uiLanguage: "UI Language: {LANGUAGES}" +# +# homepage: +# This is the main index and home for the whole wiki! There isn't +# much for strings here as the layout is very customizable and +# includes mostly wiki-provided content. +# homepage: title: "{TITLE}" + # news: + # If the wiki has news entries enabled, then there's a box in the + # homepage's sidebar (beneath custom sidebar content, if any) + # which displays the bodies the latest few entries up to a split. + news: title: "News" entry: viewRest: "(View rest of entry!)" +# +# albumSidebar: +# This sidebar is displayed on both the album and track info pages! +# It displays the groups that the album is from (each getting its +# own box on the album page, all conjoined in one box on the track +# page) and the list of tracks in the album, which can be sectioned +# similarly to normal track lists, but displays the range of tracks +# in each section rather than the section's duration. +# albumSidebar: trackList: - fallbackSectionName: "Track list" item: "{TRACK}" + # fallbackSectionName: + # If an album's track list isn't sectioned, the track list here + # will still have all the tracks grouped under a list that can + # be toggled open and closed. This controls how that list gets + # titled. + + fallbackSectionName: "Track list" + + # group: + # "Group" is a misnomer - these are track sections. Some albums + # don't use track numbers at all, and for these, the default + # string will be used instead of group.withRange. + group: _: "{GROUP}" withRange: "{GROUP} ({RANGE})" + # groupBox: + # This is the box for groups. Apart from the next and previous + # links, it also gets "visit on" and the group's descripton + # (up to a split). + groupBox: title: "{GROUP}" next: "Next: {ALBUM}" previous: "Previous: {ALBUM}" +# +# albumPage: +# +# Albums group together tracks and provide quick access to each of +# their pages, have release data (and sometimes credits) that are +# generally inherited by the album's tracks plus commentary and +# other goodies of their own, and are generally the main object on +# the wiki! +# +# Most of the strings on the album info page are tracked under +# releaseInfo, so there isn't a lot here. +# albumPage: title: "{ALBUM}" @@ -419,6 +670,10 @@ albumPage: heading: "{GROUP}" title: "{ALBUM}" + # body: + # These permutations are a bit awkward. "Tracks" is a counted + # string, ex. "63 tracks". + body: withDuration: "{DURATION}." withTracks: "{TRACKS}." @@ -428,21 +683,52 @@ albumPage: withTracks.withReleaseDate: "{TRACKS}. Released {DATE}." withDuration.withTracks.withReleaseDate: "{DURATION}, {TRACKS}. Released {DATE}." +# +# albumGalleryPage: +# Album galleries provide an alternative way to navigate the album, +# and put all its artwork - including for each track - into the +# spotlight. Apart from the main gallery grid (which usually lists +# each artwork's illustrators), this page also has a quick stats +# line about the album, and may display a message about all of the +# artworks if one applies. +# albumGalleryPage: title: "{ALBUM} - Gallery" + # statsLine: + # Most albums have release dates, but not all. These strings + # react accordingly. + statsLine: >- {TRACKS} totaling {DURATION}. statsLine.withDate: >- {TRACKS} totaling {DURATION}. Released {DATE}. + # coverArtistsLine: + # This is displayed if every track (which has artwork at all) + # has the same illustration credits. + coverArtistsLine: >- All track artwork by {ARTISTS}. + # noTrackArtworksLine: + # This is displayed if none of the tracks on the album have any + # artwork at all. Generally, this means the album gallery won't + # be linked from the album's other pages, but it is possible to + # end up on "stub galleries" using nav links on another gallery. + noTrackArtworksLine: >- This album doesn't have any track artwork. +# +# albumCommentaryPage: +# The album commentary page is a more minimal layout that brings +# the commentary for the album, and each of its tracks, to the +# front. It's basically inspired by reading in a library, or by +# following along with an album's booklet or liner notes while +# playing it back on a treasured dinky CD player late at night. +# albumCommentaryPage: title: "{ALBUM} - Commentary" @@ -457,6 +743,14 @@ albumCommentaryPage: albumCommentary: "Album commentary" trackCommentary: "{TRACK}" +# +# artistInfoPage: +# The artist info page is an artist's main home on the wiki, and +# automatically includes a full list of all the things they've +# contributed to and been credited on. It's split into a section +# for each of the kinds of things the artist is credited for, +# including tracks, artworks, flashes/games, and commentary. +# artistPage: title: "{ARTIST}" @@ -464,27 +758,73 @@ artistPage: artist: "Artist: {ARTIST}" creditList: + + # album: + # Tracks are chunked by albums, as long as the tracks are all + # of the same date (if applicable). + album: _: "{ALBUM}" withDate: "{ALBUM} ({DATE})" withDuration: "{ALBUM} ({DURATION})" withDate.withDuration: "{ALBUM} ({DATE}; {DURATION})" + # flashAct: + # Flashes are chunked by flash act, though a single flash act + # might be split into multiple chunks if it spans a long range + # and the artist contributed to a flash from some other act + # between. A date range will be shown if an act has at least + # two differently dated flashes. + flashAct: _: "{ACT}" withDate: "{ACT} ({DATE})" withDateRange: "{ACT} ({DATE_RANGE})" + # entry: + # This section covers strings for all kinds of individual + # things which an artist has contributed to, and refers to the + # items in each of the chunks described above. + entry: - rerelease: "{ENTRY} (re-release)" + + # withContribution: + # The specific contribution that an artist made to a given + # thing may be described with a word or two, and that's shown + # in the list. + withContribution: "{ENTRY} ({CONTRIBUTION})" + + # withArtists: + # This lists co-artists or co-contributors, depending on how + # the artist themselves was credited. + withArtists: "{ENTRY} (with {ARTISTS})" + withArtists.withContribution: "{ENTRY} ({CONTRIBUTION}; with {ARTISTS})" + # rerelease: + # Tracks which aren't the original release don't display co- + # artists or contributors, and get dimmed a little compared + # to original release track entries. + + rerelease: "{ENTRY} (re-release)" + + # track: + # The string without duration is used in both the artist's + # track credits list as well as their commentary list. + track: _: "{TRACK}" withDuration: "({DURATION}) {TRACK}" + # album: + # The artist info page doesn't display if the artist is + # musically credited outright for the album as a whole, + # opting to show each of the tracks from that album instead. + # But other parts belonging specifically to the album have + # credits too, and those entreis get the strings below. + album: coverArt: "(cover art)" wallpaperArt: "(wallpaper art)" @@ -494,19 +834,18 @@ artistPage: flash: _: "{FLASH}" + # contributedDurationLine: + # This is shown at the top of the artist's track list, provided + # any of their tracks have durations at all. + contributedDurationLine: >- {ARTIST} has contributed {DURATION} of music shared on this wiki. - musicGroupsLine: >- - Contributed music to groups: {GROUPS} - - artGroupsLine: >- - Contributed art to groups: {GROUPS} - - groupsLine: - item: - withCount: "{GROUP} ({COUNT})" - withDuration: "{GROUP} ({DURATION})" + # groupContributions: + # This is a special "chunk" shown at the top of an artist's + # track and artwork lists. It lists which groups an artist has + # contributed the most (and least) to, and is interactive - + # it can be sorted by count or, for tracks, by duration. groupContributions: title: @@ -536,17 +875,35 @@ artistPage: commentaryList: title: "Commentary" + # viewArtGallery: + # This is shown twice on the page - once at almost the very top + # of the page, just beneath visiting links, and once above the + # list of credited artworks, where it gets the longer + # orBrowseList form. + viewArtGallery: _: "View {LINK}!" orBrowseList: "View {LINK}! Or browse the list:" link: "art gallery" +# +# artistGalleryPage: +# The artist gallery page shows a neat grid of all of the album and +# track artworks an artist has contributed to! Co-illustrators are +# also displayed when applicable. +# artistGalleryPage: title: "{ARTIST} - Gallery" infoLine: >- Contributed to {COVER_ARTS}. +# +# commentaryIndex: +# The commentary index page shows a summary of all the commentary +# across the entire wiki, with a list linking to each album's +# dedicated commentary page. +# commentaryIndex: title: "Commentary" @@ -557,20 +914,50 @@ commentaryIndex: title: "Choose an album:" item: "{ALBUM} ({WORDS} across {ENTRIES})" +# +# flashIndex: +# The flash index page shows a very long grid including every flash +# on the wiki, sectioned with big headings for each act. It's also +# got jump links at the top to skip to a specific overarching +# section ("side") of flash acts. +# flashIndex: title: "Flashes & Games" +# +# flashSidebar: +# The flash sidebar is used on both the flash info and flash act +# gallery pages, and has two boxes - one showing all the flashes in +# the current flash act, and one showing all the flash acts on the +# wiki, sectioned by "side". +# +flashSidebar: + flashList: + + # These two strings are the default ones used when a flash act + # doesn't specify a custom phrasing. + flashesInThisAct: "Flashes in this act" + entriesInThisSection: "Entries in this section" + +# +# flashPage: +# The flash info page shows release information, links to check the +# flash out, and lists of contributors and featured tracks. Most of +# those strings are under releaseInfo, so there aren't a lot of +# strings here. +# flashPage: title: "{FLASH}" nav: flash: "{FLASH}" -flashSidebar: - flashList: - flashesInThisAct: "Flashes in this act" - entriesInThisSection: "Entries in this section" - +# +# groupSidebar: +# The group sidebar is used on both the group info and group +# gallery pages, and is formed of just one box, showing all the +# groups on the wiki, sectioned by "category". +# groupSidebar: title: "Groups" @@ -578,10 +965,19 @@ groupSidebar: category: "{CATEGORY}" item: "{GROUP}" +# +# groupPage: +# This section represents strings common to multiple group pages. +# groupPage: nav: group: "Group: {GROUP}" +# +# groupInfoPage: +# The group info page shows visiting links, the group's full +# description, and a list of albums from the group. +# groupInfoPage: title: "{GROUP}" @@ -589,6 +985,11 @@ groupInfoPage: _: "View {LINK}! Or browse the list:" link: "album gallery" + # albumList: + # Many albums are present under multiple groups, and these get an + # accent indicating what other group is highest on the album's + # list of groups. + albumList: title: "Albums" @@ -598,12 +999,24 @@ groupInfoPage: withAccent: "{ITEM} {ACCENT}" otherGroupAccent: "(from {GROUP})" +# +# groupGalleryPage: +# The group gallery page shows a grid of all the albums from that +# group, each including the number of tracks and duration, as well +# as a stats line for the group as a whole, and a neat carousel, if +# pre-configured! +# groupGalleryPage: title: "{GROUP} - Gallery" infoLine: >- {TRACKS} across {ALBUMS}, totaling {TIME}. +# +# listingIndex: +# The listing index page shows all available listings on the wiki, +# and a very exciting stats line for the wiki as a whole. +# listingIndex: title: "Listings" @@ -613,7 +1026,26 @@ listingIndex: exploreList: >- Feel free to explore any of the listings linked below and in the sidebar! +# +# listingPage: +# +# There are a lot of listings! Each is automatically generated and +# sorts or organizes the data on the wiki in some way that provides +# useful or interesting information. Most listings work primarily +# with one kind of data and are sectioned accordingly, for example +# "listAlbums.byDuration" or "listTracks.byDate". +# +# There are also some miscellaneous strings here, most of which are +# common to a variety of listings, and are often navigational in +# nature. +# listingPage: + + # target: + # Just the names for each of the sections - each chunk on the + # listing index (and in the sidebar) gets is titled with one of + # these. + target: album: "Albums" artist: "Artists" @@ -622,30 +1054,77 @@ listingPage: tag: "Tags" other: "Other" + # misc: + # Common, generic terminology across multiple listings. + + misc: + trackContributors: "Track Contributors" + artContributors: "Art Contributors" + flashContributors: "Flash & Game Contributors" + artAndFlashContributors: "Art & Flash Contributors" + + # listingFor: + # Displays quick links to navigate to other listings for the + # current target. + listingsFor: "Listings for {TARGET}: {LISTINGS}" + + # seeAlso: + # Displays directly related listings, which might be from other + # targets besides the current one. + seeAlso: "Also check out: {LISTINGS}" listAlbums: + + # listAlbums.byName: + # Lists albums alphabetically without sorting or chunking by + # any other criteria. Also displays the number of tracks for + # each album. + byName: title: "Albums - by Name" title.short: "...by Name" item: "{ALBUM} ({TRACKS})" + # listAlbums.byTracks: + # Lists albums by number of tracks, most to least, or by name + # alphabetically, if two albums have the same track count. + # Albums without any tracks are totally excluded. + byTracks: title: "Albums - by Tracks" title.short: "...by Tracks" item: "{ALBUM} ({TRACKS})" + # listAlbums.byDuration: + # Lists albums by total duration of all tracks, longest to + # shortest, falling back to an alphabetical sort if two albums + # are the same duration. Albums with zero duration are totally + # excluded. + byDuration: title: "Albums - by Duration" title.short: "...by Duration" item: "{ALBUM} ({DURATION})" + # listAlbums.byDate: + # Lists albums by release date, oldest to newest, falling back + # to an alphabetical sort if two albums were released on the + # same date. Dateless albums are totally excluded. + byDate: title: "Albums - by Date" title.short: "...by Date" item: "{ALBUM} ({DATE})" + # listAlbums.byDateAdded: + # Lists albums by the date they were added to the wiki, oldest + # to newest, and chunks these by date, since albums are usually + # added in bunches at a time. The albums in each chunk are + # sorted alphabetically, and albums which are missing the + # "Date Added" field are totally excluded. + byDateAdded: title: "Albums - by Date Added to Wiki" title.short: "...by Date Added to Wiki" @@ -654,26 +1133,66 @@ listingPage: item: "{ALBUM}" listArtists: + + # listArtists.byName: + # Lists artists alphabetically without sorting or chunking by + # any other criteria. Also displays the number of contributions + # from each artist. + byName: title: "Artists - by Name" title.short: "...by Name" item: "{ARTIST} ({CONTRIBUTIONS})" + # listArtists.byContribs: + # Lists artists by number of contributions, most to least, + # with separate lists for contributions to tracks, artworks, + # and flashes. Falls back alphabetically if two artists have + # the same number of contributions. Artists who aren't credited + # for any contributions to each of these categories are + # excluded from the respective list. + byContribs: title: "Artists - by Contributions" title.short: "...by Contributions" item: "{ARTIST} ({CONTRIBUTIONS})" + # listArtists.byCommentary: + # Lists artists by number of commentary entries, most to least, + # falling back to an alphabetical sort if two artists have the + # same count. Artists who don't have any commentary entries are + # totally excluded. + byCommentary: title: "Artists - by Commentary Entries" title.short: "...by Commentary Entries" item: "{ARTIST} ({ENTRIES})" + # listArtists.byDuration: + # Lists artists by total duration of the tracks which they're + # credited on (as either artist or contributor), longest sum to + # shortest, falling back alphabetically if two artists have + # the same duration. Artists who haven't contributed any music, + # or whose tracks all lack durations, are totally excluded. + byDuration: title: "Artists - by Duration" title.short: "...by Duration" item: "{ARTIST} ({DURATION})" + # listArtists.byLatest: + # Lists artists by the date of their latest musical, artwork, + # or flash contributions (with a separate section for each), + # latest to longest ago, and chunks artists together by the + # album/flash which their contribution was to. If two albums + # (or flashes) released on the same date, they're sorted by + # name, and artists within each album/flash are also sorted + # alphabetically. If an artist has contributions of a given + # kind, but those contributions aren't dated at all, they're + # listed at the bottom; artists who aren't credited for any + # contributions to each category are totally excluded from the + # respective lists. + byLatest: title: "Artists - by Latest Contribution" title.short: "...by Latest Contribution" @@ -688,12 +1207,24 @@ listingPage: item: "{ARTIST}" listGroups: + + # listGroups.byName: + # Lists groups alphabetically without sorting or chunking by + # any other criteria. Also displays a link to each group's + # gallery page. + byName: title: "Groups - by Name" title.short: "...by Name" item: "{GROUP} ({GALLERY})" item.gallery: "Gallery" + # listGroups.byCategory: + # Lists groups directly reflecting the way they're sorted in + # the wiki's groups.yaml data file, with no automatic sorting, + # chunked (as sectioned in groups.yaml) by category. Also shows + # a link to each group's gallery page. + byCategory: title: "Groups - by Category" title.short: "...by Category" @@ -703,32 +1234,70 @@ listingPage: item: "{GROUP} ({GALLERY})" item.gallery: "Gallery" + # listGroups.byAlbums: + # Lists groups by number of belonging albums, most to least, + # falling back alphabetically if two groups have the same + # number of albums. Groups without any albums are totally + # excluded. + byAlbums: title: "Groups - by Albums" title.short: "...by Albums" item: "{GROUP} ({ALBUMS})" + # listGroups.byTracks: + # Lists groups by number of tracks under each group's albums, + # most to least, falling back to an alphabetical sort if two + # groups have the same track counts. Groups without any tracks + # are totally excluded. + byTracks: title: "Groups - by Tracks" title.short: "...by Tracks" item: "{GROUP} ({TRACKS})" + # listGroups.byDuration: + # Lists groups by sum of durations of all the tracks under each + # of the group's albums, longest to shortest, falling back to + # an alphabetical sort if two groups have the same duration. + # Groups whose total duration is zero are totally excluded. + byDuration: title: "Groups - by Duration" title.short: "...by Duration" item: "{GROUP} ({DURATION})" + # listGroups.byLatest: + # List groups by release date of each group's most recent + # album, most recent to longest ago, falling back to sorting + # alphabetically if two groups' latest albums were released + # on the same date. Groups which don't have any albums, or + # whose albums are all dateless, are totally excluded. + byLatest: title: "Groups - by Latest Album" title.short: "...by Latest Album" item: "{GROUP} ({DATE})" listTracks: + + # listTracks.byName: + # List tracks alphabetically without sorting or chunking by + # any other criteria. + byName: title: "Tracks - by Name" title.short: "...by Name" item: "{TRACK}" + # listTracks.byAlbum: + # List tracks chunked by the album they're from, retaining the + # position each track occupies in its album, and sorting albums + # from oldest to newest (or alphabetically, if two albums were + # released on the same date). Dateless albums are included at + # the bottom of the list. Custom "Date First Released" fields + # on individual tracks are totally ignored. + byAlbum: title: "Tracks - by Album" title.short: "...by Album" @@ -737,6 +1306,15 @@ listingPage: title: "{ALBUM}" item: "{TRACK}" + # listTracks.byDate: + # List tracks according to their own release dates, which may + # differ from that of the album via the "Date First Released" + # field, oldest to newest, and chunked by album when multiple + # tracks from one album were released on the same date. Track + # order within a given album is preserved where possible. + # Dateless albums are excluded, except for contained tracks + # which have custom "Date First Released" fields. + byDate: title: "Tracks - by Date" title.short: "...by Date" @@ -746,11 +1324,22 @@ listingPage: item: "{TRACK}" item.rerelease: "{TRACK} (re-release)" + # listTracks.byDuration: + # List tracks by duration, longest to shortest, falling back to + # an alphabetical sort if two tracks have the same duration. + # Tracks which don't have any duration are totally excluded. + byDuration: title: "Tracks - by Duration" title.short: "...by Duration" item: "{TRACK} ({DURATION})" + # listTracks.byDurationInAlbum: + # List tracks chunked by the album they're from, then sorted + # by duration, longest to shortest; albums are sorted by date, + # oldest to newest, and both sorts fall back alphabetically. + # Dateless albums are included at the bottom of the list. + byDurationInAlbum: title: "Tracks - by Duration (in Album)" title.short: "...by Duration (in Album)" @@ -759,11 +1348,25 @@ listingPage: title: "{ALBUM}" item: "{TRACK} ({DURATION})" + # listTracks.byTimesReferenced: + # List tracks by how many other tracks' reference lists each + # appears in, most times referenced to fewest, falling back + # alphabetically if two tracks have been referenced the same + # number of times. Tracks that aren't referenced by any other + # tracks are totally excluded from the list. + byTimesReferenced: title: "Tracks - by Times Referenced" title.short: "...by Times Referenced" item: "{TRACK} ({TIMES_REFERENCED})" + # listTracks.inFlashes.byAlbum: + # List tracks, chunked by album (which are sorted by date, + # falling back alphabetically) and in their usual track order, + # and display the list of flashes that eack track is featured + # in. Tracks which aren't featured in any flashes are totally + # excluded from the list. + inFlashes.byAlbum: title: "Tracks - in Flashes & Games (by Album)" title.short: "...in Flashes & Games (by Album)" @@ -772,6 +1375,14 @@ listingPage: title: "{ALBUM}" item: "{TRACK} (in {FLASHES})" + # listTracks.inFlashes.byFlash: + # List tracks, chunked by flash (which are sorted by date, + # retaining their positions in a common act where applicable, + # or else by the two acts' names) and sorted according to the + # featured list of the flash, and display a link to the album + # each track is contained in. Tracks which aren't featured in + # any flashes are totally excluded from the list. + inFlashes.byFlash: title: "Tracks - in Flashes & Games (by Flash)" title.short: "...in Flashes & Games (by Flash)" @@ -780,6 +1391,13 @@ listingPage: title: "{FLASH}" item: "{TRACK} (from {ALBUM})" + # listTracks.withLyrics: + # List tracks, chunked by album (which are sorted by date, + # falling back alphabetically) and in their usual track order, + # displaying only tracks which have lyrics. The chunk titles + # also display the date each album was released, and tracks' + # own custom "Date First Released" fields are totally ignored. + withLyrics: title: "Tracks - with Lyrics" title.short: "...with Lyrics" @@ -789,6 +1407,14 @@ listingPage: title.withDate: "{ALBUM} ({DATE})" item: "{TRACK}" + # listTracks.withSheetMusicFiles: + # List tracks, chunked by album (which are sorted by date, + # falling back alphabetically) and in their usual track order, + # displaying only tracks which have sheet music files. The + # chunk titles also display the date each album was released, + # and tracks' own custom "Date First Released" fields are + # totally ignored. + withSheetMusicFiles: title: "Tracks - with Sheet Music Files" title.short: "...with Sheet Music Files" @@ -798,6 +1424,14 @@ listingPage: title.withDate: "{ALBUM} ({DATE})" item: "{TRACK}" + # listTracks.withMidiProjectFiles: + # List tracks, chunked by album (which are sorted by date, + # falling back alphabetically) and in their usual track order, + # displaying only tracks which have MIDI & project files. The + # chunk titles also display the date each album was released, + # and tracks' own custom "Date First Released" fields are + # totally ignored. + withMidiProjectFiles: title: "Tracks - with MIDI & Project Files" title.short: "...with MIDI & Project Files" @@ -808,17 +1442,38 @@ listingPage: item: "{TRACK}" listTags: + + # listTags.byName: + # List art tags alphabetically without sorting or chunking by + # any other criteria. Also displays the number of times each + # art tag has been featured. + byName: title: "Tags - by Name" title.short: "...by Name" item: "{TAG} ({TIMES_USED})" + # listTags.byUses: + # List art tags by number of times used, falling back to an + # alphabetical sort if two art tags have been featured the same + # number of times. Art tags which haven't haven't been featured + # at all yet are totally excluded from the list. + byUses: title: "Tags - by Uses" title.short: "...by Uses" item: "{TAG} ({TIMES_USED})" other: + + # other.allSheetMusic: + # List all sheet music files, sectioned by album (which are + # sorted by date, falling back alphabetically) and then by + # track (which retain album ordering). If one "file" entry + # contains multiple files, then it's displayed as an expandable + # list, collapsed by default, accented with the number of + # downloadable files. + allSheetMusic: title: "All Sheet Music" title.short: "All Sheet Music" @@ -828,6 +1483,9 @@ listingPage: _: "{TITLE}" withMultipleFiles: "{TITLE} ({FILES})" + # other.midiProjectFiles: + # Same as other.allSheetMusic, but for MIDI & project files. + allMidiProjectFiles: title: "All MIDI/Project Files" title.short: "All MIDI/Project Files" @@ -837,6 +1495,9 @@ listingPage: _: "{TITLE}" withMultipleFiles: "{TITLE} ({FILES})" + # other.additionalFiles: + # Same as other.allSheetMusic, but for additional files. + allAdditionalFiles: title: "All Additional Files" title.short: "All Additional Files" @@ -846,21 +1507,38 @@ listingPage: _: "{TITLE}" withMultipleFiles: "{TITLE} ({FILES})" + # other.randomPages: + # Special listing which shows a bunch of buttons that each + # link to a random page on the wiki under a particular scope. + randomPages: title: "Random Pages" title.short: "Random Pages" + # chooseLinkLine: + # Introductory line explaining the links on this listing. + chooseLinkLine: >- Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry. + # dataLoadingLine, dataLoadedLine: + # Since the links on this page depend on access to a fairly + # large data file that is downloaded separately and in the + # background, these messages indicate the status of that + # download and whether or not the links will work yet. + dataLoadingLine: >- (Data files are downloading in the background! Please wait for data to load.) dataLoadedLine: >- (Data files have finished being downloaded. The links should work!) + # misc: + # The first chunk in the list includes general links which + # bring you to some random page across the whole site! + misc: _: "Miscellaneous:" randomArtist: "Random Artist" @@ -868,35 +1546,65 @@ listingPage: randomAlbumWholeSite: "Random Album (whole site)" randomTrackWholeSite: "Random Track (whole site)" + # group: + # The remaining chunks are one for each of the main groups on + # the site, and each includes a list of all the albums from + # that group - clicking one brings to a random track from the + # album. + group: _: "From {GROUP}: ({RANDOM_ALBUM}, {RANDOM_TRACK})" randomAlbum: "Random Album" randomTrack: "Random Track" - album: "{ALBUM}" + # album: + # Album entries under each group. - misc: - trackContributors: "Track Contributors" - artContributors: "Art Contributors" - flashContributors: "Flash & Game Contributors" - artAndFlashContributors: "Art & Flash Contributors" + album: "{ALBUM}" +# +# newsIndex: +# The news index page shows a list of every news entry on the wiki! +# (If it's got news entries enabled.) Each entry gets a stylized +# heading with its name of and date, sorted newest to oldest, as +# well as its body (up to a split) and a link to view the rest of +# the entry on its dedicated news entry page. +# newsIndex: title: "News" entry: viewRest: "(View rest of entry!)" +# +# newsEntryPage: +# The news entry page displays all the content of a news entry, +# as well as its date published, in one big list, and has nav links +# to go to the previous and next news entry. +# newsEntryPage: title: "{ENTRY}" published: "(Published {DATE}.)" +# +# redirectPage: +# Static "placeholder" pages when redirecting a visitor from one +# page to another - this generally happens automatically, before +# you have a chance to read the page, so content is concise. +# redirectPage: title: "Moved to {TITLE}" infoLine: >- This page has been moved to {TARGET}. +# +# tagPage: +# The tag gallery page displays all the artworks that a tag has +# been featured in, in one neat grid, with each artwork displaying +# its illustrators, as well as a short info line that indicates +# how many artworks the tag's part of. +# tagPage: title: "{TAG}" @@ -906,6 +1614,20 @@ tagPage: infoLine: >- Appears in {COVER_ARTS}. +# +# trackPage: +# +# The track info page is pretty much the most discrete and common +# chunk of information across the whole site, displaying info about +# the track like its release date, artists, cover illustrators, +# commentary, and more, as well as relational info, like the tracks +# it references and tracks which reference it, and flashes which +# it's been featured in. Tracks can also have extra related files, +# like sheet music and MIDI/project files. +# +# Most of the details about tracks use strings that are defined +# under releaseInfo, so this section is a little sparse. +# trackPage: title: "{TRACK}" @@ -916,10 +1638,6 @@ trackPage: _: "{TRACK}" withNumber: "{NUMBER}. {TRACK}" - referenceList: - fandom: "Fandom:" - official: "Official:" - socialEmbed: heading: "{ALBUM}" title: "{TRACK}" -- cgit 1.3.0-6-gf8a5