runtime/shared/substituteTokens.js (91 lines of code) (raw):

/** * (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. * * This file is shared between www and fbsource and www is the source of truth. * When you make change to this file on www, please make sure you test it on * fbsource and send a diff to update the files too so that the 2 versions are * kept in sync. * * Run the following command to sync the change from www to fbsource. * js1 upgrade www-shared -p fbt --local ~/www * * @format * @flow strict-local * @emails oncall+i18n_fbt_js */ // flowlint ambiguous-object-type:error import { PUNCT_CHAR_CLASS, applyPhonologicalRules, dedupeStops, } from 'IntlPunctuation'; import invariant from 'invariant'; /* * $FlowFixMe[method-unbinding] Use original method in case the token names contain * a 'hasOwnProperty' key too; or if userland code redefined that method. */ const {hasOwnProperty} = Object.prototype; // This pattern finds tokens inside a string: 'string with {token} inside'. // It also grabs any punctuation that may be present after the token, such as // brackets, fullstops and elipsis (for various locales too!) const parameterRegexp = new RegExp( '\\{([^}]+)\\}(' + PUNCT_CHAR_CLASS + '*)', 'g', ); type MaybeReactComponent = $Shape<{ type?: string, props?: {...}, _store?: { validated: boolean, ... }, ... }>; // Hack into React internals to avoid key warnings function markAsSafeForReact<T: MaybeReactComponent>(object: T): T { if (__DEV__) { // If this looks like a ReactElement, mark it as safe to silence any // key warnings. // I use a string key to avoid any possible private variable transforms. const storeKey = '_store'; const store = object[storeKey]; if ( object.type != null && object.type != '' && typeof object.props === 'object' && store != null && typeof store === 'object' && typeof store.validated === 'boolean' ) { store.validated = true; } } return object; } /** * Does the token substitution fbt() but without the string lookup. * Used for in-place substitutions in translation mode. */ function substituteTokens<Arg: mixed>( template: string, args: {[paramName: string]: Arg, ...}, ): string | Array<string | Arg> { if (args == null) { return template; } invariant( typeof args === 'object', 'The 2nd argument must be an object (not a string) for tx(%s, ...)', template, ); // Splice in the arguments while keeping rich object ones separate. const objectPieces = []; const argNames = []; const stringPieces = template .replace( parameterRegexp, (_match: string, parameter: string, punctuation: string): string => { if (__DEV__) { invariant( hasOwnProperty.call(args, parameter), 'Expected fbt parameter names (%s) to also contain `%s`', Object.keys(args) .map(paramName => `\`${paramName}\``) .join(', '), parameter, ); } // TODO(T106260833) Log error when we cannot resolve all fbt parameters using FbtHooks const argument = args[parameter]; if (argument != null && typeof argument === 'object') { objectPieces.push(argument); argNames.push(parameter); // End of Transmission Block sentinel marker return '\x17' + punctuation; } else if (argument === null) { return ''; } return String(argument) + dedupeStops(String(argument), punctuation); }, ) .split('\x17') .map(applyPhonologicalRules); if (stringPieces.length === 1) { return stringPieces[0]; } // Zip together the lists of pieces. // We skip adding empty strings from stringPieces since they were // injected from translation patterns that only contain tokens. See D20453562 const pieces = stringPieces[0] !== '' ? [stringPieces[0]] : []; for (let i = 0; i < objectPieces.length; i++) { pieces.push(markAsSafeForReact(objectPieces[i])); if (stringPieces[i + 1] !== '') { pieces.push(stringPieces[i + 1]); } } return pieces; } module.exports = substituteTokens;