Skip to content

Commit

Permalink
fix: still plenty of errors, but simple window.i18n work
Browse files Browse the repository at this point in the history
  • Loading branch information
Bilb committed Jan 1, 2025
1 parent e225d58 commit 66e80fc
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 72 deletions.
1 change: 0 additions & 1 deletion _locales/th/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,6 @@
"searchContacts": "ค้นหาผู้ติดต่อ",
"searchConversation": "ค้นหาอะไรในการสนทนา",
"searchEnter": "เขียนที่ค้นหา",
"searchMatches": "{count, plural, other [{found_count} จาก # รายการ]}",
"searchMatchesNone": "ไม่พบข้อมูลเลย",
"searchMatchesNoneSpecific": "ไม่พบข้อมูลเกี่ยวกับ '{query}",
"searchMembers": "ค้นหาสมาชิก",
Expand Down
93 changes: 55 additions & 38 deletions tools/localization/localeTypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,11 @@ def extract_vars(text):
vars = re.findall(r'\{(.*?)\}', text)
return vars

def extract_plurals(text: str) -> List[Tuple[str, str]]:
pattern = r'(\b\w+\b)\s*(\[[^\]]+\])'

matches = re.findall(pattern, text)

return matches


def vars_to_record(vars):
arr = []
for var in vars:
to_append = '"' + var + '": ' + ('number' if var == 'count' else 'string')
to_append = '' + var + ': ' + ('"number"' if var == 'count' or var == 'found_count' else '"string"')
if to_append not in arr:
arr.append(to_append)

Expand Down Expand Up @@ -100,45 +93,69 @@ def generate_type_object(locales):
str: A string representation of the JavaScript object.
"""
js_object = "{\n"
plural_pattern = r"(zero|one|two|few|many|other)\s*\[([^\]]+)\]"

for key, value_en in locales['en'].items():
# print('value',value)
if value_en.startswith("{count, plural, "):
continue
# plurals = extract_plurals(value)
# print(plurals)
# js_plural_object = "{\n"

# for plural in plurals:
# plural_token = plural[0]
# plural_str = plural[1].replace('#', '{count}')
# extracted_vars = extract_vars(replace_static_strings(plural_str))
# if('count' not in extracted_vars):
# extracted_vars.append('count')
# print('extracted_vars',extracted_vars)

# as_record_type = vars_to_record(extracted_vars)
# js_plural_object += f" {wrapValue(plural_token)}: {as_record_type},\n"
# js_plural_object += " }"
# js_object += f" {wrapValue(key)}: {js_plural_object},\n"
extracted_vars_en = extract_vars(replaced_en)
plurals_other = [[locale, replace_static_strings(data.get(key, ""))] for locale, data in locales.items()]
en_plurals_with_token = re.findall(plural_pattern, value_en.replace('#', '{count}'))

if not en_plurals_with_token:
raise ValueError("invalid plural string")

all_locales_plurals = []

extracted_vars = extract_vars(replace_static_strings(en_plurals_with_token[0][1]))
if('count' not in extracted_vars):
extracted_vars.append('count')

for plural in plurals_other:
js_plural_object = ""

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

plurals_with_token = re.findall(plural_pattern, plural_str)


all_locales_strings = []
as_record_type_en = vars_to_record(extracted_vars)

for token, localized_string in plurals_with_token:
if localized_string:
to_append = ""
to_append += token
to_append += f": \"{localized_string.replace("\n", "\\n")}\""
all_locales_strings.append(to_append)

# if that locale doesn't have translation in plurals, add the english hones
if not len(all_locales_strings):
for plural_en_token, plural_en_str in en_plurals_with_token:
all_locales_strings.append(f"{plural_en_token}: \"{plural_en_str.replace("\n", "\\n")}\"")
js_plural_object += f" {wrapValue(locale_key)}:"
js_plural_object += "{\n "
js_plural_object += ",\n ".join(all_locales_strings)
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"

else:
replaced_en = replace_static_strings(value_en)
extracted_vars = extract_vars(replaced_en)
as_record_type = vars_to_record(extracted_vars)
other_locales_replaced_values = [[locale, replace_static_strings(data.get(key, ""))] for locale, data in locales.items()]
extracted_vars_en = extract_vars(replaced_en)
as_record_type_en = vars_to_record(extracted_vars_en)
other_locales_replaced_values = [[locale, replace_static_strings(data.get(key, ""))] for locale, data in locales.items()]

filtered_values = []
# filter out strings that matches the english one (i.e. untranslated strings)
all_locales_strings = []
for locale, replaced_val in other_locales_replaced_values:
if replaced_val == replaced_en and not locale == 'en':
# print(f"{locale}: ${key} saved content is the same as english saved.")
filtered_values.append(f"{locale}: undefined")
if replaced_val:
all_locales_strings.append(f"{locale}: \"{replaced_val.replace("\n", "\\n")}\"")
else:
filtered_values.append(f"{locale}: \"{replaced_val}\"")

all_locales_strings.append(f"{locale}: \"{replaced_en.replace("\n", "\\n")}\"")

# print('key',key, " other_locales_replaced_values:", other_locales_replaced_values)
js_object += f" {wrapValue(key)}:{{\n {",\n ".join(filtered_values)},\n args: {as_record_type if as_record_type else 'undefined'}\n }},\n"
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 += "}"
return js_object
Expand Down Expand Up @@ -189,7 +206,7 @@ def generateLocalesMergedType(locales):
)

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

return f"Locales generated at: {OUTPUT_FILE}"
Expand Down
3 changes: 2 additions & 1 deletion ts/test/session/unit/utils/i18n/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const testDictionary = {

export function initI18n(dictionary: Record<string, string> = en) {
return setupI18n({
// testing
crowdinLocale: 'en',
translationDictionary: dictionary as LocalizerDictionary,
translationDictionary: dictionary as LocalizerDictionary, // testing
});
}
49 changes: 25 additions & 24 deletions ts/types/localizer.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ElementType } from 'react';
import type { Dictionary } from '../localization/locales';
import type { LOCALE_DEFAULTS } from '../localization/constants';

/** The dictionary of localized strings */
export type LocalizerDictionary = Dictionary;
Expand All @@ -9,7 +8,8 @@ export type LocalizerDictionary = Dictionary;
export type LocalizerToken = keyof Dictionary;

/** A dynamic argument that can be used in a localized string */
export type DynamicArg = string | number;
type DynamicArg = string | number;
type DynamicArgStr = 'string' | 'number';

/** A record of dynamic arguments for a specific key in the localization dictionary */
export type ArgsRecord<T extends LocalizerToken> = Record<DynamicArgs<Dictionary[T]>, DynamicArg>;
Expand All @@ -19,32 +19,33 @@ export type DictionaryWithoutPluralStrings = Dictionary;
export type PluralKey = 'count';
export type PluralString = `{${string}, plural, one [${string}] other [${string}]}`;

/** The dynamic arguments in a localized string */
type DynamicArgs<LocalizedString extends string> =
/** If a string follows the plural format use its plural variable name and recursively check for
* dynamic args inside all plural forms */
LocalizedString extends `{${infer PluralVar}, plural, one [${infer PluralOne}] other [${infer PluralOther}]}`
? PluralVar | DynamicArgs<PluralOne> | DynamicArgs<PluralOther>
: /** If a string segment follows the variable form parse its variable name and recursively
* check for more dynamic args */
LocalizedString extends `${string}{${infer Var}}${infer Rest}`
? Var | DynamicArgs<Rest>
: never;
type ArgsTypeStrToTypes<T extends DynamicArgStr> = T extends 'string'
? string
: T extends 'number'
? number
: never;

export type ArgsRecordExcludingDefaults<T extends LocalizerToken> = Omit<
ArgsRecord<T>,
keyof typeof LOCALE_DEFAULTS
>;
// those are still a string of the type "string" | "number" and not the typescript types themselves
type ArgsFromTokenStr<T extends LocalizerToken> = Dictionary[T]['args'] extends undefined
? never
: Dictionary[T]['args'];

type ArgsFromToken<T extends LocalizerToken> = MappedToTsTypes<ArgsFromTokenStr<T>>;
type IsTokenWithCountArgs<T extends LocalizerToken> = 'count' extends keyof ArgsFromToken<T>
? true
: false;

/** The arguments for retrieving a localized message */
export type GetMessageArgs<T extends LocalizerToken> = T extends LocalizerToken
? DynamicArgs<Dictionary[T]> extends never
? ArgsFromToken<T> extends never
? [T]
: ArgsRecordExcludingDefaults<T> extends Record<string, never>
? [T]
: [T, ArgsRecordExcludingDefaults<T>]
: [T, ArgsFromToken<T>]
: never;

type MappedToTsTypes<T extends Record<string, DynamicArgStr>> = {
[K in keyof T]: ArgsTypeStrToTypes<T[K]>;
};

/** Basic props for all calls of the Localizer component */
type LocalizerComponentBaseProps<T extends LocalizerToken> = {
token: T;
Expand All @@ -54,11 +55,11 @@ type LocalizerComponentBaseProps<T extends LocalizerToken> = {

/** The props for the localization component */
export type LocalizerComponentProps<T extends LocalizerToken> = T extends LocalizerToken
? DynamicArgs<Dictionary[T]> extends never
? ArgsFromToken<T> extends never
? LocalizerComponentBaseProps<T>
: ArgsRecordExcludingDefaults<T> extends Record<string, never>
: ArgsFromToken<T> extends Record<string, never>
? LocalizerComponentBaseProps<T>
: LocalizerComponentBaseProps<T> & { args: ArgsRecordExcludingDefaults<T> }
: LocalizerComponentBaseProps<T> & { args: ArgsFromToken<T> }
: never;

export type LocalizerComponentPropsObject = LocalizerComponentProps<LocalizerToken>;
Expand Down
1 change: 1 addition & 0 deletions ts/util/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const setupI18n = ({
if (!translationDictionary || isEmpty(translationDictionary)) {
throw new Error('translationDictionary was not provided');
}
console.warn('translationDictionary', translationDictionary);

setInitialLocale(crowdinLocale, translationDictionary);

Expand Down
2 changes: 1 addition & 1 deletion ts/util/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function update(forceRefresh = false) {
if (shouldHideExpiringMessageBody) {
message = window.i18n('messageNew', { count: messagesNotificationCount });
}

window.drawAttention();
if (status.shouldPlayNotificationSound) {
if (!sound) {
Expand Down
9 changes: 2 additions & 7 deletions ts/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { Store } from '@reduxjs/toolkit';
import { Persistor } from 'redux-persist/es/types';

import { PrimaryColorStateType, ThemeStateType } from './themes/constants/colors';
import type {
GetMessageArgs,
I18nMethods,
LocalizerDictionary,
LocalizerToken,
} from './types/localizer';
import type { GetMessageArgs, I18nMethods, LocalizerToken } from './types/localizer';

export interface LibTextsecure {
messaging: boolean;
Expand Down Expand Up @@ -50,7 +45,7 @@ declare global {
* window.i18n('search', { count: 1, found_count: 1 });
* // => '1 of 1 match'
*/
i18n: (<T extends LocalizerToken, R extends LocalizerDictionary[T]>(
i18n: (<T extends LocalizerToken, R extends string>(
...[token, args]: GetMessageArgs<T>
) => R) & {
/** NOTE: Because of docstring limitations changes MUST be manually synced between {@link setupI18n.getRawMessage } and {@link window.i18n.getRawMessage } */
Expand Down

0 comments on commit 66e80fc

Please sign in to comment.