diff --git a/app/common/util.mjs b/app/common/util.mjs index 98d501e..8afee7e 100644 --- a/app/common/util.mjs +++ b/app/common/util.mjs @@ -1,3 +1,5 @@ +import path from 'path'; + export function getTopOfCurrentHour(date = null) { date ??= new Date; @@ -15,3 +17,16 @@ export function getGearIcon(gear) { default: return null; } } + +export function deriveId(node) { + // Unfortunately, SplatNet doesn't return IDs for a lot of gear properties. + // Derive IDs from image URLs instead. + + let url = new URL(node.image.url); + let id =path.basename(url.pathname, '.png'); + + return { + '__splatoon3ink_id': id, + ...node, + }; +} diff --git a/app/data/LocalizationProcessor.mjs b/app/data/LocalizationProcessor.mjs index e69de29..ca274aa 100644 --- a/app/data/LocalizationProcessor.mjs +++ b/app/data/LocalizationProcessor.mjs @@ -0,0 +1,99 @@ +import fs from 'fs/promises'; +import path from 'path'; +import mkdirp from 'mkdirp'; +import jsonpath from 'jsonpath'; +import get from 'lodash/get.js'; +import set from 'lodash/set.js'; + +function makeArray(value) { + return Array.isArray(value) ? value : [value]; +} + +export class LocalizationProcessor { + outputDirectory = 'dist/data/locale'; + + constructor(locale, rulesets) { + this.locale = locale; + this.rulesets = rulesets; + } + + get filename() { + return `${this.outputDirectory}/${this.locale.code}.json`; + } + + *rulesetIterations() { + for (let ruleset of this.rulesets) { + for (let node of makeArray(ruleset.nodes)) { + for (let value of makeArray(ruleset.values)) { + yield { + key: ruleset.key, + node, + id: ruleset.id, + value, + }; + } + } + } + } + + *dataIterations(data) { + for (let ruleset of this.rulesetIterations()) { + for (let node of jsonpath.query(data, ruleset.node)) { + let id = get(node, ruleset.id); + + yield { + ruleset, + node, + id, + value: get(node, ruleset.value), + path: `${ruleset.key}.${id}.${ruleset.value}`, + }; + } + } + } + + async updateLocalizations(data) { + let localizations = await this.readData(); + + for (let { path, value } of this.dataIterations(data)) { + set(localizations, path, value); + } + + await this.writeData(localizations); + } + + async hasMissingLocalizations(data) { + let localizations = await this.readData(); + + for (let { path } of this.dataIterations(data)) { + if (get(localizations, path) === undefined) { + return true; + } + } + + return false; + } + + async writeData(data) { + // If we're running in debug mode, format the JSON output so it's easier to read + let debug = !!process.env.DEBUG; + let space = debug ? 2 : undefined; + + data = JSON.stringify(data, undefined, space); + + await mkdirp(path.dirname(this.filename)) + await fs.writeFile(this.filename, data); + } + + async readData() { + try { + let result = await fs.readFile(this.filename); + + return JSON.parse(result) || {}; + } catch (e) { + // + } + + return {}; + } +} diff --git a/app/data/updaters/CoopUpdater.mjs b/app/data/updaters/CoopUpdater.mjs index 53b5da6..5f27da7 100644 --- a/app/data/updaters/CoopUpdater.mjs +++ b/app/data/updaters/CoopUpdater.mjs @@ -1,3 +1,5 @@ +import jsonpath from 'jsonpath'; +import { deriveId } from "../../common/util.mjs"; import DataUpdater from "./DataUpdater.mjs"; export default class CoopUpdater extends DataUpdater @@ -9,7 +11,20 @@ export default class CoopUpdater extends DataUpdater '$..image.url', ]; - getData(locale) { - return this.splatnet(locale).getCoopHistoryData(); + localizations = [ + { + key: 'gear', + nodes: '$..monthlyGear', + id: '__splatoon3ink_id', + values: 'name', + }, + ]; + + async getData(locale) { + let data = await this.splatnet(locale).getCoopHistoryData(); + + jsonpath.apply(data, '$..monthlyGear', deriveId); + + return data; } } diff --git a/app/data/updaters/DataUpdater.mjs b/app/data/updaters/DataUpdater.mjs index d48cc8b..e3f9214 100644 --- a/app/data/updaters/DataUpdater.mjs +++ b/app/data/updaters/DataUpdater.mjs @@ -7,6 +7,7 @@ import SplatNet3Client from "../../splatnet/SplatNet3Client.mjs"; import ImageProcessor from '../ImageProcessor.mjs'; import NsoClient from '../../splatnet/NsoClient.mjs'; import { locales } from '../../../src/common/i18n.mjs'; +import { LocalizationProcessor } from '../LocalizationProcessor.mjs'; export default class DataUpdater { @@ -15,6 +16,7 @@ export default class DataUpdater outputDirectory = 'dist/data'; imagePaths = []; + localizations = []; constructor(region = null) { this.nsoClient = NsoClient.make(region); @@ -52,6 +54,9 @@ export default class DataUpdater // Retrieve the data let data = await this.tryRequest(this.getData(this.defaultLocale)); + // Update localizations + await this.updateLocalizations(this.defaultLocale, data); + // Download any new images await this.downloadImages(data); @@ -79,6 +84,24 @@ export default class DataUpdater // Processing + async updateLocalizations(initialLocale, data) { + // Save localizations for the initial locale + let processor = new LocalizationProcessor(initialLocale, this.localizations); + await processor.updateLocalizations(data); + + // Retrieve data for missing languages + for (let locale of this.locales.filter(l => l !== initialLocale)) { + processor = new LocalizationProcessor(locale, this.localizations); + + if (await processor.hasMissingLocalizations(data)) { + this.console.info(`Retrieving localized data for ${locale.code}`); + + let regionalData = await this.getData(locale); + await processor.updateLocalizations(regionalData); + } + } + } + async downloadImages(data) { for (let expression of this.imagePaths) { // This JSONPath library is completely synchronous, so we have to diff --git a/app/data/updaters/GearUpdater.mjs b/app/data/updaters/GearUpdater.mjs index 152ab23..afaf82d 100644 --- a/app/data/updaters/GearUpdater.mjs +++ b/app/data/updaters/GearUpdater.mjs @@ -1,4 +1,6 @@ import DataUpdater from "./DataUpdater.mjs"; +import jsonpath from 'jsonpath'; +import { deriveId } from "../../common/util.mjs"; export default class GearUpdater extends DataUpdater { @@ -9,7 +11,39 @@ export default class GearUpdater extends DataUpdater '$..image.url', ]; - getData(locale) { - return this.splatnet(locale).getGesotownData(); + localizations = [ + { + key: 'brands', + nodes: '$..brand', + id: 'id', + values: 'name', + }, + { + key: 'gear', + nodes: '$..gear', + id: '__splatoon3ink_id', + values: 'name', + }, + { + key: 'powers', + nodes: [ + '$..usualGearPower', + '$..primaryGearPower', + '$..additionalGearPowers.*', + ], + id: '__splatoon3ink_id', + values: 'name', + }, + ]; + + async getData(locale) { + let data = await this.splatnet(locale).getGesotownData(); + + jsonpath.apply(data, '$..gear', deriveId); + jsonpath.apply(data, '$..usualGearPower', deriveId); + jsonpath.apply(data, '$..primaryGearPower', deriveId); + jsonpath.apply(data, '$..additionalGearPowers.*', deriveId); + + return data; } } diff --git a/app/data/updaters/StageScheduleUpdater.mjs b/app/data/updaters/StageScheduleUpdater.mjs index 78d7813..bcbd48f 100644 --- a/app/data/updaters/StageScheduleUpdater.mjs +++ b/app/data/updaters/StageScheduleUpdater.mjs @@ -1,4 +1,6 @@ import DataUpdater from "./DataUpdater.mjs"; +import jsonpath from 'jsonpath'; +import { deriveId } from "../../common/util.mjs"; export default class StageScheduleUpdater extends DataUpdater { @@ -11,7 +13,32 @@ export default class StageScheduleUpdater extends DataUpdater '$..thumbnailImage.url', ]; - getData(locale) { - return this.splatnet(locale).getStageScheduleData(); + localizations = [ + { + key: 'stages', + nodes: '$..vsStages.nodes.*', + id: 'id', + values: 'name', + }, + { + key: 'rules', + nodes: '$..vsRule', + id: 'id', + values: 'name', + }, + { + key: 'weapons', + nodes: '$..weapons.*', + id: '__splatoon3ink_id', + values: 'name', + }, + ]; + + async getData(locale) { + let data = await this.splatnet(locale).getStageScheduleData(); + + jsonpath.apply(data, '$..weapons.*', deriveId); + + return data; } } diff --git a/package-lock.json b/package-lock.json index 33cdf57..b958282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dotenv": "^16.0.2", "ecstatic": "^4.1.4", "jsonpath": "^1.1.1", + "lodash": "^4.17.21", "mkdirp": "^1.0.4", "nxapi": "^1.4.0", "pinia": "^2.0.22", diff --git a/package.json b/package.json index a2529bc..7916639 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dotenv": "^16.0.2", "ecstatic": "^4.1.4", "jsonpath": "^1.1.1", + "lodash": "^4.17.21", "mkdirp": "^1.0.4", "nxapi": "^1.4.0", "pinia": "^2.0.22",