runtime/shared/FbtTable.js (49 lines of code) (raw):
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* Provides types related to fbt table access and the algorithm for
* recursively accessing the table entries and returning the leaves
*
* @emails oncall+i18n_fbt_js
* @flow strict-local
* @format
*/
'use strict';
import type {FbtRuntimeInput, FbtTableArgs} from 'FbtHooks';
const invariant = require('invariant');
/**
* A leaf "pattern string" in our FbtInputTable that represents an Fbt UI text string.
*
* 1. the text can either be written in the build-in locale (en_US) or already translated
* 2. Special token name patterns may be used. E.g. "Hello {yourName}" and need to be interpolated
* to get a clear text.
*/
export type PatternString = string;
/**
* An optional pattern hash bundled in leaf - aka the Fbt Hash.
*/
export type PatternHash = string;
export type FbtSubstitution = {[token: string]: mixed, ...};
export type FbtTableKey = string | number;
const FbtTable = {
/**
* fbt.XXX calls return arguments in the form of
* [<INDEX>, <SUBSTITUTION>] to be processed by fbt._
*/
ARG: {
INDEX: 0,
SUBSTITUTION: 1,
},
/**
* Performs a depth-first search on our table, attempting to access
* each table entry. The first entry found is the one we want, as we
* set defaults after preferred indices. For example:
*
* @param @table - {
* // viewer gender
* '*': {
* // {num} plural
* '*': {
* // user-defined enum
* LIKE: '{num} people liked your update',
* COMMENT: '{num} people commented on your update',
* POST: '{num} people posted on a wall',
* },
* SINGULAR: {
* LIKE: '{num} person liked your update',
* // ...
* },
* DUAL: { ... }
* },
* FEMALE: {
* // {num} plural
* '*': { ... },
* SINGULAR: { ... },
* DUAL: { ... }
* },
* MALE: { ... }
* }
*
* Notice that LIKE and COMMENT here both have 'your' in them, whereas
* POST doesn't. The fallback ('*') translation for POST will be the same
* in both the male and female version, so that entry won't exist under
* table[FEMALE]['*'] or table[MALE]['*'].
*
* Similarly, PLURAL is a number variation that never appears in the table as it
* is the default/fallback.
*
* For example, if we have a female viewer, and a PLURAL number and a POST enum
* value, in the above example, we'll first attempt to get:
* table[FEMALE][PLURAL][POST]. undefined. Back Up, attempting to get
* table[FEMALE]['*'][POST]. undefined also. since it's the same as the '*'
* table['*'][PLURAL][POST]. ALSO undefined. Deduped to '*'
* table['*']['*'][POST]. There it is.
*
* @param args - fbt runtime arguments
* @param argsIndex - argument index we're currently visiting
*/
access(
table: FbtRuntimeInput,
args: FbtTableArgs,
argsIndex: number,
): ?PatternString | [PatternString, PatternHash] {
if (argsIndex >= args.length) {
// We've reached the end of our arguments at a valid entry, in which case
// table is now a string (leaf) or undefined (key doesn't exist)
invariant(
typeof table === 'string' || Array.isArray(table),
'Expected leaf, but got: %s',
JSON.stringify(table),
);
return table;
}
const arg = args[argsIndex];
const tableIndices = arg[FbtTable.ARG.INDEX];
if (tableIndices == null) {
return FbtTable.access(table, args, argsIndex + 1);
}
invariant(
typeof table !== 'string' && !Array.isArray(table),
'If tableIndex is non-null, we should have a table, but we got: %s',
typeof table,
);
// Is there a variation? Attempt table access in order of variation preference
for (let k = 0; k < tableIndices.length; ++k) {
const subTable = table[tableIndices[k]];
if (subTable == null) {
continue;
}
const pattern = FbtTable.access(subTable, args, argsIndex + 1);
if (pattern != null) {
return pattern;
}
}
return null;
},
};
module.exports = FbtTable;