120
common/resources/client/i18n/trans.tsx
Executable file
120
common/resources/client/i18n/trans.tsx
Executable file
@@ -0,0 +1,120 @@
|
||||
import {cloneElement, Fragment, isValidElement, memo} from 'react';
|
||||
import {shallowEqual} from '../utils/shallow-equal';
|
||||
import {useSelectedLocale} from './selected-locale';
|
||||
import {handlePluralMessage} from './handle-plural-message';
|
||||
import {MessageDescriptor} from './message-descriptor';
|
||||
|
||||
function hasOwn(obj: any, key: string): boolean {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (Object.hasOwn !== undefined) {
|
||||
return Object.hasOwn(obj, key);
|
||||
}
|
||||
return Object.hasOwnProperty(key);
|
||||
}
|
||||
|
||||
export const Trans = memo((props: MessageDescriptor) => {
|
||||
const {message: initialMessage, values} = props;
|
||||
const {lines, localeCode} = useSelectedLocale();
|
||||
let translatedMessage: string | undefined;
|
||||
|
||||
if (hasOwn(lines, initialMessage)) {
|
||||
translatedMessage = lines?.[initialMessage];
|
||||
} else if (hasOwn(lines, initialMessage?.toLowerCase())) {
|
||||
translatedMessage = lines?.[initialMessage.toLowerCase()];
|
||||
} else {
|
||||
translatedMessage = initialMessage;
|
||||
}
|
||||
|
||||
if (!values || !translatedMessage) {
|
||||
return <Fragment>{translatedMessage}</Fragment>;
|
||||
}
|
||||
|
||||
translatedMessage = handlePluralMessage(localeCode, {
|
||||
message: translatedMessage,
|
||||
values,
|
||||
});
|
||||
|
||||
// placeholders that need to be replaced with react element, eg. <Icon/>
|
||||
const nodePlaceholders: string[] = [];
|
||||
// placeholders that need to be replaced with render fn, eg. <a>link text</a>
|
||||
const tagNames: string[] = [];
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
// value is react render function
|
||||
if (typeof value === 'function') {
|
||||
tagNames.push(key);
|
||||
// value is react element
|
||||
} else if (isValidElement(value)) {
|
||||
nodePlaceholders.push(key);
|
||||
// value is primitive, can do simple string replace
|
||||
} else if (value != undefined) {
|
||||
translatedMessage = translatedMessage?.replace(`:${key}`, `${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
// if we need to replace placeholder with react element or render fn, we will need to split the
|
||||
// string by these placeholders and replace static string values with matching react element value
|
||||
if (tagNames.length || nodePlaceholders.length) {
|
||||
// we'll build simple OR regex to split the string eg. (<[ab]>content</[ab]>)|({(?:icon|link)})
|
||||
const regexArray: string[] = [];
|
||||
if (tagNames.length) {
|
||||
const tagNameMatchers = tagNames.join('');
|
||||
regexArray.push(`(<[${tagNameMatchers}]>.+?<\\/[${tagNameMatchers}]>)`);
|
||||
}
|
||||
if (nodePlaceholders.length) {
|
||||
const nodePlaceholderMatchers = nodePlaceholders.join('|');
|
||||
regexArray.push(`(\:(?:${nodePlaceholderMatchers}))`);
|
||||
}
|
||||
|
||||
const regex = new RegExp(regexArray.join('|'), 'gm');
|
||||
const parts = translatedMessage.split(regex);
|
||||
|
||||
// get rid of any empty strings or undefined from split by regex
|
||||
const compiledMessage = parts.filter(Boolean).map((part, i) => {
|
||||
// it's a tag name placeholder, eg. <a>content</a>
|
||||
if (part.startsWith('<') && part.endsWith('>')) {
|
||||
// grab tag content
|
||||
const matches = part.match(/<([a-z]+)>(.+?)<\/([a-z]+)>/);
|
||||
if (matches) {
|
||||
const [, tagName, content] = matches;
|
||||
const renderFn = values?.[tagName];
|
||||
if (typeof renderFn === 'function') {
|
||||
// pass it to render fn from values
|
||||
const node = renderFn(content);
|
||||
// add a key to avoid react errors
|
||||
return cloneElement(node, {key: i});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// it's a regular placeholder with react element value, eg. {icon}
|
||||
if (part.startsWith(':')) {
|
||||
const key = part.replace(':', '');
|
||||
const node = values?.[key];
|
||||
if (isValidElement(node)) {
|
||||
return cloneElement(node, {key: i});
|
||||
}
|
||||
}
|
||||
|
||||
// it's a regular string
|
||||
return part;
|
||||
});
|
||||
return <Fragment>{compiledMessage}</Fragment>;
|
||||
}
|
||||
|
||||
return <Fragment>{translatedMessage}</Fragment>;
|
||||
}, areEqual);
|
||||
|
||||
export function areEqual<T extends MessageDescriptor = MessageDescriptor>(
|
||||
prevProps: T,
|
||||
nextProps: T,
|
||||
): boolean {
|
||||
const {values, ...otherProps} = prevProps;
|
||||
const {values: nextValues, ...nextOtherProps} = nextProps;
|
||||
return (
|
||||
shallowEqual(nextValues, values) &&
|
||||
shallowEqual(otherProps as any, nextOtherProps)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user