-
Notifications
You must be signed in to change notification settings - Fork 351
refactor(i18n): migrate i18n from Flow to TypeScript #4776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bonchevskyi
wants to merge
1
commit into
box:master
Choose a base branch
from
bonchevskyi:refactor/flow-to-ts-i18n
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| /* | ||
| * Utility class for the FormattedCompMessage component. | ||
| */ | ||
| import * as React from 'react'; | ||
| import MessageAccumulator from 'message-accumulator'; | ||
| import Node from 'ilib-tree-node'; | ||
|
|
||
| import { JSTYPE_BOOLEAN, JSTYPE_NUMBER, JSTYPE_OBJECT, JSTYPE_STRING } from './constants'; | ||
|
|
||
| interface CompositionNode { | ||
| children: CompositionNode[]; | ||
| extra?: React.ReactElement<{ | ||
| children?: React.ReactNode; | ||
| temp?: React.ReactNode | React.ReactNode[]; | ||
| }>; | ||
| value?: React.ReactNode; | ||
| } | ||
|
|
||
| interface MessageAccumulatorInstance { | ||
| addParam: (element: React.ReactElement) => void; | ||
| addText: (text: string) => void; | ||
| getMinimalString: () => string; | ||
| getPrefix: () => CompositionNode[]; | ||
| getSuffix: () => CompositionNode[]; | ||
| pop: () => void; | ||
| push: (element: React.ReactElement) => void; | ||
| } | ||
|
|
||
| type ComposableElement = React.ReactNode | React.ElementType; | ||
|
|
||
| /** | ||
| * @class Compose a tree of React elements into a single string. | ||
| * | ||
| * @param {React.Element} element the element to compose | ||
| */ | ||
| class Composition { | ||
| element: ComposableElement; | ||
|
|
||
| isComposed: boolean; | ||
|
|
||
| ma: MessageAccumulatorInstance; | ||
|
|
||
| keyIndex: number; | ||
|
|
||
| constructor(element?: ComposableElement) { | ||
| this.element = element; | ||
| this.isComposed = false; | ||
|
|
||
| this.ma = new MessageAccumulator(); | ||
| this.keyIndex = 0; | ||
| } | ||
|
|
||
| recompose(element: ComposableElement): void { | ||
| switch (typeof element) { | ||
| case JSTYPE_OBJECT: | ||
| if (Array.isArray(element)) { | ||
| element.forEach(subelement => this.recompose(subelement)); | ||
| } else if (element) { | ||
| const reactElement = element as React.ReactElement<{ children?: React.ReactNode }>; | ||
| const elementType = reactElement.type; | ||
| const elementName = | ||
| typeof elementType === 'string' ? undefined : (elementType as { name?: string }).name; | ||
| if (elementType === 'Param' || elementName === 'Param') { | ||
| this.ma.addParam(reactElement); | ||
| } else { | ||
| this.ma.push(reactElement); | ||
| React.Children.forEach(reactElement.props.children, child => this.recompose(child)); | ||
| this.ma.pop(); | ||
| } | ||
| } | ||
| break; | ||
|
|
||
| case JSTYPE_NUMBER: | ||
| case JSTYPE_BOOLEAN: | ||
| this.ma.addText(String(element)); | ||
| break; | ||
|
|
||
| case JSTYPE_STRING: | ||
| this.ma.addText(element as string); | ||
| break; | ||
|
|
||
| default: | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Compose a tree of react elements to a string that can be translated. | ||
| * | ||
| * @return {string} a string representing the tree of react elements | ||
| */ | ||
| compose(): string { | ||
| if (!this.isComposed) { | ||
| this.recompose(this.element); | ||
| } | ||
| this.isComposed = true; | ||
| return this.ma.getMinimalString(); | ||
| } | ||
|
|
||
| /** | ||
| * @private | ||
| */ | ||
| nextKey(): string { | ||
| const result = `key${this.keyIndex}`; | ||
| this.keyIndex += 1; | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * @private | ||
| */ | ||
| mapToReactElements(node?: CompositionNode): React.ReactNode { | ||
| if (!node) return ''; | ||
|
|
||
| let children: React.ReactNode | React.ReactNode[] = node.children.map(child => this.mapToReactElements(child)); | ||
|
|
||
| const el = node.extra; | ||
| if (Array.isArray(children) && children.length === 0 && el?.props) { | ||
| const { temp } = el.props; | ||
| children = temp; | ||
| } | ||
|
|
||
| const childrenWithLength = children as React.ReactNode[] | string; | ||
| if (childrenWithLength?.length === 1 && typeof childrenWithLength[0] === 'string') { | ||
| children = childrenWithLength[0]; | ||
| } | ||
|
|
||
| const normalizedChildren = children as React.ReactNode[] | string; | ||
| if (el) { | ||
| return normalizedChildren?.length | ||
| ? React.cloneElement(el, { key: el.key || this.nextKey() }, children) | ||
| : React.cloneElement(el, { key: el.key || this.nextKey() }); | ||
| } | ||
|
|
||
| if (normalizedChildren.length) { | ||
| return normalizedChildren.length > 1 ? children : normalizedChildren[0]; | ||
| } | ||
|
|
||
| return node.value || ''; | ||
| } | ||
|
|
||
| /** | ||
| * Convert a composed string back into an array of React elements. The elements are clones of | ||
| * the same ones that this composition was created with, so that they have the same type and | ||
| * props and such as the originals. The elements may be re-ordered from the original, however, | ||
| * if the grammar of the target language requires moving around text, HTML tags, or | ||
| * subcomponents. | ||
| * | ||
| * @param {string} string the string to decompose into a tree of React elements. | ||
| * @return {React.Element} a react element | ||
| */ | ||
| decompose(string: string): React.ReactNode { | ||
| if (!this.isComposed) { | ||
| // need to create the mapping first from names to react elements | ||
| this.compose(); | ||
| } | ||
|
|
||
| const translation = MessageAccumulator.create(string, this.ma); | ||
| const nodeArray = [ | ||
| new Node({ | ||
| type: 'root', | ||
| use: 'start', | ||
| }), | ||
| ] | ||
| .concat(this.ma.getPrefix()) | ||
| .concat(translation.root.toArray().slice(1, -1)) | ||
| .concat(this.ma.getSuffix()) | ||
| .concat([ | ||
| new Node({ | ||
| type: 'root', | ||
| use: 'end', | ||
| }), | ||
| ]); | ||
| // convert to a tree again | ||
| return this.mapToReactElements(Node.fromArray(nodeArray)); | ||
| } | ||
| } | ||
|
|
||
| export default Composition; |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| // @deprecated, use FormattedMessage from react-intl v6 instead. | ||
| import * as React from 'react'; | ||
| import { injectIntl, IntlShape } from 'react-intl'; | ||
| import isNaN from 'lodash/isNaN'; | ||
|
|
||
| import isDevEnvironment from '../../utils/env'; | ||
| import { CATEGORY_ZERO, CATEGORY_ONE, CATEGORY_TWO, CATEGORY_FEW, CATEGORY_MANY, CATEGORY_OTHER } from './constants'; | ||
| import Composition from './Composition'; | ||
| import type { PluralProps } from './Plural'; | ||
|
|
||
| export interface FormattedCompMessageProps extends React.HTMLAttributes<HTMLElement> { | ||
| /** | ||
| * The text to translate. This may be a string or JSX. The defaultMessage prop may be | ||
| * given or the component may have children, but not both. | ||
| */ | ||
| children?: React.ReactNode; | ||
| /** | ||
| * Specify the pivot count to choose which plural form to use. | ||
| * When specified, this FormattedCompMessage component will choose one of the | ||
| * Plural elements in its children according to the value of this count | ||
| * and the linguistic rules of the locale which determine which numbers | ||
| * belong to which plural class. | ||
| */ | ||
| count?: number; | ||
| /** | ||
| * The text to translate. This may be a string or JSX. This prop may be | ||
| * given or the component may have children, but not both. | ||
| */ | ||
| defaultMessage?: React.ElementType | string; | ||
| /** | ||
| * A description to send to the translators to explain the context of | ||
| * this string. | ||
| */ | ||
| description: string; | ||
| /** The unique id of this string. */ | ||
| id: string; | ||
| /** | ||
| * The intl provider. This is injected into this component | ||
| * via the injectIntl function from react-intl. | ||
| */ | ||
| intl: IntlShape; | ||
| /** | ||
| * Specify the name of the HTML tag you would like to use to wrap the | ||
| * translations. | ||
| */ | ||
| tagName: string; | ||
| } | ||
|
|
||
| type FormattedCompMessageState = { | ||
| composition: Composition; | ||
| source: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Replace the text inside of this component with a translation. This | ||
| * component is built on top of react-intl, so it works along with the | ||
| * regular react-intl components and objects you are used to, and it gets | ||
| * its translations from react intl as well. The FormattedCompMessage component can | ||
| * be used wherever it is valid to put JSX text. In regular Javascript | ||
| * code, you should continue to use the intl.formatMessage() call and | ||
| * extract your strings into a message.js file. | ||
| */ | ||
| class FormattedCompMessage extends React.Component<FormattedCompMessageProps, FormattedCompMessageState> { | ||
| static readonly defaultProps = { | ||
| tagName: 'span', | ||
| }; | ||
|
|
||
| constructor(props: FormattedCompMessageProps) { | ||
| super(props); | ||
|
|
||
| /* eslint-disable no-console */ | ||
| console.warn( | ||
| "box-ui-elements: the FormattedCompMessage component is deprecated! Use react-intl's FormattedMessage instead.", | ||
| ); | ||
| /* eslint-enable no-console */ | ||
|
|
||
| // these parameters echo the ones in react-intl's FormattedMessage | ||
| // component, plus a few extra | ||
| const { | ||
| defaultMessage, // The English string + HTML + components that you want translated | ||
| count, // the pivot count to choose a plural form | ||
| children, // the components within the body | ||
| } = this.props; | ||
|
|
||
| const sourceElements = defaultMessage || children; | ||
|
|
||
| if (sourceElements) { | ||
| const composition = new Composition(sourceElements); | ||
| let source = ''; | ||
|
|
||
| if (!isNaN(Number(count))) { | ||
| if (children) { | ||
| source = this.composePluralString(children); | ||
| } else if (isDevEnvironment()) { | ||
| throw new Error('Cannot use count prop on a FormattedCompMessage component that has no children.'); | ||
| } | ||
| } else { | ||
| source = composition.compose(); | ||
| } | ||
|
|
||
| this.state = { | ||
| source, | ||
| composition, | ||
| }; | ||
| } | ||
| } | ||
|
bonchevskyi marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Search for any Plural elements in the children, and | ||
| * then construct the English source string in the correct | ||
| * format for react-intl to use for pluralization | ||
| * @param {React.Element} children the children of this node | ||
| * @return {string} the composed plural string | ||
| */ | ||
| composePluralString(children: React.ReactNode): string { | ||
| const categories: Partial<Record<PluralProps['category'], string>> = {}; | ||
| React.Children.forEach(children, child => { | ||
| if (React.isValidElement<PluralProps>(child)) { | ||
| const childType = child.type as React.ElementType & { name?: string }; | ||
| if (childType.name !== 'Plural') { | ||
| return; | ||
| } | ||
|
|
||
| const childComposition = new Composition(child.props.children); | ||
| categories[child.props.category] = childComposition.compose(); | ||
| } | ||
| }); | ||
| if (!categories.one || !categories.other) { | ||
| if (isDevEnvironment()) { | ||
| throw new Error( | ||
| 'Cannot use count prop on a FormattedCompMessage component without giving both a "one" and "other" Plural component in the children.', | ||
| ); | ||
| } | ||
| } | ||
| // add these to the string in a particular order so that | ||
| // we always end up with the same string regardless of | ||
| // the order that the Plural elements were specified in | ||
| // the source code | ||
| const categoriesString = [ | ||
| CATEGORY_ZERO, | ||
| CATEGORY_ONE, | ||
| CATEGORY_TWO, | ||
| CATEGORY_FEW, | ||
| CATEGORY_MANY, | ||
| CATEGORY_OTHER, | ||
| ] | ||
| .map(category => (categories[category] ? ` ${category} {${categories[category]}}` : '')) | ||
| .join(''); | ||
|
bonchevskyi marked this conversation as resolved.
|
||
|
|
||
| // see the intl-messageformat project for an explanation of this syntax | ||
| return `{count, plural,${categoriesString}}`; | ||
| } | ||
|
|
||
| render() { | ||
| const { count, tagName, intl, description, id, ...rest } = this.props; | ||
| delete rest.defaultMessage; | ||
| const { composition, source } = this.state; | ||
| const values: Record<string, number> = {}; | ||
| if (typeof count === 'number') { | ||
| // make sure intl.formatMessage switches properly on the count | ||
| values.count = count; | ||
| } | ||
|
|
||
| // react-intl will do the correct plurals if necessary | ||
| const descriptor = { | ||
| id, | ||
| defaultMessage: source, | ||
| description, | ||
| } as const; | ||
| const translation = intl.formatMessage(descriptor, values); | ||
|
|
||
| // always wrap the translated string in a tag to contain everything | ||
| // and to give us a spot to record the id. The resource id is the | ||
| // the id in mojito for the string. Having this attr has these advantages: | ||
| // 1. When debugging i18n or translation problems, it is MUCH easier to find | ||
| // the exact string to fix in Mojito rather than guessing. It might be useful | ||
| // for general debugging as well to map from something you see in the UI to | ||
| // the actual code that implements it. | ||
| // 2. It can be used by an in-context linguistic review tool. The tool code | ||
| // can contact mojito and retrieve the English for any translation errors that | ||
| // the reviewer finds and submit translation tickets to Jira and/or fixed | ||
| // translations directly back to Mojito. | ||
| // 3. It can be used by the planned "text experiment framework" to identify | ||
| // whole strings in the UI that can be A/B tested in various languages without | ||
| // publishing new versions of the code. | ||
| return React.createElement( | ||
| tagName, | ||
| { | ||
| key: id, | ||
| 'x-resource-id': id, | ||
| ...rest, | ||
| }, | ||
| composition.decompose(translation), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export default injectIntl(FormattedCompMessage); | ||
File renamed without changes.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.