diff options
-rw-r--r-- | src/data/things.js | 6 | ||||
-rw-r--r-- | src/listing-spec.js | 4 | ||||
-rw-r--r-- | src/misc-templates.js | 16 | ||||
-rw-r--r-- | src/page/album.js | 2 | ||||
-rw-r--r-- | src/page/artist.js | 2 | ||||
-rwxr-xr-x | src/upd8.js | 247 |
6 files changed, 136 insertions, 141 deletions
diff --git a/src/data/things.js b/src/data/things.js index aa761356..97e70cde 100644 --- a/src/data/things.js +++ b/src/data/things.js @@ -1402,6 +1402,10 @@ Language.propertyDescriptors = { update: {validate: isLanguageCode} }, + // Human-readable name. This should be the language's own native name, not + // localized to any other language. + name: Thing.common.simpleString(), + // Language code specific to JavaScript's Internationalization (Intl) API. // Usually this will be the same as the language's general code, but it // may be overridden to provide Intl constructors an alternative value. @@ -1556,7 +1560,7 @@ Object.assign(Language.prototype, { return this.intl_date.formatRange(startDate, endDate); }, - formatDuration(secTotal, {approximate = false, unit = false}) { + formatDuration(secTotal, {approximate = false, unit = false} = {}) { if (secTotal === 0) { return this.formatString('count.duration.missing'); } diff --git a/src/listing-spec.js b/src/listing-spec.js index e1561d8e..bb0c0a5b 100644 --- a/src/listing-spec.js +++ b/src/listing-spec.js @@ -572,7 +572,7 @@ const listingSpec = [ data({wikiData}) { return wikiData.albumData.map(album => ({ album, - tracks: album.tracks.slice().sort((a, b) => b.duration - a.duration) + tracks: album.tracks.slice().sort((a, b) => (b.duration ?? 0) - (a.duration ?? 0)) })); }, @@ -587,7 +587,7 @@ const listingSpec = [ ${(tracks .map(track => language.$('listingPage.listTracks.byDurationInAlbum.track', { track: link.track(track), - duration: language.formatDuration(track.duration) + duration: language.formatDuration(track.duration ?? 0) })) .map(row => `<li>${row}</li>`) .join('\n'))} diff --git a/src/misc-templates.js b/src/misc-templates.js index 2e25a738..5aec92ee 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -381,6 +381,7 @@ export function generatePreviousNextLinks(current, { // Footer stuff export function getFooterLocalizationLinks(pathname, { + defaultLanguage, languages, paths, language, @@ -392,16 +393,13 @@ export function getFooterLocalizationLinks(pathname, { const links = Object.entries(languages) .filter(([ code ]) => code !== 'default') - .map(([ code, strings ]) => strings) - .sort(( - { json: { 'meta.languageName': a } }, - { json: { 'meta.languageName': b } } - ) => a < b ? -1 : a > b ? 1 : 0) - .map(strings => html.tag('span', html.tag('a', { - href: (strings.baseDirectory === languages.default.baseDirectory + .map(([ code, language ]) => language) + .sort(({ name: a }, { name: b }) => a < b ? -1 : a > b ? 1 : 0) + .map(language => html.tag('span', html.tag('a', { + href: (language === defaultLanguage ? to('localizedDefaultLanguage' + keySuffix, ...toArgs) - : to('localizedWithBaseDirectory' + keySuffix, strings.baseDirectory, ...toArgs)) - }, strings.json['meta.languageName']))); + : to('localizedWithBaseDirectory' + keySuffix, language.code, ...toArgs)) + }, language.name))); return html.tag('div', {class: 'footer-localization-links'}, diff --git a/src/page/album.js b/src/page/album.js index 45e5439a..5ea7c5a0 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -33,7 +33,7 @@ export function write(album, {wikiData}) { language }) => { const itemOpts = { - duration: language.formatDuration(track.duration), + duration: language.formatDuration(track.duration ?? 0), track: link.track(track) }; return `<li style="${getLinkThemeString(track.color)}">${ diff --git a/src/page/artist.js b/src/page/artist.js index 0bfcf1c4..c15e0342 100644 --- a/src/page/artist.js +++ b/src/page/artist.js @@ -187,7 +187,7 @@ export function write(artist, {wikiData}) { aka: track.aka, entry: language.$('artistPage.creditList.entry.track.withDuration', { track: link.track(track), - duration: language.formatDuration(track.duration) + duration: language.formatDuration(track.duration ?? 0) }), ...props })) diff --git a/src/upd8.js b/src/upd8.js index 01330355..6f04599a 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -70,6 +70,10 @@ import CacheableObject from './data/cacheable-object.js'; import { serializeThings } from './data/serialize.js'; import { + Language, +} from './data/things.js'; + +import { filterDuplicateDirectories, filterReferenceErrors, linkWikiDataArrays, @@ -114,12 +118,6 @@ import { } from './util/replacer.js'; import { - genStrings, - count, - list -} from './util/strings.js'; - -import { chunkByConditions, chunkByProperties, getAlbumCover, @@ -214,8 +212,6 @@ let wikiData = {}; let queueSize; -let languages; - const urls = generateURLs(urlSpec); function splitLines(text) { @@ -246,7 +242,7 @@ const replacerSpec = { 'date': { find: null, value: ref => new Date(ref), - html: (date, {strings}) => `<time datetime="${date.toString()}">${language.formatDate(date)}</time>` + html: (date, {language}) => `<time datetime="${date.toString()}">${language.formatDate(date)}</time>` }, 'flash': { find: 'flash', @@ -311,7 +307,7 @@ const replacerSpec = { 'string': { find: null, value: ref => ref, - html: (ref, {strings, args}) => language.$(ref, args) + html: (ref, {language, args}) => language.$(ref, args) }, 'tag': { find: 'artTag', @@ -823,9 +819,11 @@ writePage.to = ({ }; writePage.html = (pageFn, { + defaultLanguage, + language, + languages, localizedPaths, paths, - strings, to, transformMultiline, wikiData @@ -878,7 +876,7 @@ writePage.html = (pageFn, { footer.content ??= (wikiInfo.footerContent ? transformMultiline(wikiInfo.footerContent) : ''); footer.content += '\n' + getFooterLocalizationLinks(paths.pathname, { - languages, paths, strings, to + defaultLanguage, languages, paths, language, to }); const canonical = (wikiInfo.canonicalBase @@ -1045,7 +1043,7 @@ writePage.html = (pageFn, { src: '', link: true, square: true, - reveal: getRevealStringFromWarnings('<span class="info-card-art-warnings"></span>', {strings}) + reveal: getRevealStringFromWarnings('<span class="info-card-art-warnings"></span>', {language}) })} </div> <h1 class="info-card-name"><a></a></h1> @@ -1060,7 +1058,7 @@ writePage.html = (pageFn, { return filterEmptyLines(fixWS` <!DOCTYPE html> <html ${html.attributes({ - lang: strings.code, + lang: language.code, 'data-rebase-localized': to('localized.root'), 'data-rebase-shared': to('shared.root'), 'data-rebase-media': to('media.root'), @@ -1166,12 +1164,12 @@ function writeSymlinks() { } } -function writeSharedFilesAndPages({strings, wikiData}) { +function writeSharedFilesAndPages({language, wikiData}) { const { groupData, wikiInfo } = wikiData; const redirect = async (title, from, urlKey, directory) => { const target = path.relative(from, urls.from('shared.root').to(urlKey, directory)); - const content = generateRedirectPage(title, target, {strings}); + const content = generateRedirectPage(title, target, {language}); await mkdir(path.join(outputPath, from), {recursive: true}); await writeFile(path.join(outputPath, from, 'index.html'), content); }; @@ -1196,7 +1194,7 @@ function writeSharedFilesAndPages({strings, wikiData}) { ].filter(Boolean)); } -function generateRedirectPage(title, target, {strings}) { +function generateRedirectPage(title, target, {language}) { return fixWS` <!DOCTYPE html> <html> @@ -1230,33 +1228,37 @@ function linkAnythingMan(anythingMan, {link, wikiData, ...opts}) { ) } -async function processLanguageFile(file, defaultStrings = null) { - let contents; - try { - contents = await readFile(file, 'utf-8'); - } catch (error) { - return {error: `Could not read ${file} (${error.code}).`}; +async function processLanguageFile(file) { + const contents = await readFile(file, 'utf-8'); + const json = JSON.parse(contents); + + const code = json['meta.languageCode']; + if (!code) { + throw new Error(`Missing language code (file: ${file})`); } + delete json['meta.languageCode']; - let json; - try { - json = JSON.parse(contents); - } catch (error) { - return {error: `Could not parse JSON from ${file} (${error}).`}; + const name = json['meta.languageName']; + if (!name) { + throw new Error(`Missing language name (${code})`); } + delete json['meta.languageName']; - return genStrings(json, { - he, - defaultJSON: defaultStrings?.json, - bindUtilities: { - count, - list - } - }); + if (json['meta.baseDirectory']) { + logWarn`(${code}) Language JSON still has unused meta.baseDirectory`; + delete json['meta.baseDirectory']; + } + + const language = new Language(); + language.code = code; + language.name = name; + language.escapeHTML = string => he.encode(string, {useNamedReferences: true}); + language.strings = json; + return language; } // Wrapper function for running a function once for all languages. -async function wrapLanguages(fn, {writeOneLanguage = null}) { +async function wrapLanguages(fn, {languages, writeOneLanguage = null}) { const k = writeOneLanguage; const languagesToRun = (k ? {[k]: languages[k]} @@ -1266,9 +1268,9 @@ async function wrapLanguages(fn, {writeOneLanguage = null}) { .filter(([ key ]) => key !== 'default'); for (let i = 0; i < entries.length; i++) { - const [ key, strings ] = entries[i]; + const [ key, language ] = entries[i]; - await fn(strings, i, entries); + await fn(language, i, entries); } } @@ -1444,50 +1446,6 @@ async function main() { CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; } - const defaultStrings = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); - if (defaultStrings.error) { - logError`Error loading default strings: ${defaultStrings.error}`; - return; - } - - if (langPath) { - const languageDataFiles = await findFiles(langPath, { - filter: f => path.extname(f) === '.json' - }); - const results = await progressPromiseAll(`Reading & processing language files.`, languageDataFiles - .map(file => processLanguageFile(file, defaultStrings))); - - let error = false; - for (const strings of results) { - if (strings.error) { - logError`Error loading provided strings: ${strings.error}`; - error = true; - } - } - if (error) return; - - languages = Object.fromEntries(results.map(strings => [strings.baseDirectory, strings])); - } else { - languages = {}; - } - - if (!languages[defaultStrings.code]) { - languages[defaultStrings.code] = defaultStrings; - } - - logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`; - - if (noBuild) { - logInfo`Not generating any site or page files this run (--no-build passed).`; - } else if (writeOneLanguage && !(writeOneLanguage in languages)) { - logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`; - return; - } else if (writeOneLanguage) { - logInfo`Writing only language ${writeOneLanguage} this run.`; - } else { - logInfo`Writing all languages.`; - } - const { aggregate: processDataAggregate, result: wikiDataResult @@ -1610,23 +1568,61 @@ async function main() { // which are only available after the initial linking. sortWikiDataArrays(wikiData); - // Update languages o8ject with the wiki-specified default language! - // This will make page files for that language 8e gener8ted at the root - // directory, instead of the language-specific su8directory. - if (WD.wikiInfo.defaultLanguage) { - if (Object.keys(languages).includes(WD.wikiInfo.defaultLanguage)) { - languages.default = languages[WD.wikiInfo.defaultLanguage]; + const internalDefaultLanguage = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); + + let languages; + if (langPath) { + const languageDataFiles = await findFiles(langPath, { + filter: f => path.extname(f) === '.json' + }); + + const results = await progressPromiseAll(`Reading & processing language files.`, languageDataFiles + .map(file => processLanguageFile(file))); + + languages = Object.fromEntries(results.map(language => [language.code, language])); + } else { + languages = {}; + } + + const customDefaultLanguage = languages[WD.wikiInfo.defaultLanguage ?? internalDefaultLanguage.code]; + let finalDefaultLanguage; + + if (customDefaultLanguage) { + logInfo`Applying new default strings from custom ${customDefaultLanguage.code} language file.`; + customDefaultLanguage.inheritedStrings = internalDefaultLanguage.strings; + finalDefaultLanguage = customDefaultLanguage; + } else if (WD.wikiInfo.defaultLanguage) { + logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`; + if (langPath) { + logError`Check if an appropriate file exists in ${langPath}?`; } else { - logError`Wiki info file specified default language is ${WD.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'} or ${'HSMUSIC_LANG'} with the path to language files.`; - } - return; + logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`; } + return; } else { - languages.default = defaultStrings; + languages[defaultLanguage.code] = internalDefaultLanguage; + finalDefaultLanguage = internalDefaultLanguage; + } + + for (const language of Object.values(languages)) { + if (language === finalDefaultLanguage) { + continue; + } + + language.inheritedStrings = finalDefaultLanguage.strings; + } + + logInfo`Loaded language strings: ${Object.keys(languages).join(', ')}`; + + if (noBuild) { + logInfo`Not generating any site or page files this run (--no-build passed).`; + } else if (writeOneLanguage && !(writeOneLanguage in languages)) { + logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`; + return; + } else if (writeOneLanguage) { + logInfo`Writing only language ${writeOneLanguage} this run.`; + } else { + logInfo`Writing all languages.`; } { @@ -1683,7 +1679,7 @@ async function main() { logInfo`Writing site pages: ${writeAll ? 'all' : Object.keys(writeFlags).join(', ')}`; await writeSymlinks(); - await writeSharedFilesAndPages({strings: defaultStrings, wikiData}); + await writeSharedFilesAndPages({language: finalDefaultLanguage, wikiData}); const buildSteps = (writeAll ? Object.entries(buildDictionary) @@ -1830,20 +1826,15 @@ async function main() { )); */ - const getBaseDirectory = strings => - (strings === languages.default - ? '' - : strings.baseDirectory); - - const perLanguageFn = async (strings, i, entries) => { - const baseDirectory = getBaseDirectory(strings); + const perLanguageFn = async (language, i, entries) => { + const baseDirectory = (language === finalDefaultLanguage ? '' : language.code); console.log(`\x1b[34;1m${ - (`[${i + 1}/${entries.length}] ${strings.code} (-> /${baseDirectory}) ` + (`[${i + 1}/${entries.length}] ${language.code} (-> /${baseDirectory}) ` .padEnd(60, '-')) }\x1b[0m`); - await progressPromiseAll(`Writing ${strings.code}`, queue([ + await progressPromiseAll(`Writing ${language.code}`, queue([ ...pageWrites.map(({type, ...props}) => () => { const { path, page } = props; @@ -1853,8 +1844,8 @@ async function main() { const localizedPaths = Object.fromEntries(Object.entries(languages) .filter(([ key ]) => key !== 'default') - .map(([ key, strings ]) => [strings.code, writePage.paths( - getBaseDirectory(strings), + .map(([ key, language ]) => [language.code, writePage.paths( + (language === finalDefaultLanguage ? '' : language.code), 'localized.' + pageSubKey, directory )])); @@ -1894,7 +1885,7 @@ async function main() { find: bound.find, link: bound.link, replacerSpec, - strings, + language, to, wikiData }); @@ -1910,17 +1901,17 @@ async function main() { }); bound.iconifyURL = bindOpts(iconifyURL, { - strings, + language, to }); bound.fancifyURL = bindOpts(fancifyURL, { - strings + language }); bound.fancifyFlashURL = bindOpts(fancifyFlashURL, { [bindOpts.bindIndex]: 2, - strings + language }); bound.getLinkThemeString = getLinkThemeString; @@ -1930,7 +1921,7 @@ async function main() { bound.getArtistString = bindOpts(getArtistString, { iconifyURL: bound.iconifyURL, link: bound.link, - strings + language }); bound.getAlbumCover = bindOpts(getAlbumCover, { @@ -1952,7 +1943,7 @@ async function main() { bound.generateChronologyLinks = bindOpts(generateChronologyLinks, { link: bound.link, linkAnythingMan: bound.linkAnythingMan, - strings, + language, wikiData }); @@ -1960,7 +1951,7 @@ async function main() { [bindOpts.bindIndex]: 0, img, link: bound.link, - strings, + language, to, wikiData }); @@ -1968,18 +1959,18 @@ async function main() { bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, { [bindOpts.bindIndex]: 2, link: bound.link, - strings + language }); bound.generatePreviousNextLinks = bindOpts(generatePreviousNextLinks, { link: bound.link, - strings + language }); bound.getGridHTML = bindOpts(getGridHTML, { [bindOpts.bindIndex]: 0, img, - strings + language }); bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, { @@ -1987,7 +1978,7 @@ async function main() { getAlbumCover: bound.getAlbumCover, getGridHTML: bound.getGridHTML, link: bound.link, - strings + language }); bound.getFlashGridHTML = bindOpts(getFlashGridHTML, { @@ -1998,11 +1989,11 @@ async function main() { }); bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, { - strings + language }); bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, { - strings + language }); bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, { @@ -2011,14 +2002,16 @@ async function main() { const pageFn = () => page({ ...bound, - strings, + language, to }); const content = writePage.html(pageFn, { + defaultLanguage: finalDefaultLanguage, + language, + languages, localizedPaths, paths, - strings, to, transformMultiline: bound.transformMultiline, wikiData @@ -2028,7 +2021,7 @@ async function main() { }), ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => { const title = titleFn({ - strings + language }); // TODO: This only supports one <>-style argument. @@ -2036,15 +2029,15 @@ async function main() { const to = writePage.to({baseDirectory, pageSubKey: fromPath[0], paths: fromPaths}); const target = to('localized.' + toPath[0], ...toPath.slice(1)); - const content = generateRedirectPage(title, target, {strings}); + const content = generateRedirectPage(title, target, {language}); return writePage.write(content, {paths: fromPaths}); }) ], queueSize)); }; await wrapLanguages(perLanguageFn, { + languages, writeOneLanguage, - wikiData }); // The single most important step. |