Skip to content

Commit 6096041

Browse files
committed
fix: crash of tsc when dealing with a union type too big for strings
1 parent f00bf9f commit 6096041

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+480
-398
lines changed

_locales/en/messages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
"blockUnblockNameMultiple": "Are you sure you want to unblock <b>{name}</b> and <b>{count} others</b>?",
158158
"blockUnblockNameTwo": "Are you sure you want to unblock <b>{name}</b> and 1 other?",
159159
"blockUnblockedUser": "Unblocked {name}",
160-
"blockedContactsmanageDescription": "View and manage blocked contacts.",
160+
"blockedContactsManageDescription": "View and manage blocked contacts.",
161161
"call": "Call",
162162
"callsCalledYou": "{name} called you",
163163
"callsCannotStart": "You cannot start a new call. Finish your current call first.",

tools/localization/localeTypes.py

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
#!/bin/python3
22
import re
3-
from typing import List, Tuple
43

54
OUTPUT_FILE = "./ts/localization/locales.ts"
65

76

7+
# Define the mapping of special fields to their types and corresponding "WithX"
8+
with_map = {
9+
("name", "string"): "WithName",
10+
("group_name", "string"): "WithGroupName",
11+
("community_name", "string"): "WithCommunityName",
12+
("other_name", "string"): "WithOtherName",
13+
("author", "string"): "WithAuthor",
14+
("emoji", "string"): "WithEmoji",
15+
("emoji_name", "string"): "WithEmojiName",
16+
("admin_name", "string"): "WithAdminName",
17+
("time", "string"): "WithTime",
18+
("time_large", "string"): "WithTimeLarge",
19+
("time_small", "string"): "WithTimeSmall",
20+
("disappearing_messages_type", "string"): "WithDisappearingMessagesType",
21+
("conversation_name", "string"): "WithConversationName",
22+
("file_type", "string"): "WithFileType",
23+
("date", "string"): "WithDate",
24+
("date_time", "string"): "WithDateTime",
25+
("message_snippet", "string"): "WithMessageSnippet",
26+
("query", "string"): "WithQuery",
27+
("version", "string"): "WithVersion",
28+
("information", "string"): "WithInformation",
29+
("device", "string"): "WithDevice",
30+
("percent_loader", "string"): "WithPercentLoader",
31+
("message_count", "string"): "WithMessageCount",
32+
("conversation_count", "string"): "WithConversationCount",
33+
("found_count", "number"): "WithFoundCount",
34+
("hash", "string"): "WithHash",
35+
("url", "string"): "WithUrl",
36+
("account_id", "string"): "WithAccountId",
37+
("count", "number"): "WithCount",
38+
("service_node_id", "string"): "WithServiceNodeId",
39+
("limit", "string"): "WithLimit",
40+
("relative_time", "string"): "WithRelativeTime",
41+
("icon", "string"): "WithIcon",
42+
("storevariant", "string"): "WithStoreVariant",
43+
("min", "string"): "WithMin",
44+
("max", "string"): "WithMax",
45+
}
46+
47+
848
def wrapValue(value):
949
"""
1050
Wraps the given value in single quotes if it contains any characters other than letters, digits, or underscores.
@@ -62,14 +102,26 @@ def extract_vars(text):
62102
return vars
63103

64104

105+
106+
def vars_to_record_ts(vars):
107+
arr = []
108+
for var in vars:
109+
to_append = [var, 'number' if var == 'count' or var == 'found_count' else 'string']
110+
if to_append not in arr:
111+
arr.append(to_append)
112+
113+
return arr
114+
115+
116+
117+
65118
def vars_to_record(vars):
66119
arr = []
67120
for var in vars:
68121
to_append = '' + var + ': ' + ('"number"' if var == 'count' or var == 'found_count' else '"string"')
69122
if to_append not in arr:
70123
arr.append(to_append)
71124

72-
# print(arr)
73125
if not arr:
74126
return ''
75127
return "{" + ', '.join(arr) + "}"
@@ -90,10 +142,15 @@ def generate_type_object(locales):
90142
Returns:
91143
str: A string representation of the JavaScript object.
92144
"""
93-
js_object = "{\n"
94-
js_plural_object_container = "{\n"
145+
js_object_no_args = "{\n"
146+
js_object_with_args = "{\n"
147+
js_plural_object_container_with_args = "{\n"
95148
plural_pattern = r"(zero|one|two|few|many|other)\s*\[([^\]]+)\]"
96149

150+
tokens_simple_no_args = []
151+
tokens_simple_with_args = {}
152+
tokens_plurals_with_args = {}
153+
97154
for key, value_en in locales['en'].items():
98155
if value_en.startswith("{count, plural, "):
99156
extracted_vars_en = extract_vars(value_en)
@@ -119,7 +176,6 @@ def generate_type_object(locales):
119176

120177

121178
all_locales_strings = []
122-
as_record_type_en = vars_to_record(extracted_vars)
123179

124180
for token, localized_string in plurals_with_token:
125181
if localized_string:
@@ -138,11 +194,11 @@ def generate_type_object(locales):
138194
js_plural_object += "\n },"
139195

140196
all_locales_plurals.append(js_plural_object)
141-
js_plural_object_container += f' {wrapValue(key)}: {{\n{"\n".join(all_locales_plurals)}\n args: {args_to_type(as_record_type_en)}\n }},\n'
197+
js_plural_object_container_with_args += f' {wrapValue(key)}: {{\n{"\n".join(all_locales_plurals)}\n }},\n'
198+
tokens_plurals_with_args[key] = vars_to_record_ts(extracted_vars)
142199

143200
else:
144201
extracted_vars_en = extract_vars(value_en)
145-
as_record_type_en = vars_to_record(extracted_vars_en)
146202
other_locales_replaced_values = [[locale, data.get(key, "")] for locale, data in locales.items()]
147203

148204
all_locales_strings = []
@@ -152,12 +208,28 @@ def generate_type_object(locales):
152208
else:
153209
all_locales_strings.append(f'{wrapValue(locale.replace("_","-"))}: "{escape_str(value_en)}"')
154210

155-
# print('key',key, " other_locales_replaced_values:", other_locales_replaced_values)
156-
js_object += f' {wrapValue(key)}: {{\n {",\n ".join(all_locales_strings)},\n args: {args_to_type(as_record_type_en)}\n }},\n'
211+
if extracted_vars_en:
212+
js_object_with_args += f' {wrapValue(key)}: {{\n {",\n ".join(all_locales_strings)},\n }},\n'
213+
tokens_simple_with_args[key] = vars_to_record_ts(extracted_vars_en)
214+
else:
215+
js_object_no_args += f' {wrapValue(key)}: {{\n {",\n ".join(all_locales_strings)},\n }},\n'
216+
tokens_simple_no_args.append(key)
157217

158-
js_object += "}"
159-
js_plural_object_container += "}"
160-
return js_object,js_plural_object_container
218+
tokens_simple_no_args_str = "\n '" + "' |\n '".join(tokens_simple_no_args) + "'"
219+
220+
js_object_no_args += "}"
221+
js_object_with_args += "}"
222+
js_plural_object_container_with_args += "}"
223+
224+
dicts = {
225+
"simple_no_args": js_object_no_args,
226+
"simple_with_args": js_object_with_args,
227+
"plurals_with_args": js_plural_object_container_with_args,
228+
"tokens_simple_no_args_str": tokens_simple_no_args_str,
229+
"tokens_simple_with_args": tokens_simple_with_args,
230+
"tokens_plural_with_args": tokens_plurals_with_args,
231+
}
232+
return dicts
161233

162234

163235
DISCLAIMER = """
@@ -190,6 +262,40 @@ def generateLocalesType(locale, data):
190262
return f"Locales generated at: {OUTPUT_FILE}"
191263

192264

265+
def format_tokens_with_named_args(token_args_dict):
266+
result = []
267+
268+
for token, args in token_args_dict.items():
269+
extras = []
270+
with_types = []
271+
272+
for arg_name, arg_type in args:
273+
key = (arg_name, arg_type)
274+
if key in with_map:
275+
with_types.append(with_map[key])
276+
else:
277+
extras.append(f"{arg_name}: {arg_type}")
278+
279+
# Join parts
280+
joined = " & ".join(with_types)
281+
if extras:
282+
extras_str = "{ " + ", ".join(extras) + " }"
283+
joined = f"{joined} & {extras_str}" if joined else extras_str
284+
285+
result.append(f" {token}: {joined}")
286+
287+
return "{\n" + ",\n".join(result) +"\n}"
288+
289+
290+
def generate_with_types(with_map):
291+
lines = []
292+
for (arg_name, arg_type), type_name in with_map.items():
293+
lines.append(f"type {type_name} = {{{arg_name}: {arg_type}}};")
294+
return "\n".join(lines)
295+
296+
297+
298+
193299
def generateLocalesMergedType(locales):
194300
"""
195301
Generate the locales type and write it to a file.
@@ -204,17 +310,45 @@ def generateLocalesMergedType(locales):
204310
f"{DISCLAIMER}"
205311
)
206312

313+
ts_file.write("import type { CrowdinLocale } from './constants';\n")
314+
207315
dicts = generate_type_object(locales)
208316

209-
dictVar = "simpleDictionary"
210-
pluralDictVar = "pluralsDictionary"
317+
tokens_simple_with_args = dicts['tokens_simple_with_args']
318+
tokens_plural_with_args = dicts['tokens_plural_with_args']
319+
320+
tokens_union_simple_args = format_tokens_with_named_args(tokens_simple_with_args)
321+
tokens_union_plural_args = format_tokens_with_named_args(tokens_plural_with_args)
211322

212323

213324
ts_file.write(f"""
214-
export const {dictVar} = {dicts[0]} as const;
325+
{generate_with_types(with_map)}
326+
327+
export type TokenSimpleNoArgs = {dicts['tokens_simple_no_args_str']};
328+
329+
export type TokensSimpleAndArgs = {tokens_union_simple_args};
330+
331+
export type TokensPluralAndArgs = {tokens_union_plural_args};
332+
333+
export type TokenSimpleWithArgs = {"\n '" + "' |\n '".join(list(tokens_simple_with_args.keys())) + "'"}
334+
335+
export type TokenPluralWithArgs = {"\n '" + "' |\n '".join(list(tokens_plural_with_args.keys())) + "'"}
336+
337+
export const simpleDictionaryNoArgs: Record<
338+
TokenSimpleNoArgs,
339+
Record<CrowdinLocale, string>
340+
> = {dicts['simple_no_args']} as const;
341+
342+
343+
export const simpleDictionaryWithArgs: Record<
344+
TokenSimpleWithArgs,
345+
Record<CrowdinLocale, string>
346+
> = {dicts['simple_with_args']} as const;
347+
348+
export const pluralsDictionaryWithArgs = {dicts['plurals_with_args']} as const;
215349
216-
export const {pluralDictVar} = {dicts[1]} as const;
217350
""")
218351

352+
219353
return f"Locales generated at: {OUTPUT_FILE}"
220354

ts/components/EmptyMessageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const EmptyMessageView = () => {
9090
<Spacer2XL />
9191
<StyledHeading>{tr('onboardingAccountCreated')}</StyledHeading>
9292
<StyledSessionWelcome color={'var(--renderer-span-primary-color)'}>
93-
<Localizer token="onboardingBubbleWelcomeToSession" args={{ emoji: '👋' }} />
93+
<Localizer token="onboardingBubbleWelcomeToSession" emoji={'👋'} />
9494
</StyledSessionWelcome>
9595
</>
9696
) : (

ts/components/basic/Localizer.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { SessionHtmlRenderer } from './SessionHTMLRenderer';
33
import {
44
formatMessageWithArgs,
55
GetMessageArgs,
6-
isArgsFromTokenWithIcon,
76
MergedLocalizerTokens,
87
sanitizeArgs,
98
getRawMessage,
10-
type LocalizerComponentProps,
9+
messageArgsToArgsOnly,
10+
type ArgsFromToken,
11+
type LocalizerHtmlTag,
1112
} from '../../localization/localeTools';
1213
import { getCrowdinLocale } from '../../util/i18n/shared';
13-
import { LUCIDE_INLINE_ICONS, type LucideInlineIconKeys } from '../icon/lucide';
1414

1515
/** An array of supported html tags to render if found in a string */
1616
export const supportedFormattingTags = ['b', 'i', 'u', 's', 'br', 'span'];
@@ -35,7 +35,8 @@ const StyledHtmlRenderer = styled.span`
3535
}
3636
`;
3737

38-
export type LocalizerProps = LocalizerComponentProps<MergedLocalizerTokens, LucideInlineIconKeys>;
38+
export type WithAsTag = { asTag?: LocalizerHtmlTag };
39+
export type WithClassName = { className?: string };
3940

4041
/**
4142
* Retrieve a localized message string, substituting dynamic parts where necessary and formatting it as HTML if necessary.
@@ -47,19 +48,11 @@ export type LocalizerProps = LocalizerComponentProps<MergedLocalizerTokens, Luci
4748
* @returns The localized message string with substitutions and formatting applied.
4849
*/
4950
export const Localizer = <T extends MergedLocalizerTokens>(
50-
props: LocalizerComponentProps<T, LucideInlineIconKeys>
51+
props: GetMessageArgs<T> & WithAsTag & WithClassName
5152
) => {
52-
const args = 'args' in props ? props.args : undefined;
53+
const args = messageArgsToArgsOnly(props);
5354

54-
let rawString: string = getRawMessage<T>(
55-
getCrowdinLocale(),
56-
...([props.token, args] as GetMessageArgs<T>)
57-
);
58-
59-
// NOTE If the string contains an icon we want to replace it with the relevant html from LUCIDE_ICONS before we sanitize the args
60-
if (isArgsFromTokenWithIcon<MergedLocalizerTokens, LucideInlineIconKeys>(props.args)) {
61-
rawString = rawString.replaceAll(/\{icon}/g, LUCIDE_INLINE_ICONS[props.args.icon]);
62-
}
55+
let rawString: string = getRawMessage<T>(getCrowdinLocale(), props);
6356

6457
const containsFormattingTags = createSupportedFormattingTagsRegex().test(rawString);
6558
const cleanArgs = args && containsFormattingTags ? sanitizeArgs(args) : args;
@@ -69,7 +62,7 @@ export const Localizer = <T extends MergedLocalizerTokens>(
6962
rawString = rawString.replaceAll(/\{icon}/g, `<span role='img'>{icon}</span>`);
7063
}
7164

72-
const i18nString = formatMessageWithArgs(rawString, cleanArgs as GetMessageArgs<T>[1]);
65+
const i18nString = formatMessageWithArgs(rawString, cleanArgs as ArgsFromToken<T>);
7366

7467
return containsFormattingTags || containsIcons ? (
7568
/** If the string contains a relevant formatting tag, render it as HTML */

0 commit comments

Comments
 (0)