Retrieve localized string data from SplatNet

This commit is contained in:
Matt Isenhower 2017-12-03 10:46:47 -08:00
parent 2a5f2ed684
commit db6d018ef5
10 changed files with 361 additions and 47 deletions

View File

@ -33,6 +33,7 @@
"html-webpack-plugin": "^2.30.1",
"json-stable-stringify": "^1.0.1",
"jsonpath": "^1.0.0",
"lodash": "^4.17.4",
"make-runnable": "^1.3.6",
"mkdirp": "^0.5.1",
"moment-timezone": "^0.5.13",

View File

@ -5,6 +5,20 @@ const splatoonRegions = [
{ key: 'jp', name: 'Japan', demonym: 'Japanese' },
];
const languages = [
{ region: 'NA', language: 'en' },
{ region: 'NA', language: 'es-MX' },
{ region: 'NA', language: 'fr-CA' },
{ region: 'EU', language: 'en' },
{ region: 'EU', language: 'de' },
{ region: 'EU', language: 'nl' },
{ region: 'EU', language: 'fr' },
{ region: 'EU', language: 'it' },
{ region: 'EU', language: 'ru' },
{ region: 'EU', language: 'es' },
{ region: 'JP', language: 'ja' },
];
function getRegionByKey(key) {
return splatoonRegions.find(r => r.key == key);
}
@ -64,6 +78,7 @@ function detectSplatoonRegionFromLanguage(language) {
module.exports = {
splatoonRegions,
languages,
getRegionByKey,
detectSplatoonRegion,
}

View File

@ -0,0 +1,96 @@
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const jsonpath = require('jsonpath');
const _ = require('lodash');
const { readJson, writeJson } = require('./utilities');
const localizationsPath = path.resolve('public/data/lang');
class LocalizationProcessor {
constructor(ruleset, languageInfo) {
this.ruleset = ruleset;
this.languageInfo = languageInfo;
let entities = this.ruleset.entities;
this.entityExpressions = (Array.isArray(entities)) ? entities : [entities];
let values = this.ruleset.values;
this.valueExpressions = (Array.isArray(values)) ? values : [values];
this.readData();
}
getFilename() {
return `${localizationsPath}/${this.languageInfo.language}.json`;
}
readData() {
if (fs.existsSync(this.getFilename()))
this.data = readJson(this.getFilename());
else
this.data = {};
}
writeData() {
mkdirp(path.dirname(this.getFilename()));
writeJson(this.getFilename(), this.data);
}
getExpression(id, valueKey) {
return [this.ruleset.name, id, valueKey];
}
readValue(id, valueKey) {
return _.get(this.data, this.getExpression(id, valueKey));
}
writeValue(id, valueKey, newValue) {
return _.setWith(this.data, this.getExpression(id, valueKey), newValue, Object);
}
eachEntity(data, callback) {
for (let expression of this.entityExpressions) {
let entities = jsonpath.query(data, expression);
for (let entity of entities) {
let result = callback(entity);
if (result === false)
return false;
}
}
}
updateLocalizations(data) {
this.eachEntity(data, entity => this.updateLocalizationsForEntity(entity));
}
updateLocalizationsForEntity(entity) {
for (let valueKey of this.valueExpressions) {
let id = _.get(entity, this.ruleset.id);
let value = _.get(entity, valueKey);
this.writeValue(id, valueKey, value);
}
this.writeData();
}
hasLocalizations(data) {
let result = this.eachEntity(data, entity => this.hasLocalizationsForEntity(entity));
return (result !== false);
}
hasLocalizationsForEntity(entity) {
for (let valueKey of this.valueExpressions) {
let id = _.get(entity, this.ruleset.id);
if (this.readValue(id, valueKey) === undefined)
return false;
}
return true;
}
}
module.exports = LocalizationProcessor;

View File

@ -2,47 +2,20 @@ require('./bootstrap');
const Updater = require('./updaters/Updater');
const SchedulesUpdater = require('./updaters/SchedulesUpdater');
const CoopSchedulesUpdater = require('./updaters/CoopSchedulesUpdater');
const TimelineUpdater = require('./updaters/TimelineUpdater');
const OriginalGearImageUpdater = require('./updaters/OriginalGearImageUpdater');
const FestivalsUpdater = require('./updaters/FestivalsUpdater');
const MerchandisesUpdater = require('./updaters/MerchandisesUpdater');
const updaters = [
// Original gear images
new OriginalGearImageUpdater(),
// Schedules
new OriginalGearImageUpdater,
new SchedulesUpdater,
// Co-op Schedules
new Updater({
name: 'Co-op Schedules',
filename: 'coop-schedules.json',
request: (splatnet) => splatnet.getCoopSchedules(),
imagePaths: [
'$..stage.image',
'$..weapons[*].image',
],
}),
// Timeline
new Updater({
name: 'Timeline',
filename: 'timeline.json',
request: (splatnet) => splatnet.getTimeline(),
rootKeys: ['coop', 'weapon_availability'],
imagePaths: [
'$.coop..gear.image',
'$.coop..gear.brand.image',
'$.weapon_availability..weapon.image',
'$.weapon_availability..weapon.special.image_a',
'$.weapon_availability..weapon.sub.image_a',
],
}),
// Festivals
new FestivalsUpdater,
// Merchandises
new CoopSchedulesUpdater,
new TimelineUpdater,
new FestivalsUpdater('NA'),
new FestivalsUpdater('EU'),
new FestivalsUpdater('JP'),
new MerchandisesUpdater,
];

View File

@ -0,0 +1,43 @@
const Updater = require('./Updater');
class CoopSchedulesUpdater extends Updater {
constructor() {
super({
name: 'Co-op Schedules',
filename: 'coop-schedules.json',
request: (splatnet) => splatnet.getCoopSchedules(),
imagePaths: [
'$..stage.image',
'$..weapons[*].image',
],
localization: [
{
name: 'coop_stages',
entities: '$..stage',
id: 'image', // Unfortunately these don't have an ID, so we'll just match them by image URL
values: 'name',
},
{
name: 'weapons',
entities: '$..weapons[?(@.id)]',
id: 'id',
values: 'name',
},
{
name: 'weapon_subs',
entities: '$..weapons[?(@.id)].sub',
id: 'id',
values: 'name',
},
{
name: 'weapon_specials',
entities: '$..weapons[?(@.id)].special',
id: 'id',
values: 'name',
},
],
});
}
}
module.exports = CoopSchedulesUpdater;

View File

@ -1,10 +1,15 @@
const Updater = require('./Updater');
const SplatNet = require('../splatnet');
const mkdirp = require('mkdirp');
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const { readJson, writeJson } = require('../utilities');
const { languages } = require('../../js/regions');
class FestivalsUpdater extends Updater {
constructor() {
constructor(region) {
super({
name: 'Festivals',
name: `Festivals ${region}`,
filename: 'festivals.json',
request: (splatnet) => splatnet.getCombinedFestivals(),
imagePaths: [
@ -13,15 +18,41 @@ class FestivalsUpdater extends Updater {
'$..images.panel',
'$..special_stage.image',
],
localization: [
{
name: 'festivals',
entities: '$.festivals[*]',
id: 'festival_id',
values: 'names',
},
{
name: 'stages',
entities: '$..special_stage',
id: 'id',
values: 'name',
},
],
});
this.region = region;
}
async getData() {
return {
na: await this.options.request(new SplatNet('NA')),
eu: await this.options.request(new SplatNet('EU')),
jp: await this.options.request(new SplatNet('JP')),
}
writeFile(filename, regionData) {
mkdirp(path.dirname(filename));
// Load existing data since we only need to modify this region's data
let data = {};
if (fs.existsSync(filename))
data = readJson(filename);
let region = this.region.toLowerCase();
data[region] = JSON.parse(regionData);
writeJson(filename, data);
}
getLanguages() {
// Return all languages for this region
return _.filter(languages, { region: this.region });
}
}

View File

@ -15,6 +15,32 @@ class MerchandisesUpdater extends Updater {
'$..gear.brand.frequent_skill.image',
'$..skill.image',
],
localization: [
{
name: 'gear',
entities: '$..gear',
id: 'id',
values: 'name',
},
{
name: 'brands',
entities: '$..gear.brand',
id: 'id',
values: 'name',
},
{
name: 'skills',
entities: '$..gear.brand.frequent_skill',
id: 'id',
values: 'name',
},
{
name: 'skills',
entities: '$..skill',
id: 'id',
values: 'name',
},
],
});
}

View File

@ -16,6 +16,29 @@ class SchedulesUpdater extends Updater {
'$..stage_a.image',
'$..stage_b.image',
],
localization: [
{
name: 'stages',
entities: [
'$..stage_a',
'$..stage_b',
],
id: 'id',
values: 'name',
},
{
name: 'game_modes',
entities: '$..game_mode',
id: 'key',
values: 'name',
},
{
name: 'rules',
entities: '$..rule',
id: 'key',
values: ['name', 'multiline_name'],
},
],
});
}

View File

@ -0,0 +1,53 @@
const Updater = require('./Updater');
class TimelineUpdater extends Updater {
constructor() {
super({
name: 'Timeline',
filename: 'timeline.json',
request: (splatnet) => splatnet.getTimeline(),
rootKeys: ['coop', 'weapon_availability'],
imagePaths: [
'$.coop..gear.image',
'$.coop..gear.brand.image',
'$.weapon_availability..weapon.image',
'$.weapon_availability..weapon.special.image_a',
'$.weapon_availability..weapon.sub.image_a',
],
localization: [
{
name: 'gear',
entities: '$.coop..gear',
id: 'id',
values: 'name',
},
{
name: 'brands',
entities: '$.coop..gear.brand',
id: 'id',
values: 'name',
},
{
name: 'weapons',
entities: '$.weapon_availability..weapon',
id: 'id',
values: 'name',
},
{
name: 'weapon_subs',
entities: '$.weapon_availability..weapon.sub',
id: 'id',
values: 'name',
},
{
name: 'weapon_specials',
entities: '$.weapon_availability..weapon.special',
id: 'id',
values: 'name',
},
],
});
}
}
module.exports = TimelineUpdater;

View File

@ -1,9 +1,12 @@
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const _ = require('lodash');
const jsonpath = require('jsonpath');
const SplatNet = require('../splatnet');
const raven = require('raven');
const { languages } = require('../../js/regions');
const LocalizationProcessor = require('../LocalizationProcessor');
const dataPath = path.resolve('public/data');
const splatnetAssetPath = path.resolve('public/assets/splatnet');
@ -16,12 +19,18 @@ class Updater {
async update() {
this.info('Updating data...');
// Use the first language as the default
let languageInfo = this.getLanguages()[0];
// Retrieve the data
let data = await this.handleRequest(this.getData());
let data = await this.handleRequest(this.getData(languageInfo));
// Filter the root keys if necessary
data = this.filterRootKeys(data);
// Update localizations
data = await this.updateLocalizations(data, languageInfo);
// Apply any other processing
data = await this.processData(data);
@ -38,8 +47,8 @@ class Updater {
this.info('Done.');
}
getData() {
let splatnet = new SplatNet;
getData({ region, language }) {
let splatnet = new SplatNet(region, language);
return this.options.request(splatnet);
}
@ -78,6 +87,50 @@ class Updater {
return data;
}
getLanguages() {
// Only return one entry per language
// (i.e., only return "region: NA language: en" and not "region: EU language: en")
return _.uniqBy(languages, 'language');
}
forEachLanguage(callback) {
for (let languageInfo of this.getLanguages())
this.forEachRuleset(languageInfo, callback);
}
forEachRuleset(languageInfo, callback) {
for (let ruleset of (this.options.localization)) {
let processor = new LocalizationProcessor(ruleset, languageInfo);
callback(processor, languageInfo);
}
}
async updateLocalizations(data, initialLanguageInfo) {
if (this.options.localization) {
// Update localization data for the initial language
this.forEachRuleset(initialLanguageInfo, processor => processor.updateLocalizations(data));
// Do we need to retrieve data for any other languages?
let missingLanguages = [];
this.forEachLanguage((processor, languageInfo) => {
if (missingLanguages.indexOf(languageInfo) === -1) {
if (!processor.hasLocalizations(data))
missingLanguages.push(languageInfo);
}
});
// Retrieve data for missing languages
for (let missingLanguageInfo of missingLanguages) {
this.info(`Retrieving localized data for region: ${missingLanguageInfo.region}, language: ${missingLanguageInfo.language}`);
let localData = await this.handleRequest(this.getData(missingLanguageInfo));
localData = this.filterRootKeys(localData);
this.forEachRuleset(missingLanguageInfo, processor => processor.updateLocalizations(localData));
}
}
return data;
}
shouldIncludeRootValue(value) {
if (!value)
return false;