| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview This file defines a singleton which provides access to all data |
| * that is available as soon as the page's resources are loaded (before DOM |
| * content has finished loading). This data includes both localized strings and |
| * any data that is important to have ready from a very early stage (e.g. things |
| * that must be displayed right away). |
| * |
| * Note that loadTimeData is not guaranteed to be consistent between page |
| * refreshes (https://crbug.com/740629) and should not contain values that might |
| * change if the page is re-opened later. |
| */ |
| |
| import {assert} from './assert.js'; |
| |
| export interface LoadTimeDataRaw { |
| [key: string]: any; |
| } |
| |
| class LoadTimeData { |
| private data_: LoadTimeDataRaw|null = null; |
| |
| /** |
| * Sets the backing object. |
| * |
| * Note that there is no getter for |data_| to discourage abuse of the form: |
| * |
| * var value = loadTimeData.data()['key']; |
| */ |
| set data(value: LoadTimeDataRaw) { |
| assert(!this.data_, 'Re-setting data.'); |
| this.data_ = value; |
| } |
| |
| /** |
| * @param id An ID of a value that might exist. |
| * @return True if |id| is a key in the dictionary. |
| */ |
| valueExists(id: string): boolean { |
| assert(this.data_, 'No data. Did you remember to include strings.js?'); |
| return id in this.data_; |
| } |
| |
| /** |
| * Fetches a value, expecting that it exists. |
| * @param id The key that identifies the desired value. |
| * @return The corresponding value. |
| */ |
| getValue(id: string): any { |
| assert(this.data_, 'No data. Did you remember to include strings.js?'); |
| const value = this.data_[id]; |
| assert(typeof value !== 'undefined', 'Could not find value for ' + id); |
| return value; |
| } |
| |
| /** |
| * As above, but also makes sure that the value is a string. |
| * @param id The key that identifies the desired string. |
| * @return The corresponding string value. |
| */ |
| getString(id: string): string { |
| const value = this.getValue(id); |
| assert(typeof value === 'string', `[${value}] (${id}) is not a string`); |
| return value; |
| } |
| |
| /** |
| * Returns a formatted localized string where $1 to $9 are replaced by the |
| * second to the tenth argument. |
| * @param id The ID of the string we want. |
| * @param args The extra values to include in the formatted output. |
| * @return The formatted string. |
| */ |
| getStringF(id: string, ...args: Array<string|number>): string { |
| const value = this.getString(id); |
| if (!value) { |
| return ''; |
| } |
| |
| return this.substituteString(value, ...args); |
| } |
| |
| /** |
| * Returns a formatted localized string where $1 to $9 are replaced by the |
| * second to the tenth argument. Any standalone $ signs must be escaped as |
| * $$. |
| * @param label The label to substitute through. This is not an resource ID. |
| * @param args The extra values to include in the formatted output. |
| * @return The formatted string. |
| */ |
| substituteString(label: string, ...args: Array<string|number>): string { |
| return label.replace(/\$(.|$|\n)/g, function(m) { |
| assert(m.match(/\$[$1-9]/), 'Unescaped $ found in localized string.'); |
| if (m === '$$') { |
| return '$'; |
| } |
| |
| const substitute = args[Number(m[1]) - 1]; |
| if (substitute === undefined || substitute === null) { |
| // Not all callers actually provide values for all substitutes. Return |
| // an empty value for this case. |
| return ''; |
| } |
| return substitute.toString(); |
| }); |
| } |
| |
| /** |
| * Returns a formatted string where $1 to $9 are replaced by the second to |
| * tenth argument, split apart into a list of pieces describing how the |
| * substitution was performed. Any standalone $ signs must be escaped as $$. |
| * @param label A localized string to substitute through. |
| * This is not an resource ID. |
| * @param args The extra values to include in the formatted output. |
| * @return The formatted string pieces. |
| */ |
| getSubstitutedStringPieces(label: string, ...args: Array<string|number>): |
| Array<{value: string, arg: (string|null)}> { |
| // Split the string by separately matching all occurrences of $1-9 and of |
| // non $1-9 pieces. |
| const pieces = (label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g) || |
| []).map(function(p) { |
| // Pieces that are not $1-9 should be returned after replacing $$ |
| // with $. |
| if (!p.match(/^\$[1-9]$/)) { |
| assert( |
| (p.match(/\$/g) || []).length % 2 === 0, |
| 'Unescaped $ found in localized string.'); |
| return {value: p.replace(/\$\$/g, '$'), arg: null}; |
| } |
| |
| // Otherwise, return the substitution value. |
| const substitute = args[Number(p[1]) - 1]; |
| if (substitute === undefined || substitute === null) { |
| // Not all callers actually provide values for all substitutes. Return |
| // an empty value for this case. |
| return {value: '', arg: p}; |
| } |
| return {value: substitute.toString(), arg: p}; |
| }); |
| |
| return pieces; |
| } |
| |
| /** |
| * As above, but also makes sure that the value is a boolean. |
| * @param id The key that identifies the desired boolean. |
| * @return The corresponding boolean value. |
| */ |
| getBoolean(id: string): boolean { |
| const value = this.getValue(id); |
| assert(typeof value === 'boolean', `[${value}] (${id}) is not a boolean`); |
| return value; |
| } |
| |
| /** |
| * As above, but also makes sure that the value is an integer. |
| * @param id The key that identifies the desired number. |
| * @return The corresponding number value. |
| */ |
| getInteger(id: string): number { |
| const value = this.getValue(id); |
| assert(typeof value === 'number', `[${value}] (${id}) is not a number`); |
| assert(value === Math.floor(value), 'Number isn\'t integer: ' + value); |
| return value; |
| } |
| |
| /** |
| * Override values in loadTimeData with the values found in |replacements|. |
| * @param replacements The dictionary object of keys to replace. |
| */ |
| overrideValues(replacements: LoadTimeDataRaw) { |
| assert( |
| typeof replacements === 'object', |
| 'Replacements must be a dictionary object.'); |
| assert(this.data_, 'Data must exist before being overridden'); |
| for (const key in replacements) { |
| this.data_[key] = replacements[key]; |
| } |
| } |
| |
| /** |
| * Reset loadTimeData's data. Should only be used in tests. |
| * @param newData The data to restore to, when null restores to unset state. |
| */ |
| resetForTesting(newData: LoadTimeDataRaw|null = null) { |
| this.data_ = newData; |
| } |
| |
| /** |
| * @return Whether loadTimeData.data has been set. |
| */ |
| isInitialized(): boolean { |
| return this.data_ !== null; |
| } |
| } |
| |
| export const loadTimeData = new LoadTimeData(); |