Skip to content

Commit

Permalink
fix: strings do not load dict from json, but use built ts file
Browse files Browse the repository at this point in the history
  • Loading branch information
Bilb committed Jan 3, 2025
1 parent 66e80fc commit da39198
Show file tree
Hide file tree
Showing 28 changed files with 461 additions and 725 deletions.
3 changes: 1 addition & 2 deletions about_preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ const os = require('os');
const { setupI18n } = require('./ts/util/i18n/i18n');

const config = url.parse(window.location.toString(), true).query;
const { dictionary, crowdinLocale } = ipcRenderer.sendSync('locale-data');
const { crowdinLocale } = ipcRenderer.sendSync('locale-data');

window.theme = config.theme;
window.i18n = setupI18n({
crowdinLocale,
translationDictionary: dictionary,
});

window.getOSRelease = () =>
Expand Down
3 changes: 1 addition & 2 deletions password_preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ const url = require('url');
const { setupI18n } = require('./ts/util/i18n/i18n');

const config = url.parse(window.location.toString(), true).query;
const { dictionary, crowdinLocale } = ipcRenderer.sendSync('locale-data');
const { crowdinLocale } = ipcRenderer.sendSync('locale-data');

// If the app is locked we can't access the database to check the theme.
window.theme = 'classic-dark';
window.primaryColor = 'green';

window.i18n = setupI18n({
crowdinLocale,
translationDictionary: dictionary,
});

window.getEnvironment = () => config.environment;
Expand Down
4 changes: 2 additions & 2 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ const _ = require('lodash');

const { setupI18n } = require('./ts/util/i18n/i18n');

const { dictionary, crowdinLocale } = ipc.sendSync('locale-data');
const { crowdinLocale } = ipc.sendSync('locale-data');

const config = url.parse(window.location.toString(), true).query;
const configAny = config;

window.i18n = setupI18n({ crowdinLocale, translationDictionary: dictionary });
window.i18n = setupI18n({ crowdinLocale });

let title = config.name;
if (config.environment !== 'production') {
Expand Down
28 changes: 19 additions & 9 deletions tools/localization/localeTypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def generate_type_object(locales):
str: A string representation of the JavaScript object.
"""
js_object = "{\n"
js_plural_object_container = "{\n"
plural_pattern = r"(zero|one|two|few|many|other)\s*\[([^\]]+)\]"

for key, value_en in locales['en'].items():
Expand All @@ -113,7 +114,7 @@ def generate_type_object(locales):
for plural in plurals_other:
js_plural_object = ""

locale_key = plural[0] # 'lo', 'th', ....
locale_key = plural[0].replace("_","-") # 'lo', 'th', 'zh-CN', ....
plural_str = plural[1].replace('#', '{count}')

plurals_with_token = re.findall(plural_pattern, plural_str)
Expand All @@ -139,7 +140,7 @@ def generate_type_object(locales):
js_plural_object += "\n },"

all_locales_plurals.append(js_plural_object)
js_object += f" {wrapValue(key)}: {{\n{"\n".join(all_locales_plurals)}\n args: {f"{as_record_type_en} as const," if as_record_type_en else 'undefined,'}\n }},\n"
js_plural_object_container += f" {wrapValue(key)}: {{\n{"\n".join(all_locales_plurals)}\n args: {as_record_type_en if as_record_type_en else 'undefined,'}\n }},\n"

else:
replaced_en = replace_static_strings(value_en)
Expand All @@ -150,15 +151,16 @@ def generate_type_object(locales):
all_locales_strings = []
for locale, replaced_val in other_locales_replaced_values:
if replaced_val:
all_locales_strings.append(f"{locale}: \"{replaced_val.replace("\n", "\\n")}\"")
all_locales_strings.append(f"{wrapValue(locale.replace("_","-"))}: \"{replaced_val.replace("\n", "\\n")}\"")
else:
all_locales_strings.append(f"{locale}: \"{replaced_en.replace("\n", "\\n")}\"")
all_locales_strings.append(f"{wrapValue(locale.replace("_","-"))}: \"{replaced_en.replace("\n", "\\n")}\"")

# print('key',key, " other_locales_replaced_values:", other_locales_replaced_values)
js_object += f" {wrapValue(key)}: {{\n {",\n ".join(all_locales_strings)},\n args: {f"{as_record_type_en} as const," if as_record_type_en else 'undefined,'}\n }},\n"
js_object += f" {wrapValue(key)}: {{\n {",\n ".join(all_locales_strings)},\n args: {as_record_type_en if as_record_type_en else 'undefined,'}\n }},\n"

js_object += "}"
return js_object
js_plural_object_container += "}"
return js_object,js_plural_object_container


DISCLAIMER = """
Expand Down Expand Up @@ -205,9 +207,17 @@ def generateLocalesMergedType(locales):
f"{DISCLAIMER}"
)

ts_file.write(
f"export const dictionary = {generate_type_object(locales)};\n\nexport type Dictionary = typeof dictionary;\n"
)
dicts = generate_type_object(locales)

dictVar = "simpleDictionary"
pluralDictVar = "pluralsDictionary"


ts_file.write(f"""
export const {dictVar} = {dicts[0]} as const;
export const {pluralDictVar} = {dicts[1]} as const;
""")

return f"Locales generated at: {OUTPUT_FILE}"

82 changes: 12 additions & 70 deletions ts/components/basic/Localizer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import styled from 'styled-components';
import type {
ArgsRecord,
GetMessageArgs,
LocalizerComponentProps,
LocalizerDictionary,
LocalizerToken,
} from '../../types/localizer';
import type { LocalizerComponentProps } from '../../types/localizer';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
import {
GetMessageArgs,
MergedLocalizerTokens,
sanitizeArgs,
} from '../../localization/localeTools';
import { getCrowdinLocale } from '../../util/i18n/shared';

/** An array of supported html tags to render if found in a string */
export const supportedFormattingTags = ['b', 'i', 'u', 's', 'br', 'span'];
Expand All @@ -20,58 +20,6 @@ function createSupportedFormattingTagsRegex() {
);
}

/**
* 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}&amp;${identifier}`)
.replace(/</g, `${identifier}&lt;${identifier}`)
.replace(/>/g, `${identifier}&gt;${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}&amp;${identifier}`, 'g'), '&')
.replace(new RegExp(`${identifier}&lt;${identifier}`, 'g'), '<')
.replace(new RegExp(`${identifier}&gt;${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`
* > span {
color: var(--renderer-span-primary-color);
Expand All @@ -86,27 +34,21 @@ const StyledHtmlRenderer = styled.span`
* @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.
*
* @returns The localized message string with substitutions and formatting applied.
*
* @example
* ```tsx
* <Localizer token="about" />
* <Localizer token="about" as='h1' />
* <Localizer token="disappearingMessagesFollowSettingOn" args={{ time: 10, type: 'mode' }} />
* ```
*/
export const Localizer = <T extends LocalizerToken>(props: LocalizerComponentProps<T>) => {
export const Localizer = <T extends MergedLocalizerTokens>(props: LocalizerComponentProps<T>) => {
const args = 'args' in props ? props.args : undefined;

const rawString = window.i18n.getRawMessage<T, LocalizerDictionary[T]>(
const rawString = window.i18n.getRawMessage<T>(
getCrowdinLocale(),
...([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>
rawString,
cleanArgs as GetMessageArgs<T>[1]
);

return containsFormattingTags ? (
Expand Down
21 changes: 10 additions & 11 deletions ts/components/basic/StyledI18nSubText.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled from 'styled-components';
import { forwardRef } from 'react';
import { Localizer } from './Localizer';
import type { LocalizerComponentProps, LocalizerToken } from '../../types/localizer';
import type { LocalizerComponentPropsObject } from '../../types/localizer';

const StyledI18nSubTextContainer = styled('div')`
font-size: var(--font-size-md);
Expand All @@ -15,13 +15,12 @@ const StyledI18nSubTextContainer = styled('div')`
padding-inline: var(--margins-lg);
`;

export const StyledI18nSubText = forwardRef<
HTMLSpanElement,
LocalizerComponentProps<LocalizerToken>
>(({ className, ...props }) => {
return (
<StyledI18nSubTextContainer className={className}>
<Localizer {...props} />
</StyledI18nSubTextContainer>
);
});
export const StyledI18nSubText = forwardRef<HTMLSpanElement, LocalizerComponentPropsObject>(
({ className, ...props }) => {
return (
<StyledI18nSubTextContainer className={className}>
<Localizer {...props} />
</StyledI18nSubTextContainer>
);
}
);
5 changes: 3 additions & 2 deletions ts/components/conversation/TimerNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { ReleasedFeatures } from '../../util/releaseFeature';
import { Flex } from '../basic/Flex';
import { SpacerMD, TextWithChildren } from '../basic/Text';
import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage';
import { LocalizerComponentPropsObject } from '../../types/localizer';

// eslint-disable-next-line import/order
import { ConversationInteraction } from '../../interactions';
import { ConvoHub } from '../../session/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import type { LocalizerComponentProps, LocalizerToken } from '../../types/localizer';
import { Localizer } from '../basic/Localizer';
import { SessionButtonColor } from '../basic/SessionButton';
import { SessionIcon } from '../icon';
Expand All @@ -47,7 +48,7 @@ function useFollowSettingsButtonClick(
? window.i18n('disappearingMessagesTypeRead')
: window.i18n('disappearingMessagesTypeSent');

const i18nMessage: LocalizerComponentProps<LocalizerToken> = props.disabled
const i18nMessage: LocalizerComponentPropsObject = props.disabled
? {
token: 'disappearingMessagesFollowSettingOff',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Items = (props: Props): JSX.Element => {
/>
);
default:
return missingCaseError(type);
throw missingCaseError(type);
}
})}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { CallNotificationType, PropsForCallNotification } from '../../../../../state/ducks/types';

import { useSelectedNicknameOrProfileNameOrShortenedPubkey } from '../../../../../state/selectors/selectedConversation';
import type { LocalizerToken } from '../../../../../types/localizer';
import { SessionIconType } from '../../../../icon';
import { ExpirableReadableMessage } from '../ExpirableReadableMessage';
import { NotificationBubble } from './NotificationBubble';
import { Localizer } from '../../../../basic/Localizer';
import { MergedLocalizerTokens } from '../../../../../localization/localeTools';

type StyleType = Record<
CallNotificationType,
{ notificationTextKey: LocalizerToken; iconType: SessionIconType; iconColor: string }
{ notificationTextKey: MergedLocalizerTokens; iconType: SessionIconType; iconColor: string }
>;

const style = {
Expand Down
4 changes: 2 additions & 2 deletions ts/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ import {
getIsMessageSection,
} from '../../state/selectors/section';
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import type { LocalizerToken } from '../../types/localizer';
import { SessionButtonColor } from '../basic/SessionButton';
import { ItemWithDataTestId } from './items/MenuItemWithDataTestId';
import { useLibGroupDestroyed } from '../../state/selectors/userGroups';
import { NetworkTime } from '../../util/NetworkTime';
import { MergedLocalizerTokens } from '../../localization/localeTools';

/** Menu items standardized */

Expand Down Expand Up @@ -516,7 +516,7 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
isPrivate ? n !== 'mentions_only' : true
).map((n: ConversationNotificationSettingType) => {
// do this separately so typescript's compiler likes it
const keyToUse: LocalizerToken =
const keyToUse: MergedLocalizerTokens =
n === 'all' || !n
? 'notificationsAllMessages'
: n === 'disabled'
Expand Down
6 changes: 3 additions & 3 deletions ts/components/registration/components/BackButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { deleteDbLocally } from '../../../util/accountManager';
import { Flex } from '../../basic/Flex';
import { SessionButtonColor } from '../../basic/SessionButton';
import { SessionIconButton } from '../../icon';
import type { LocalizerComponentProps, LocalizerToken } from '../../../types/localizer';
import type { LocalizerComponentPropsObject } from '../../../types/localizer';

/** Min height should match the onboarding step with the largest height this prevents the loading spinner from jumping around while still keeping things centered */
const StyledBackButtonContainer = styled(Flex)`
Expand All @@ -38,7 +38,7 @@ export const BackButtonWithinContainer = ({
callback?: () => void;
onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitI18nMessageArgs: LocalizerComponentProps<LocalizerToken>;
quitI18nMessageArgs: LocalizerComponentPropsObject;
}) => {
return (
<StyledBackButtonContainer
Expand Down Expand Up @@ -70,7 +70,7 @@ export const BackButton = ({
callback?: () => void;
onQuitVisible?: () => void;
shouldQuitOnClick?: boolean;
quitI18nMessageArgs: LocalizerComponentProps<LocalizerToken>;
quitI18nMessageArgs: LocalizerComponentPropsObject;
}) => {
const step = useOnboardStep();
const restorationStep = useOnboardAccountRestorationStep();
Expand Down
4 changes: 2 additions & 2 deletions ts/interactions/conversationInteractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { Storage, setLastProfileUpdateTimestamp } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { ConversationInteractionStatus, ConversationInteractionType } from './types';
import { BlockedNumberController } from '../util';
import type { LocalizerComponentProps, LocalizerToken } from '../types/localizer';
import type { LocalizerComponentPropsObject } from '../types/localizer';
import { sendInviteResponseToGroup } from '../session/sending/group/GroupInviteResponse';
import { NetworkTime } from '../util/NetworkTime';
import { ClosedGroup } from '../session';
Expand Down Expand Up @@ -270,7 +270,7 @@ export const declineConversationWithConfirm = ({

const convoName = ConvoHub.use().get(conversationId)?.getNicknameOrRealUsernameOrPlaceholder();

const i18nMessage: LocalizerComponentProps<LocalizerToken> = isGroupV2
const i18nMessage: LocalizerComponentPropsObject = isGroupV2
? alsoBlock && originNameToBlock
? { token: 'blockDescription', args: { name: originNameToBlock } } // groupv2, and blocking by sender name
: { token: 'groupInviteDelete' } // groupv2, and no info about the sender, falling back to delete only
Expand Down
Loading

0 comments on commit da39198

Please sign in to comment.