import { makeMetatype } from "@jet/environment/util/metatype"; import { AmpLocalization } from "../amp-localization/amp-localization"; import { isNullOrEmpty } from "../json-parsing/server-data"; import { Wrapper } from "./wrapper"; import { isSome } from "@jet/environment/types/optional"; import * as validation from "@jet/environment/json/validation"; /** * Wrapper around an object or `Localization` type. * The wrapped object is implemented as part of a native app. */ export class LocalizationWrapper extends Wrapper { constructor(loc, objectGraph) { super(loc); /** * Path to localization file. * The path is set when the localization file is loaded * for the first time. */ this.locFile = null; /** * Instance of `AMPLocalization` providing server-side * values for localized stirngs. */ this.ampLoc = new AmpLocalization(); /** * Localization strings cache. * Cache and reuse values returned by * wrapped `Localization` implementation. */ this.LOC_STRING_CACHE = {}; this.objectGraph = objectGraph; } /** * Returns the wrapped localization's identifier. */ get identifier() { return this.implementation.identifier; } /** * Returns the wrapped localization's safe identifier. */ get safeIdentifier() { return this.implementation.identifier.split("_")[0]; } // endregion // region Localization /** * Localizes a string replacing placehoders in key with values in the parameters dictionary * @param key The loc key to look up * @param params Parameters to replace in the loc string * @return The localized string */ string(key, defaultValue) { return this.implementation.string(key); } /** * Localizes a string, and logs & throws an error if the key is a screamer. * @param key The loc key to look up * @return The localized string */ tryString(key) { const value = this.implementation.string(key); if (value === key || value === `**${key}**`) { validation.context("tryString", () => { validation.unexpectedType("coercedValue", "Localization key", key, null); }); throw new Error(`No value exists for localization key '${key}'`); } return value; } /** * Localizes a string, and logs an error if the key is a screamer. * @param key The loc key to look up * @param fallback The fallback value for unlocalized keys, e.g. English value * @return The localized string */ stringWithFallback(key, fallback) { const value = this.implementation.string(key); return value === `**AppStore.${key}**` ? fallback : value; } /** * Localizes a string using a preferred locale. * * The implementation of this function is similar to `string(key:)` above, but with the option * to prefer a particular locale. If one is provided, we augment the lookup key in the cache * with the `locale` value. If one is not provided, we fallback to `string(key:)`. This makes it * much easier to use this function directly, in place of `string(key:)` where necessary. * @param objectGraph The object graph, used to forward this call on if the required native function is unavailable. * Can be removed for 2023. * @param key The loc key to look up. * @param locale The preferred locale to use for look up. Falls back to default, if unavailable. * @param defaultValue A default value to use if nothing is found. */ stringForPreferredLocale(objectGraph, key, locale, defaultValue) { if (isNullOrEmpty(locale)) { return this.string(key, defaultValue); } const cacheKey = `${key}_${locale}`; let value = this.LOC_STRING_CACHE[cacheKey]; if (!value) { value = this.implementation.stringForPreferredLocale(key, locale); if (value && value !== key) { this.LOC_STRING_CACHE[cacheKey] = value; } else { const serverValue = this.ampLoc.localize(key); if (serverValue !== key) { value = serverValue; } else if (defaultValue) { value = defaultValue; } else { value = key; } } } return value; } /** * Localize with appropriate plural form based on the count. * * Some languages have more plural forms than others. * The full set of categories is "zero", "one", "two", "few", "many", and "other". * * The base loc key is used for "other" (the default). * Otherwise the category is appended to the base loc key with a "." separator. * * http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html * http://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-numbers.html#Language_Plural_Rules * * English loc keys * key = "@@count@@ dogs"; // default, aka "plural" * key.zero = "no dogs"; // used when count is 0 * key.one = "@@count@@ dog"; // used when count is 1, aka "singular" * * @param key base loc key * @param count number used to determine the plural form * @param params (optional) substitution keys and values, "count" will be added if it's not already present * @return localized string */ stringWithCount(key, count, params) { let value = this.implementation.stringWithCount(key, count); if (!value || value === key) { const serverValue = this.ampLoc.localizeWithCount(this.objectGraph, key, count, params); if (serverValue) { value = serverValue; } } return value; } /** * A variation of `stringWithCount` that supports multiple plural forms. * @param key base loc key * @param counts numbers used to determine the plural forms * @param params (optional) substitution keys and values, "count" will be added if it's not already present * @return localized string */ stringWithCounts(key, counts, params) { return this.implementation.stringWithCounts(key, counts); } /** * Converts a string into its uppercased form. * @param value The string to apply the uppercase to. * @returns {string} The loacle-specific, uppercased form of the string. If for whatever reason the upper-casing cannot * be applied, this function simply returns the input value. */ uppercased(value) { if (!value) { return null; } return value.toLocaleUpperCase(this.safeIdentifier); } /** * Converts a number into a localized string * * @param n The number to convert * @param decimalPlaces The number of decimal places to include. * @return {string} The localized version of the number */ decimal(n, decimalPlaces) { let value = this.implementation.decimal(n, decimalPlaces); if (!value) { if (typeof n === "number") { value = `* ${n.toString()} *`; } else { value = this.nullString(); } } return value; } /** * Converts a number of bytes into a localized file size string * * @param bytes The number of bytes to convert * @return The localized file size string */ fileSize(bytes) { let value = this.implementation.fileSize(bytes); if (!value) { value = this.nullString(); } return value; } /** * Converts a number of bytes into a localized file size string * * @param count The number of bytes to convert * @return The localized file size string */ formattedCount(count) { let value = this.implementation.formattedCount(count); if (!value) { value = this.nullString(); } return value; } /** * Converts a number into a formatted string representation using the preferred locale identifier. * * @param count The number to format. * @param locale The locale identifier to prefer for lookup. * @return The localized string. */ formattedCountForPreferredLocale(objectGraph, count, locale) { if (isNullOrEmpty(locale)) { return this.formattedCount(count); } let value = this.implementation.formattedCountForPreferredLocale(count, locale); if (!value) { value = this.nullString(); } return value; } /** * Converts a date into a time ago label, showing how long ago * the date occurred * * @param date The date object to convert * @param context The context in which the date should be formatted * @return The localized string representing the amount of time that has passed */ timeAgoWithContext(date, context) { let value = this.implementation.timeAgoWithContext(date, context); if (!value) { value = this.nullString(); } return value; } /** * Converts a date into a localized date string using the provided format * * @param format The format string describing how the date should be formatted * @param date The date object to convert * @return The localized string representing the date */ formatDate(format, date) { let value = this.implementation.formatDate(format, date); if (!value) { value = this.nullString(); } return value; } formatDateWithContext(format, date, context) { let value = this.implementation.formatDateWithContext(format, date, context); if (!value) { value = this.nullString(); } return value; } formatDateInSentence(sentence, format, date) { let value = this.implementation.formatDateInSentence(sentence, format, date); if (!value) { value = this.nullString(); } return value; } /** * Converts a date into a relative date, showing how long ago * the date occurred * * @param date The date object to convert * @return The localized string representing the amount of time that has passed */ relativeDate(date) { let value = this.implementation.relativeDate(date); if (!value) { value = this.nullString(); } return value; } formatDuration(value, unit) { let result = this.implementation.formatDuration(value, unit); if (!result) { result = this.nullString(); } return result; } // endregion // region Private Methods /** * Applies a new localization file and localization dictionary as the new set of localizations * * @param file The file name of the file that is loaded * @param localizations The localizations dictionary to use * @param locale Current normalized locale which includes language code */ applyLocalizations(file, localizations, locale) { if (this.isLocFileLoaded(file)) { return; } this.locFile = file; // The first 2 characters of normalized locale are the language code. this.ampLoc.updateLocalizationData(localizations, locale.slice(0, 2)); } /** * Checks if the named loc file is already loaded * @param file The file name to check * @return {boolean} True or false depending on whether the file name is loaded */ isLocFileLoaded(file) { return this.locFile === file; } /** * Normalizes the given bag language into a locale. * @param language The language to normalize. * @param storefrontIdentifier The storefront identifier to use. * @return A locale to use. */ normalizedLocale(objectGraph, language, storefrontIdentifier) { language = language.toLowerCase(); switch (language) { case "yue-hant": { // Country code from the bag is not available in JS, so we use the storefront. const macauStoreFrontIdentifier = objectGraph.props.asString("macauStorefrontIdentifier"); if (typeof storefrontIdentifier === "string" && typeof macauStoreFrontIdentifier === "string" && storefrontIdentifier.indexOf(macauStoreFrontIdentifier) !== -1) { return "zh-ma"; } else { return "zh-hk"; } } default: return language; } } nullString() { return "* null *"; } // endregion // region API /** * Loads the localizations from the JetPack. */ load(objectGraph) { // Sanity check if (objectGraph.bag.language === undefined || objectGraph.bag.language === null) { throw new Error("Bag language is not available. Unable to load localizations."); } const locale = this.normalizedLocale(objectGraph, objectGraph.bag.language, objectGraph.client.storefrontIdentifier); const locName = `local/${locale}`; // Load localizations if needed if (!this.isLocFileLoaded(locName)) { const localizations = objectGraph.props.asDictionary(`localizations.${locale}`); if (localizations !== undefined && localizations !== null) { this.applyLocalizations(locName, localizations, locale); } else { // Fallback to english const fallbackLocalizations = objectGraph.props.asDictionary(`localizations.en-us`); if (fallbackLocalizations !== undefined && fallbackLocalizations !== null) { this.applyLocalizations(locName, fallbackLocalizations, locale); } } } } /** * Returns the localized name for the provided device type. * The company policy seems to be to always use the branded, * non-localized text, but this is just-in-case. * @returns {string} The display name for the device. */ deviceDisplayName(objectGraph) { if (objectGraph.client.isVision && isSome(objectGraph.host.deviceMarketingFamilyName)) { return objectGraph.host.deviceMarketingFamilyName; } if (objectGraph.host.deviceLocalizedModel) { return objectGraph.host.deviceLocalizedModel; } // TODO: Jet: Remove brand loc fallbacks switch (objectGraph.client.deviceType) { case "phone": const localizedPhoneName = this.string("IPHONE_BRAND_NAME"); if (localizedPhoneName === "IPHONE_BRAND_NAME") { return "iPhone"; } return localizedPhoneName; case "pad": const localizedPadName = this.string("IPAD_BRAND_NAME"); if (localizedPadName === "IPAD_BRAND_NAME") { return "iPad"; } return localizedPadName; case "tv": const localizedTvName = this.string("APPLE_TV_BRAND_NAME"); if (localizedTvName === "APPLE_TV_BRAND_NAME") { return "Apple\u00a0TV"; } return localizedTvName; case "watch": const localizedWatchName = this.string("APPLE_WATCH_BRAND_NAME"); if (localizedWatchName === "APPLE_WATCH_BRAND_NAME") { return "Apple\u00a0Watch"; } return localizedWatchName; case "mac": const localizedMacName = this.string("MAC_BRAND_NAME"); if (localizedMacName === "MAC_BRAND_NAME") { return "Mac"; } return localizedMacName; default: return null; } } } // region Properties /** * Localization wrapper metatype to use with object graph. */ LocalizationWrapper.type = makeMetatype("app-store:loc-wrapper"); //# sourceMappingURL=localization.js.map