UNPKG

@jargon/platform-sdk-core

Version:

Core components of the Jargon Platform SDK for node.js

546 lines 21.9 kB
"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