diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..41e0477 --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,12 @@ +// Constants +export const storage_lastUpdateTime = 'tf2wikipricing_lastUpdate'; +export const storage_schema = 'tf2wikipricing_schema'; +export const storage_version = 'tf2wikipricing_version'; +export const storage_priceprefix = 'tf2wikipricing_sku_'; +export const conversion_ref_usd = 0.0265; +export const defindex_key = 5021; +export const defindex_metal_refined = 5002; +export const defindex_metal_reclaimed = 5001; +export const defindex_metal_scrap = 5000; + +export * as config from './config'; \ No newline at end of file diff --git a/src/content/content.ts b/src/content/content.ts index aa983e7..f603810 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -1,57 +1,16 @@ -import { getStorageValue, setStorageValue } from './storage' import styleCss from './style.css' -declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise -import './GM_fetch' -import { logDebug, log, logError } from './log' +import { logDebug, log, logError } from './utils/log' import { getPricesToken, priceUsingPricesTF } from './pricing/pricestf' -const semver = require('semver') -// Globals import itemQualities from 'tf2-static-schema/static/qualities.json'; -var itemSchema: { [key: string]: {name: string, tradable: Boolean}; } | null; +import { getItemIndexByName, getTradableStatusByDefindex, ItemSchema, prepareSchema, wipeSchema } from './schemaService' +import { $T, extractLocaleFromURL } from './utils/localization' +import { fetchPrice, fetchKeyPrice, ItemPriceData } from './priceService' +import { createPriceRow, createStoreButton } from './uiRenderer' +import { findFirstElement, findFirstChildElement } from './utils/dom' +var itemSchema: ItemSchema | null; -declare const __VERSION__: string; - -// Constants -const storage_lastUpdateTime = 'tf2wikipricing_lastUpdate'; -const storage_schema = 'tf2wikipricing_schema'; -const storage_version = 'tf2wikipricing_version'; -const storage_priceprefix = 'tf2wikipricing_sku_'; -const conversion_ref_usd = 0.0265; -const defindex_key = 5021; -const defindex_metal_refined = 5002; -const defindex_metal_reclaimed = 5001; -const defindex_metal_scrap = 5000; - -/** Pricing data for a given TF2 item. */ -class ItemPriceData { - /** Item SKU. */ - sku: string - /** Date updated. */ - update: Date - /** TTL in milliseconds. */ - ttl: number - /** Price in keys. */ - keys: number - /** Price in refined metal. */ - metal: number - /** Steam Community Market price. */ - scmPrice: number - - toString(): string { - return `Price for ${this.sku}, fetched ${this.update} (expires ${this.update.getTime() + this.ttl})\n` + - JSON.stringify({ - keys: this.keys, - metal: this.metal, - scmPrice: this.scmPrice - }) - } -} - -class SteamMarketSearchResult { - name: string - sell_price: number -} +var pageLocale: string = 'en' /** Exclude these from the pricelist. */ const excludedQualities = new Set([ @@ -59,48 +18,7 @@ const excludedQualities = new Set([ 5, // Unusual ]); -const localizations: {[lang: string]: any} = { - 'en': require('../strings/en'), // English - 'es': require('../strings/es'), // Spanish - // 'ja': require('../strings/ja'), // Japanese - // 'it': require('../strings/it'), // Italian - // 'ar': require('../strings/ar.json') as object, // Arabic - // 'cs': require('../strings/cs.json') as object, // Czech - // 'da': require('../strings/da.json') as object, // Danish - // 'de': require('../strings/de.json') as object, // German - // 'fi': require('../strings/fi.json') as object, // Finnish - // 'fr': require('../strings/fr.json') as object, // French - // 'hu': require('../strings/hu.json') as object, // Hungarian - // 'ko': require('../strings/ko.json') as object, // Korean - // 'nl': require('../strings/nl.json') as object, // Dutch - // 'no': require('../strings/no.json') as object, // Norwegian Bokmål - // 'pl': require('../strings/pl.json') as object, // Polish - // 'pt': require('../strings/pt.json') as object, // Portuguese - // 'pt-BR': require('../strings/pt-BR.json') as object, // Brazilian Portuguese - // 'ro': require('../strings/ro.json') as object, // Romanian - // 'ru': require('../strings/ru.json') as object, // Russian - // 'sv': require('../strings/sv.json') as object, // Swedish - // 'tr': require('../strings/tr.json') as object, // Turkish - // 'zh-Hans': require('../strings/zh-Hans.json') as object, // Simplified Chinese - // 'zh-Hant': require('../strings/zh-Hant.json') as object, // Traditional Chinese -} - -function $T(s: string, locale?: Intl.LocalesArgument): string { - const code = locale ? locale.toString() : extractLocaleFromURL(document.URL) - return localizations.hasOwnProperty(code) ? (localizations[code as unknown as keyof object])[s] || s : s; -} - // Helper functions -function findFirstElement(selector: string): HTMLElement | null { - const elements = document.querySelectorAll(selector); - return elements.length > 0 ? elements[0] as HTMLElement : null; -} - -function findFirstChildElement(selector: string, root: Element): HTMLElement | null { - const elements = root.querySelectorAll(selector); - return elements.length > 0 ? elements[0] as HTMLElement : null; -} - function getKeyByValue(object: any, value: string) { return Object.keys(object).find(key => object[key] === value); } @@ -114,44 +32,6 @@ function extractPageTitleFromURL(url: string): string { return decodeURIComponent(split.replaceAll('_', ' ')); } -function extractLocaleFromURL(url: string): string { - var split = url.substring(url.indexOf("/wiki/") + "/wiki/".length); - if (split.indexOf('/') != -1) { - // Remove language suffix e.g. `/es` - return split.substring(split.indexOf('/') + 1); - } else { - return 'en'; - } -} - -function isDateAfterOneDay(date1: Date, date2: Date): boolean { - var diff = date2.getTime() - date1.getTime(); - var diffDays = Math.round(diff / (1000 * 3600 * 24)); - return diffDays > 1; -} - -function getItemIndexByName(name: string) { - for (const [defindex, value] of Object.entries(itemSchema)) { - if (value['name'] == name) { - return parseInt(defindex) - } - } - return null -} - -function getTradableStatusByDefindex(defindex: number) { - return itemSchema[defindex.toString()].tradable -} - -function getTradableStatusByName(name: string) { - for (const [defindex, value] of Object.entries(itemSchema)) { - if (value['name'] == name) { - return value.tradable - } - } - return true -} - // Main function async function inject() { const itemInfobox = findFirstElement('.item-infobox'); @@ -196,7 +76,7 @@ async function inject() { const url = document.URL; if (itemName && !itemIndex) { - itemIndex = getItemIndexByName(itemName) + itemIndex = getItemIndexByName(itemSchema, itemName) } } @@ -213,7 +93,7 @@ async function inject() { log(`Starting lookup for ${itemName} (defindex ${itemIndex})`); - if(getTradableStatusByDefindex(itemIndex) == false) { + if(getTradableStatusByDefindex(itemSchema, itemIndex) == false) { log(`${itemName} is not tradable, exiting`) return; } @@ -329,45 +209,6 @@ async function inject() { const qualifiedName = ((quality != 6 ? itemQualities[quality as unknown as keyof typeof itemQualities].toString() : '') + ' ' + itemName).trim() // logDebug(`Fetching price for ${qualifiedName}`) - /* - var data: ItemPriceData | null; - const cached = await getStorageValue(storage_priceprefix + sku, null) - if (cached != null && 'keys' in cached && 'metal' in cached) { - data = cached - } - if (!data || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) { - data = new ItemPriceData() - data.sku = sku - data.update = new Date() - data.ttl = (5 * 60 * 1000) // Cache results for 5 minutes - - // logDebug(JSON.stringify(steamMarketResults)) - // const scmResult = steamMarketResults.find((x: object) => { x['name' as keyof object] === qualifiedName}) - // logDebug(JSON.stringify(scmResult)) - // if(scmResult) { - // data.scmPrice = scmResult.sell_price / 100 - // } - - try { - const response = await priceUsingPricesTF(token, itemIndex, quality) - if (response) { - data.keys = response.keys - data.metal = response.metal - } - } catch (error) { - log(`Received ${error} error while pricing ${sku} using prices.tf`) - } - - if ('keys' in data && 'metal' in data) { - await setStorageValue(storage_priceprefix + sku, data) - } - logDebug(JSON.stringify(data)); - updateTime = new Date(data.update) - } else { - logDebug(`Using cached data for ${sku}, expires ${new Date(data.update).getTime() + data.ttl}`); - updateTime = new Date(data.update) - } - */ var data: ItemPriceData | null try { data = await fetchPrice(token, itemIndex, quality, currentTime); @@ -376,24 +217,8 @@ async function inject() { log(`${qualifiedName} is unpriced or unavailable, skipping...`) } - const priceRow = document.createElement("tr"); - - const priceLabel = document.createElement("td"); - priceLabel.className = "infobox-label"; - const priceLabelLink = document.createElement("a"); const qualityName = itemQualities[quality as unknown as keyof typeof itemQualities].toString() - priceLabelLink.href = locale === 'en' ? `https://wiki.teamfortress.com/wiki/${qualityName}` : `https://wiki.teamfortress.com/wiki/${qualityName}/${locale}` - priceLabelLink.innerText = $T(qualityName) - priceLabel.appendChild(priceLabelLink); - priceLabel.innerHTML += ':' - priceRow.appendChild(priceLabel); - - const priceData = document.createElement("td"); - const priceLink = document.createElement("span"); - const priceString = data ? formatPrice(data.keys, data.metal, keyPrice.metal).trim() : $T('Data unavailable') - priceLink.innerHTML = priceString // + `
$${data.scmPrice}` - priceData.appendChild(priceLink); - priceRow.appendChild(priceData); + const priceRow = createPriceRow(qualityName, data, keyPrice, locale) priceRows.push({quality: quality, row: priceRow}) }) @@ -428,170 +253,6 @@ async function inject() { }) } -function createStoreButton(storeName: string, url: URL) { - const button = document.createElement("tr") - var source = `` - source = source.replace("{link}", url.toString()) - source = source.replace("{title}", $T("View listings on %@").replace('%@', storeName)) - button.innerHTML = source - return button -} - -async function fetchKeyPrice(token: string) { - return fetchPrice(token, defindex_key, 6, new Date(), 86400000) -} - -/** - * Fetch a price for a given SKU, using cached values if available. - * @param token prices.tf access token. - * @param update Date retrieved. - * @param ttl Time to cache results in milliseconds. 30 minutes by default. - */ -async function fetchPrice(token: string, defIndex: number, quality: number, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise { - return new Promise(async (resolve, reject) => { - const sku = defIndex.toString() + ";" + quality.toString(); - var data: ItemPriceData | null - - const cached = await getStorageValue(storage_priceprefix + sku, null) - if (cached != null && 'keys' in cached && 'metal' in cached) { - data = cached - } - - if (!data || data.sku != sku || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) { - logDebug(`Fetching price data for ${sku}`) - if(!token) { - reject(401) - } - data = new ItemPriceData() - data.sku = sku - data.update = update - data.ttl = ttl - - try { - const response = await priceUsingPricesTF(token, defIndex, quality) - if (response) { - data.keys = response.keys - data.metal = response.metal - } - } catch (error) { - log(`Received ${error} error while pricing ${sku} using prices.tf`) - reject(error) - } - - if ('metal' in data && 'keys' in data) { - await setStorageValue(storage_priceprefix + sku, data) - } - } else { - logDebug(`Using cached price data for ${sku}`) - } - resolve(data) - }) -} - -async function getSteamResults(itemName: string) { - logDebug(`Making network request to Steam for ${itemName}`) - const response = await GM_fetch(`https://steamcommunity.com/market/search/render?appid=440&norender=true&count=10&query=${encodeURIComponent(itemName)}`, { - method: 'get', - headers: new Headers({ - 'Accept': 'application/json' - }) - }) - if (response.status === 200) { - const json = await response.json(); - return json['results'] - } - return [] -} - -function toFixed(num: number, fixed: number) { - var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?'); - return num.toString().match(re)[0]; -} - -var pageLocale: string = 'en' - -function formatPrice(keys: number, metal: number, keyPrice: number) { - const pureMetal = (keys * keyPrice) + metal; - const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2) - - var output: string = '' - if(keys > 0) { - output += (formattedKeys == 1.0 ? $T("%@ key") : $T("%@ keys")).replace('%@', formattedKeys.toLocaleString(pageLocale)) - } else { - output += `${(+toFixed(metal, 2)).toLocaleString(pageLocale)} ref` - } - const currencyFormatter = new Intl.NumberFormat(pageLocale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - - // Round price up to nearest cent - const price = Math.ceil(pureMetal * conversion_ref_usd * 100) / 100 - output += ` (US$${currencyFormatter.format(price)})` - return output; -} - -async function prepareSchema() { - var needsUpdate: Boolean = false - - const storedVersion: string | null = await getStorageValue(storage_version, null) - if(!storedVersion || !semver.valid(storedVersion)) { - log(`Cache is from an unknown version of the extension. Updating for version ${__VERSION__}`); - needsUpdate = true - } else if(semver.valid(storedVersion) && semver.lt(storedVersion, __VERSION__)) { - log(`Cache is from a previous version (${storedVersion}) of the extension. Updating for version ${__VERSION__}`); - needsUpdate = true - } else { - itemSchema = await getStorageValue(storage_schema, null); - } - - const update = await getStorageValue(storage_lastUpdateTime, null) - if (update) { - log(`Item schema updated at ${new Date(update)}`); - const lastUpdateTime = new Date(update); - if (!itemSchema || isDateAfterOneDay(lastUpdateTime, new Date())) { - needsUpdate = true - } - } - - if(needsUpdate) { - log("Item Schema out of Date. Rebuilding..."); - const url = "https://raw.githubusercontent.com/danocmx/node-tf2-static-schema/master/static/items.json" - const response = await GM_fetch(url); - if (response.ok) { - await setStorageValue(storage_lastUpdateTime, new Date().getTime()); - - var cacheItems = {} - - var responseItems: any[] = await response.json() - // We want to keep the keys `defindex`, `item_name`, and `attributes` - responseItems.forEach((item: any) => { - const defindex = item['defindex'] - const name = item['item_name'] - var tradable: Boolean = true - try { - if(item['attributes'] != null) { - if(item['attributes'].find((attribute: {}) => (attribute as any)['class'] == "cannot_trade")) { - tradable = false - } - } - } catch(error) { - logError(error) - log(item) - } - (cacheItems as any)[defindex.toString()] = { "name": name, "tradable": tradable } - }); - - await setStorageValue(storage_schema, (cacheItems)); - itemSchema = cacheItems - await setStorageValue(storage_version, __VERSION__); - logDebug(`Item schema updated at ${new Date()}`) - } else { - logError("Could not fetch item schema."); - } - } -} - function addStyles() { const head = document.head || document.getElementsByTagName('head')[0], style = document.createElement('style'); @@ -599,9 +260,11 @@ function addStyles() { style.innerHTML = styleCss; } -prepareSchema().then(function () { +prepareSchema().then(function (schema) { + itemSchema = schema; if (!itemSchema) { logError("No item schema ready, exiting."); + wipeSchema(); // FIXME: ugly hack. requires additional page reload. if prepareSchema returns null, we should handle it properly return; } pageLocale = extractLocaleFromURL(document.URL) diff --git a/src/content/priceService.ts b/src/content/priceService.ts new file mode 100644 index 0000000..fd40e0d --- /dev/null +++ b/src/content/priceService.ts @@ -0,0 +1,81 @@ +import { defindex_key, storage_priceprefix } from "./config" +import { priceUsingPricesTF } from "./pricing/pricestf" +import { getStorageValue, setStorageValue } from "./storage" +import { logDebug, log } from "./utils/log" + +/** Pricing data for a given TF2 item. */ +export class ItemPriceData { + /** Item SKU. */ + sku: string + /** Date updated. */ + update: Date + /** TTL in milliseconds. */ + ttl: number + /** Price in keys. */ + keys: number + /** Price in refined metal. */ + metal: number + /** Steam Community Market price. */ + scmPrice: number + + toString(): string { + return `Price for ${this.sku}, fetched ${this.update} (expires ${this.update.getTime() + this.ttl})\n` + + JSON.stringify({ + keys: this.keys, + metal: this.metal, + scmPrice: this.scmPrice + }) + } +} + + +export async function fetchKeyPrice(token: string) { + return fetchPrice(token, defindex_key, 6, new Date(), 86400000) +} + +/** + * Fetch a price for a given SKU, using cached values if available. + * @param token prices.tf access token. + * @param update Date retrieved. + * @param ttl Time to cache results in milliseconds. 30 minutes by default. + */ +export async function fetchPrice(token: string, defIndex: number, quality: number, update: Date = new Date(), ttl: number = 30 * 60 * 1000): Promise { + return new Promise(async (resolve, reject) => { + const sku = defIndex.toString() + ";" + quality.toString(); + var data: ItemPriceData | null + + const cached = await getStorageValue(storage_priceprefix + sku, null) + if (cached != null && 'keys' in cached && 'metal' in cached) { + data = cached + } + + if (!data || data.sku != sku || 'update' in data && 'ttl' in data && Date.now() > (new Date(data.update).getTime() + data.ttl)) { + logDebug(`Fetching price data for ${sku}`) + if(!token) { + reject(401) + } + data = new ItemPriceData() + data.sku = sku + data.update = update + data.ttl = ttl + + try { + const response = await priceUsingPricesTF(token, defIndex, quality) + if (response) { + data.keys = response.keys + data.metal = response.metal + } + } catch (error) { + log(`Received ${error} error while pricing ${sku} using prices.tf`) + reject(error) + } + + if ('metal' in data && 'keys' in data) { + await setStorageValue(storage_priceprefix + sku, data) + } + } else { + logDebug(`Using cached price data for ${sku}`) + } + resolve(data) + }) +} diff --git a/src/content/schemaService.ts b/src/content/schemaService.ts new file mode 100644 index 0000000..219f7a6 --- /dev/null +++ b/src/content/schemaService.ts @@ -0,0 +1,109 @@ +import { getStorageValue, setStorageValue } from './storage' +import { logDebug, log, logError } from './utils/log' +import './config' +declare function GM_fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise +import './GM_fetch' +import { storage_version, storage_schema, storage_lastUpdateTime } from './config' +const semver = require('semver') + +export declare const __VERSION__: string; + +function isDateAfterOneDay(date1: Date, date2: Date): boolean { + var diff = date2.getTime() - date1.getTime(); + var diffDays = Math.round(diff / (1000 * 3600 * 24)); + return diffDays > 1; +} + +export class ItemSchema { [key: string]: {name: string, tradable: Boolean}; } + +export function getItemIndexByName(schema: ItemSchema, name: string) { + for (const [defindex, value] of Object.entries(schema)) { + if (value['name'] == name) { + return parseInt(defindex) + } + } + return null +} + +export function getTradableStatusByDefindex(schema: ItemSchema, defindex: number) { + return schema[defindex.toString()].tradable +} + +export function getTradableStatusByName(schema: ItemSchema, name: string) { + for (const [defindex, value] of Object.entries(schema)) { + if (value['name'] == name) { + return value.tradable + } + } + return true +} + +export async function wipeSchema(): Promise { + await setStorageValue(storage_version, __VERSION__) + await setStorageValue(storage_schema, null) + await setStorageValue(storage_lastUpdateTime, new Date().toISOString()) + logDebug(`Schema wiped`) +} + +export async function prepareSchema(): Promise { + var needsUpdate: Boolean = false + var itemSchema: ItemSchema | null = null + + const storedVersion: string | null = await getStorageValue(storage_version, null) + if(!storedVersion || !semver.valid(storedVersion)) { + log(`Cache is from an unknown version of the extension. Updating for version ${__VERSION__}`); + needsUpdate = true + } else if(semver.valid(storedVersion) && semver.lt(storedVersion, __VERSION__)) { + log(`Cache is from a previous version (${storedVersion}) of the extension. Updating for version ${__VERSION__}`); + needsUpdate = true + } else { + itemSchema = await getStorageValue(storage_schema, null); + } + + const update = await getStorageValue(storage_lastUpdateTime, null) + if (update) { + const lastUpdateTime = new Date(update); + log(`Item schema updated at ${lastUpdateTime}`); + if (itemSchema == null || Object.keys(itemSchema).length === 0 || isDateAfterOneDay(lastUpdateTime, new Date())) { + needsUpdate = true + } + } + + if(needsUpdate) { + log("Item Schema out of Date. Rebuilding..."); + const url = "https://raw.githubusercontent.com/danocmx/node-tf2-static-schema/master/static/items.json" + const response = await GM_fetch(url); + if (response.ok) { + await setStorageValue(storage_lastUpdateTime, new Date().getTime()); + + var cacheItems = {} + + var responseItems: any[] = await response.json() + // We want to keep the keys `defindex`, `item_name`, and `attributes` + responseItems.forEach((item: any) => { + const defindex = item['defindex'] + const name = item['item_name'] + var tradable: Boolean = true + try { + if(item['attributes'] != null) { + if(item['attributes'].find((attribute: {}) => (attribute as any)['class'] == "cannot_trade")) { + tradable = false + } + } + } catch(error) { + logError(error) + log(item) + } + (cacheItems as any)[defindex.toString()] = { "name": name, "tradable": tradable } + }); + + await setStorageValue(storage_schema, (cacheItems)); + itemSchema = cacheItems + await setStorageValue(storage_version, __VERSION__); + logDebug(`Item schema updated at ${new Date()}`) + } else { + logError("Could not fetch item schema."); + } + } + return itemSchema +} \ No newline at end of file diff --git a/src/content/uiRenderer.ts b/src/content/uiRenderer.ts new file mode 100644 index 0000000..38c30ce --- /dev/null +++ b/src/content/uiRenderer.ts @@ -0,0 +1,33 @@ +import { ItemPriceData } from "./priceService"; +import { formatPrice } from "./utils/formatting"; +import { $T } from "./utils/localization"; + +export function createPriceRow(qualityName: string, data: ItemPriceData, keyPrice: ItemPriceData, locale: string): HTMLTableRowElement { + const priceRow = document.createElement("tr"); + + const priceLabel = document.createElement("td"); + priceLabel.className = "infobox-label"; + const priceLabelLink = document.createElement("a"); + priceLabelLink.href = locale === 'en' ? `https://wiki.teamfortress.com/wiki/${qualityName}` : `https://wiki.teamfortress.com/wiki/${qualityName}/${locale}` + priceLabelLink.innerText = $T(qualityName) + priceLabel.appendChild(priceLabelLink); + priceLabel.innerHTML += ':' + priceRow.appendChild(priceLabel); + + const priceData = document.createElement("td"); + const priceLink = document.createElement("span"); + const priceString = data ? formatPrice(data.keys, data.metal, keyPrice.metal, locale).trim() : $T('Data unavailable') + priceLink.innerHTML = priceString // + `
$${data.scmPrice}` + priceData.appendChild(priceLink); + priceRow.appendChild(priceData); + return priceRow; +} + +export function createStoreButton(storeName: string, url: URL) { + const button = document.createElement("tr") + var source = `` + source = source.replace("{link}", url.toString()) + source = source.replace("{title}", $T("View listings on %@").replace('%@', storeName)) + button.innerHTML = source + return button +} diff --git a/src/content/utils/dom.ts b/src/content/utils/dom.ts new file mode 100644 index 0000000..722c5d5 --- /dev/null +++ b/src/content/utils/dom.ts @@ -0,0 +1,9 @@ +export function findFirstElement(selector: string): HTMLElement | null { + const elements = document.querySelectorAll(selector); + return elements.length > 0 ? elements[0] as HTMLElement : null; +} + +export function findFirstChildElement(selector: string, root: Element): HTMLElement | null { + const elements = root.querySelectorAll(selector); + return elements.length > 0 ? elements[0] as HTMLElement : null; +} diff --git a/src/content/utils/formatting.ts b/src/content/utils/formatting.ts new file mode 100644 index 0000000..25083db --- /dev/null +++ b/src/content/utils/formatting.ts @@ -0,0 +1,28 @@ +import { conversion_ref_usd } from '../config'; +import { $T } from './localization' + +function toFixed(num: number, fixed: number) { + var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?'); + return num.toString().match(re)[0]; +} + +export function formatPrice(keys: number, metal: number, keyPrice: number, locale: string = 'en') { + const pureMetal = (keys * keyPrice) + metal; + const formattedKeys = +(keys + (metal / keyPrice)).toFixed(2) + + var output: string = '' + if(keys > 0) { + output += (formattedKeys == 1.0 ? $T("%@ key") : $T("%@ keys")).replace('%@', formattedKeys.toLocaleString(locale)) + } else { + output += `${(+toFixed(metal, 2)).toLocaleString(locale)} ref` + } + const currencyFormatter = new Intl.NumberFormat(locale, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + + // Round price up to nearest cent + const price = Math.ceil(pureMetal * conversion_ref_usd * 100) / 100 + output += ` (US$${currencyFormatter.format(price)})` + return output; +} diff --git a/src/content/utils/localization.ts b/src/content/utils/localization.ts new file mode 100644 index 0000000..6b262b0 --- /dev/null +++ b/src/content/utils/localization.ts @@ -0,0 +1,40 @@ +const localizations: {[lang: string]: any} = { + 'en': require('../../strings/en'), // English + 'es': require('../../strings/es'), // Spanish + // 'ja': require('../../strings/ja'), // Japanese + // 'it': require('../../strings/it'), // Italian + // 'ar': require('../../strings/ar.json') as object, // Arabic + // 'cs': require('../../strings/cs.json') as object, // Czech + // 'da': require('../../strings/da.json') as object, // Danish + // 'de': require('../../strings/de.json') as object, // German + // 'fi': require('../../strings/fi.json') as object, // Finnish + // 'fr': require('../../strings/fr.json') as object, // French + // 'hu': require('../../strings/hu.json') as object, // Hungarian + // 'ko': require('../../strings/ko.json') as object, // Korean + // 'nl': require('../../strings/nl.json') as object, // Dutch + // 'no': require('../../strings/no.json') as object, // Norwegian Bokmål + // 'pl': require('../../strings/pl.json') as object, // Polish + // 'pt': require('../../strings/pt.json') as object, // Portuguese + // 'pt-BR': require('../../strings/pt-BR.json') as object, // Brazilian Portuguese + // 'ro': require('../../strings/ro.json') as object, // Romanian + // 'ru': require('../../strings/ru.json') as object, // Russian + // 'sv': require('../../strings/sv.json') as object, // Swedish + // 'tr': require('../../strings/tr.json') as object, // Turkish + // 'zh-Hans': require('../../strings/zh-Hans.json') as object, // Simplified Chinese + // 'zh-Hant': require('../../strings/zh-Hant.json') as object, // Traditional Chinese +} + +export function $T(s: string, locale?: Intl.LocalesArgument): string { + const code = locale ? locale.toString() : extractLocaleFromURL(document.URL) + return localizations.hasOwnProperty(code) ? (localizations[code as unknown as keyof object])[s] || s : s; +} + +export function extractLocaleFromURL(url: string): string { + var split = url.substring(url.indexOf("/wiki/") + "/wiki/".length); + if (split.indexOf('/') != -1) { + // Remove language suffix e.g. `/es` + return split.substring(split.indexOf('/') + 1); + } else { + return 'en'; + } +} diff --git a/src/content/log.ts b/src/content/utils/log.ts similarity index 100% rename from src/content/log.ts rename to src/content/utils/log.ts