@jargon/platform-sdk-core
Version:
Core components of the Jargon Platform SDK for node.js
546 lines • 21.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JargonResourceManager = exports.LocaleData = void 0;
const intl_format_cache_1 = __importDefault(require("intl-format-cache"));
const intl_messageformat_1 = require("intl-messageformat");
const defs_1 = require("./defs");
const fs = __importStar(require("./fsPromises"));
const icuUtils_1 = require("./icuUtils");
const references_1 = require("./references");
// TODO: Determine impact on startup time and if this should be deferred until first use
const formatters = {
getNumberFormat: intl_format_cache_1.default(Intl.NumberFormat),
getDateTimeFormat: intl_format_cache_1.default(Intl.DateTimeFormat),
getPluralRules: intl_format_cache_1.default(Intl.PluralRules)
};
const imfOpts = {
formatters: formatters
};
class LocaleData {
constructor(locale, _dataPath) {
this.locale = locale;
this._dataPath = _dataPath;
this._cache = new Map();
}
loadContent() {
if (!this._contentPromise) {
this._contentPromise = this._doLoadContent();
}
}
content() {
if (!this._contentPromise) {
this.loadContent();
}
return this._contentPromise;
}
cachedStringResource(ck, msg) {
let f = this._cache.get(ck);
if (f) {
return f;
}
f = this._buildCacheEntry(msg);
this._cache.set(ck, f);
return f;
}
async _doLoadContent() {
const data = await fs.readFile(this._dataPath, { encoding: 'utf8' });
const content = JSON.parse(data);
if (!defs_1.isLocaleContentV1(content)) {
throw new Error(`Unable to parse '${this._dataPath}' as Jargon content data`);
}
return content;
}
_buildCacheEntry(msg) {
const subs = [];
const params = icuUtils_1.extractParamNames(msg);
let count = 0;
const refs = references_1.locateCrossReferences(msg);
const processed = new Set();
refs.forEach(r => {
if (!processed.has(r)) {
const ref = references_1.parseCrossReference(r);
let n;
do {
n = makeParamID(count++);
} while (params.has(n));
subs.push({
param: n,
ref
});
processed.add(r);
msg = msg.replace(r, `{${n}}`);
}
});
return {
format: new intl_messageformat_1.IntlMessageFormat(msg, this.locale, undefined, imfOpts),
subs
};
}
}
exports.LocaleData = LocaleData;
function makeParamID(i) {
return `__cr_${i}`;
}
class JargonResourceManager {
constructor(_opts, _platform, _data, _fallbacks) {
this._opts = _opts;
this._platform = _platform;
this._data = _data;
this._fallbacks = _fallbacks;
this._rv = Math.random();
this._selectedVariants = [];
this.locale = this._data.locale;
}
async render(item) {
const params = await this._convertParams(item.params);
return this._renderString(item.key, params, item.options);
}
renderBatch(items) {
const promises = items.map(i => this.render(i));
return Promise.all(promises);
}
async renderObject(item) {
const c = await this._loadObjectContent(item.key);
const vk = this._selectVariant(c, item.options);
const obj = c.variants[vk];
let params = await this._convertParams(item.params);
return this._renderObjectResource(item.key, vk, [], params, obj.content, item.options);
}
async renderResponse(item) {
const c = await this._loadResponseContent(item.key);
const vk = this._selectVariant(c, item.options);
const ro = c.variants[vk];
let params = await this._convertParams(item.params);
const rv = {};
for (const f of defs_1.ResponseVariantStringResources) {
const v = ro[f];
if (v) {
const ck = responseComponentCacheKey(item.key, vk, f);
rv[f] = await this._renderStringResource(ck, params, v, item.options);
}
}
if (ro.alexaCard && this._platform === 'Alexa') {
const c = Object.assign({}, ro.alexaCard.content);
const ckTitle = responseComponentCacheKey(item.key, vk, 'alexaCard.title');
c.title = await this._renderCachedStringResource(ckTitle, c.title, params, item.options);
// Handle incorrect case for card type
if (c.type === 'simple') {
c.type = 'Simple';
}
else if (c.type === 'standard') {
c.type = 'Standard';
}
if (c.type === 'Simple') {
const ckContent = responseComponentCacheKey(item.key, vk, 'alexaCard.content');
c.content = await this._renderCachedStringResource(ckContent, c.content, params, item.options);
}
else if (c.type === 'Standard') {
const ckText = responseComponentCacheKey(item.key, vk, 'alexaCard.text');
c.text = await this._renderCachedStringResource(ckText, c.text, params, item.options);
}
rv.alexaCard = {
content: c
};
}
if (ro.googleCard && this._platform === 'ActionsOnGoogle') {
const c = Object.assign({}, ro.googleCard.content);
if (c.title) {
const ckTitle = responseComponentCacheKey(item.key, vk, 'googleCard.title');
c.title = await this._renderCachedStringResource(ckTitle, c.title, params, item.options);
}
if (c.subtitle) {
const ckSubtitle = responseComponentCacheKey(item.key, vk, 'googleCard.subtitle');
c.subtitle = await this._renderCachedStringResource(ckSubtitle, c.subtitle, params, item.options);
}
if (c.text) {
const ckText = responseComponentCacheKey(item.key, vk, 'googleCard.text');
c.text = await this._renderCachedStringResource(ckText, c.text, params, item.options);
}
if (c.image) {
if (c.image.imageUrl) {
const ckImageUrl = responseComponentCacheKey(item.key, vk, 'googleCard.image.imageUrl');
c.image.imageUrl = await this._renderCachedStringResource(ckImageUrl, c.image.imageUrl, params, item.options);
}
if (c.image.imageAlt) {
const ckImageAlt = responseComponentCacheKey(item.key, vk, 'googleCard.image.imageAlt');
c.image.imageAlt = await this._renderCachedStringResource(ckImageAlt, c.image.imageAlt, params, item.options);
}
}
if (c.button) {
if (c.button.buttonTitle) {
const ckButtonTitle = responseComponentCacheKey(item.key, vk, 'googleCard.button.buttonTitle');
c.button.buttonTitle = await this._renderCachedStringResource(ckButtonTitle, c.button.buttonTitle, params, item.options);
}
if (c.button.buttonUrl) {
const ckButtonUrl = responseComponentCacheKey(item.key, vk, 'googleCard.button.buttonUrl');
c.button.buttonUrl = await this._renderCachedStringResource(ckButtonUrl, c.button.buttonUrl, params, item.options);
}
}
rv.googleCard = {
content: c
};
}
if (ro.googleSuggestions && this._platform === 'ActionsOnGoogle') {
const googleSuggestions = ro.googleSuggestions.content;
const suggestionsList = [];
for (const suggestion in googleSuggestions) {
const currentSuggestion = googleSuggestions[suggestion];
const svk = `${vk}//googleSuggestions//${suggestion}`;
const ckSuggestionText = responseComponentCacheKey(item.key, svk, 'chip');
const chipResource = await this._renderCachedStringResource(ckSuggestionText, currentSuggestion.chip, params, item.options);
if (currentSuggestion.url) {
const ckSuggestionUrl = responseComponentCacheKey(item.key, svk, 'url');
const urlResource = await this._renderCachedStringResource(ckSuggestionUrl, currentSuggestion.url, params, item.options);
suggestionsList.push({ chip: chipResource, url: urlResource });
}
else {
suggestionsList.push({ chip: chipResource });
}
}
rv.googleSuggestions = {
content: suggestionsList
};
}
if (ro.googleMediaObject && this._platform === 'ActionsOnGoogle') {
const m = Object.assign({}, ro.googleMediaObject.content);
const ckUrl = responseComponentCacheKey(item.key, vk, 'googleMediaObject.url');
m.url = await this._renderCachedStringResource(ckUrl, m.url, params, item.options);
if (m.name) {
const ckName = responseComponentCacheKey(item.key, vk, 'googleMediaObject.name');
m.name = await this._renderCachedStringResource(ckName, m.name, params, item.options);
}
if (m.description) {
const ckDescription = responseComponentCacheKey(item.key, vk, 'googleMediaObject.description');
m.description = await this._renderCachedStringResource(ckDescription, m.description, params, item.options);
}
if (m.imageOrIcon) {
const ckImageUrl = responseComponentCacheKey(item.key, vk, 'googleMediaObject.imageOrIcon.url');
m.imageOrIcon.url = await this._renderCachedStringResource(ckImageUrl, m.imageOrIcon.url, params, item.options);
const ckImageAlt = responseComponentCacheKey(item.key, vk, 'googleMediaObject.imageOrIcon.alt');
m.imageOrIcon.alt = await this._renderCachedStringResource(ckImageAlt, m.imageOrIcon.alt, params, item.options);
}
rv.googleMediaObject = {
content: m
};
}
if (ro.alexaDirectives) {
const directives = ro.alexaDirectives;
const directivesRecords = {};
for (const directive in directives) {
const dvk = `${vk}//alexaDirectives//${directive}`;
const obj = await this._renderObjectResource(item.key, dvk, [], params, directives[directive], item.options);
directivesRecords[directive] = obj;
}
rv.alexaDirectives = directivesRecords;
}
return rv;
}
async renderAll(items) {
const ps = items.map(i => {
if (i.type === 'string') {
return this.render(i);
}
else if (i.type === 'response') {
return this.renderResponse(i);
}
else if (i.type === 'object') {
return this.renderObject(i);
}
throw new Error(`unrecognized item type ${i.type}`);
});
const results = [];
for (let i = 0; i < items.length; i++) {
const key = items[i].key;
const type = items[i].type;
let res = undefined;
try {
const r = await ps[i];
if (type === 'string') {
const srr = {
key,
type,
status: 'found',
result: r
};
res = srr;
}
else if (type === 'response') {
const rrr = {
key,
type,
status: 'found',
result: r
};
res = rrr;
}
else if (type === 'object') {
const orr = {
key,
type,
status: 'found',
result: r
};
res = orr;
}
}
catch (e) {
res = {
key,
type,
status: 'not_found' // TODO figure out how to return param_not_found when appropriate
};
}
if (!res) {
throw new Error(`unrecognized item type ${type}`);
}
results.push(res);
}
return results;
}
async _loadObjectContent(key) {
let c = await this._tryLoadObjectContent(key);
if (!c) {
for (const fb of this._fallbacks) {
c = await fb._tryLoadObjectContent(key);
if (c) {
break;
}
}
}
if (!c) {
throw new Error(`${key} not present in locale ${this.locale} objects`);
}
return c;
}
async _tryLoadObjectContent(key) {
const content = await this._data.content();
if (!content.objects) {
content.objects = {};
}
return content.objects[key];
}
async _loadResponseContent(key) {
let c = await this._tryLoadResponseContent(key);
if (!c) {
for (const fb of this._fallbacks) {
c = await fb._tryLoadResponseContent(key);
if (c) {
break;
}
}
}
if (!c) {
throw new Error(`${key} not present in locale ${this.locale} responses`);
}
return c;
}
async _tryLoadResponseContent(key) {
const content = await this._data.content();
if (!content.responses) {
content.responses = {};
}
return content.responses[key];
}
async _loadStringContent(key) {
let c = await this._tryLoadStringContent(key);
if (!c) {
for (const fb of this._fallbacks) {
c = await fb._tryLoadStringContent(key);
if (c) {
break;
}
}
}
if (!c) {
throw new Error(`${key} not present in locale ${this.locale} strings`);
}
return c;
}
async _tryLoadStringContent(key) {
const content = await this._data.content();
if (!content.strings) {
content.strings = {};
}
return content.strings[key];
}
async _renderString(key, params, opts) {
const c = await this._loadStringContent(key);
const variant = this._selectVariant(c, opts);
return this._renderStringContent(key, variant, c.variants[variant].content, params, opts);
}
async _renderStringVariant(key, variant, params, opts) {
const c = await this._loadStringContent(key);
const vc = c.variants[variant];
if (!vc) {
throw new Error(`string ${key} does not contain variant ${variant} in locale ${this.locale}`);
}
return this._renderStringContent(key, variant, vc.content, params, opts);
}
_renderStringContent(key, variant, content, params, opts) {
const ck = stringCacheKey(key, variant);
return this._renderCachedStringResource(ck, content, params, opts);
}
async _renderResponseComponent(key, variant, component, params, opts) {
const c = await this._loadResponseContent(key);
const vc = c.variants[variant];
if (!vc) {
throw new Error(`response ${key} does not contain variant ${variant} in locale ${this.locale}`);
}
if (!vc[component]) {
throw new Error(`response ${key} variant ${variant} does not contain ${component} in locale ${this.locale}`);
}
const content = vc[component].content;
if (!content) {
throw new Error(`response ${key} variant ${variant} component ${component} is not a string resource`);
}
const ck = responseComponentCacheKey(key, variant, component);
return this._renderCachedStringResource(ck, content, params, opts);
}
_renderCachedStringResource(ck, content, params, opts) {
const csr = this._data.cachedStringResource(ck, content);
return this._renderCacheEntry(csr, params, opts);
}
async _renderCacheEntry(csr, params, opts) {
const p = Object.assign({}, params);
for (const s of csr.subs) {
const rendered = await this._renderCrossReference(s, params, opts);
p[s.param] = rendered;
}
return csr.format.format(p);
}
async _renderCrossReference(s, params, opts) {
const ref = s.ref;
if (ref.type === 'String') {
return this._renderString(ref.resource, params, opts);
}
else if (ref.type === 'StringVariant') {
return this._renderStringVariant(ref.resource, ref.variant, params, opts);
}
else if (ref.type === 'ResponseComponent') {
return this._renderResponseComponent(ref.resource, ref.variant, ref.component, params, opts);
}
throw new Error(`Unsupported cross-reference type`);
}
async _renderStringResource(ck, params, resource, opts) {
const content = await this._renderCachedStringResource(ck, resource.content, params, opts);
return Object.assign(Object.assign({}, resource), { content });
}
async _renderObjectResource(key, variant, path, params, obj, opts) {
const rv = {};
for (const k of Object.keys(obj)) {
const v = obj[k];
if (typeof v === 'object') {
path.push(k);
rv[k] = await this._renderObjectResource(key, variant, path, params, v, opts);
path.pop();
}
else if (typeof v === 'string') {
path.push(k);
const ck = objectPartCacheKey(key, variant, path);
path.pop();
rv[k] = await this._renderCachedStringResource(ck, v, params, opts);
}
else {
// TODO Traverse through arrays
rv[k] = v;
}
}
return rv;
}
_selectVariant(vc, opts) {
const candidates = [];
const variants = vc.variants;
Object.keys(variants).forEach(k => {
const v = variants[k];
if (v.__jmd) {
const md = v.__jmd;
if (md.platforms) {
if (md.platforms.includes(this._platform)) {
candidates.push(k);
}
}
else if (!(md.excludedPlatforms && md.excludedPlatforms.includes(this._platform))) {
candidates.push(k);
}
}
else {
candidates.push(k);
}
});
return this._selectKey(candidates, opts);
}
selectedVariation(item) {
let v = this._selectedVariants.filter(i => i.item === item);
if (v.length === 0) {
return Promise.reject(new Error('Item was not used for any render calls that selected a variation'));
}
return Promise.resolve(v[0]);
}
selectedVariations() {
return Promise.resolve(this._selectedVariants);
}
async _convertParams(params) {
const r = {};
for (let k in params) {
let v = params[k];
if (typeof v === 'object') {
if (v.key) {
try {
r[k] = await this.render(v);
}
catch (e) {
r[k] = JSON.stringify(v);
}
}
else {
r[k] = JSON.stringify(v);
}
}
else {
r[k] = v;
}
}
return r;
}
_selectKey(keys, opts) {
let rv = this._rv;
if ((opts && opts.forceNewRandom) || !this._opts.consistentRandom) {
rv = Math.random();
}
let i = Math.floor(rv * keys.length);
return keys[i];
}
}
exports.JargonResourceManager = JargonResourceManager;
function stringCacheKey(key, variant) {
return `strings:${key}//${variant}`;
}
function responseComponentCacheKey(key, variant, component) {
return `responses:${key}//${variant}//${component}`;
}
function objectPartCacheKey(key, variant, parts) {
return `objects:${key}//${variant}//${parts.join('.')}`;
}
//# sourceMappingURL=jrm.js.map