You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			126 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			126 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			TypeScript
		
	
| import styled from 'styled-components';
 | |
| import type {
 | |
|   ArgsRecord,
 | |
|   GetMessageArgs,
 | |
|   I18nProps,
 | |
|   LocalizerDictionary,
 | |
|   LocalizerToken,
 | |
| } from '../../types/Localizer';
 | |
| import { useIsDarkTheme } from '../../state/selectors/theme';
 | |
| import { SessionHtmlRenderer } from './SessionHTMLRenderer';
 | |
| 
 | |
| /** An array of supported html tags to render if found in a string */
 | |
| export const supportedFormattingTags = ['b', 'i', 'u', 's', 'br', 'span'];
 | |
| /** NOTE: self-closing tags must also be listed in {@link supportedFormattingTags} */
 | |
| const supportedSelfClosingFormattingTags = ['br'];
 | |
| 
 | |
| function createSupportedFormattingTagsRegex() {
 | |
|   return new RegExp(
 | |
|     `<(?:${supportedFormattingTags.join('|')})>.*?</(?:${supportedFormattingTags.join('|')})>|<(?:${supportedSelfClosingFormattingTags.join('|')})\\/>`,
 | |
|     'g'
 | |
|   );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Replaces all html tag identifiers with their escaped equivalents
 | |
|  * @param str The string to sanitize
 | |
|  * @param identifier The identifier to use for the args. Use this if you want to de-sanitize the args later.
 | |
|  * @returns The sanitized string
 | |
|  */
 | |
| export function sanitizeHtmlTags(str: string, identifier: string = ''): string {
 | |
|   if (identifier && /[a-zA-Z0-9></\\\-\s]+/g.test(identifier)) {
 | |
|     throw new Error('Identifier is not valid');
 | |
|   }
 | |
| 
 | |
|   return str
 | |
|     .replace(/&/g, `${identifier}&${identifier}`)
 | |
|     .replace(/</g, `${identifier}<${identifier}`)
 | |
|     .replace(/>/g, `${identifier}>${identifier}`);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Replaces all sanitized html tags with their real equivalents
 | |
|  * @param str The string to de-sanitize
 | |
|  * @param identifier The identifier used when the args were sanitized
 | |
|  * @returns The de-sanitized string
 | |
|  */
 | |
| export function deSanitizeHtmlTags(str: string, identifier: string): string {
 | |
|   if (!identifier || /[a-zA-Z0-9></\\\-\s]+/g.test(identifier)) {
 | |
|     throw new Error('Identifier is not valid');
 | |
|   }
 | |
| 
 | |
|   return str
 | |
|     .replace(new RegExp(`${identifier}&${identifier}`, 'g'), '&')
 | |
|     .replace(new RegExp(`${identifier}<${identifier}`, 'g'), '<')
 | |
|     .replace(new RegExp(`${identifier}>${identifier}`, 'g'), '>');
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Sanitizes the args to be used in the i18n function
 | |
|  * @param args The args to sanitize
 | |
|  * @param identifier The identifier to use for the args. Use this if you want to de-sanitize the args later.
 | |
|  * @returns The sanitized args
 | |
|  */
 | |
| export function sanitizeArgs(
 | |
|   args: Record<string, string | number>,
 | |
|   identifier?: string
 | |
| ): Record<string, string | number> {
 | |
|   return Object.fromEntries(
 | |
|     Object.entries(args).map(([key, value]) => [
 | |
|       key,
 | |
|       typeof value === 'string' ? sanitizeHtmlTags(value, identifier) : value,
 | |
|     ])
 | |
|   );
 | |
| }
 | |
| 
 | |
| const StyledHtmlRenderer = styled.span<{ isDarkTheme: boolean }>`
 | |
|   * > span {
 | |
|     color: ${props => (props.isDarkTheme ? 'var(--primary-color)' : 'var(--text-primary-color)')};
 | |
|   }
 | |
| `;
 | |
| 
 | |
| /**
 | |
|  * Retrieve a localized message string, substituting dynamic parts where necessary and formatting it as HTML if necessary.
 | |
|  *
 | |
|  * @param props.token - The token identifying the message to retrieve and an optional record of substitution variables and their replacement values.
 | |
|  * @param props.args - An optional record of substitution variables and their replacement values. This is required if the string has dynamic parts.
 | |
|  * @param props.as - An optional HTML tag to render the component as. Defaults to a fragment, unless the string contains html tags. In that case, it will render as HTML in a div tag.
 | |
|  * @param props.startTagProps - An optional object of props to pass to the start tag.
 | |
|  * @param props.endTagProps - An optional object of props to pass to the end tag.
 | |
|  *
 | |
|  * @returns The localized message string with substitutions and formatting applied.
 | |
|  *
 | |
|  * @example
 | |
|  * ```tsx
 | |
|  * <I18n token="about" />
 | |
|  * <I18n token="about" as='h1' />
 | |
|  * <I18n token="disappearingMessagesFollowSettingOn" args={{ time: 10, type: 'mode' }} />
 | |
|  * ```
 | |
|  */
 | |
| export const I18n = <T extends LocalizerToken>(props: I18nProps<T>) => {
 | |
|   const isDarkMode = useIsDarkTheme();
 | |
| 
 | |
|   const args = 'args' in props ? props.args : undefined;
 | |
| 
 | |
|   const rawString = window.i18n.getRawMessage<T, LocalizerDictionary[T]>(
 | |
|     ...([props.token, args] as GetMessageArgs<T>)
 | |
|   );
 | |
| 
 | |
|   const containsFormattingTags = createSupportedFormattingTagsRegex().test(rawString);
 | |
|   const cleanArgs = args && containsFormattingTags ? sanitizeArgs(args) : args;
 | |
| 
 | |
|   const i18nString = window.i18n.formatMessageWithArgs(
 | |
|     rawString as LocalizerDictionary[T],
 | |
|     cleanArgs as ArgsRecord<T>
 | |
|   );
 | |
| 
 | |
|   return containsFormattingTags ? (
 | |
|     /** If the string contains a relevant formatting tag, render it as HTML */
 | |
|     <StyledHtmlRenderer isDarkTheme={isDarkMode}>
 | |
|       <SessionHtmlRenderer tag={props.asTag} html={i18nString} className={props.className} />
 | |
|     </StyledHtmlRenderer>
 | |
|   ) : (
 | |
|     i18nString
 | |
|   );
 | |
| };
 |