From 945acf5df0c332e81cd5ff4bbd22261faadbf4ad Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sun, 21 Mar 2021 13:59:59 +0400 Subject: [PATCH] Temp language commit --- src/components/button.ts | 5 +- src/components/buttonMenu.ts | 5 +- src/components/inputField.ts | 9 +- src/components/sidebarLeft/index.ts | 7 +- .../sidebarLeft/tabs/chatFolders.ts | 15 ++- .../sidebarLeft/tabs/editProfile.ts | 15 +-- src/components/sidebarLeft/tabs/settings.ts | 14 +-- src/components/sliderTab.ts | 6 + src/lib/langPack.ts | 103 +++++++++++++++++- 9 files changed, 140 insertions(+), 39 deletions(-) diff --git a/src/components/button.ts b/src/components/button.ts index fe4064b5..3a8b32f7 100644 --- a/src/components/button.ts +++ b/src/components/button.ts @@ -1,6 +1,7 @@ +import { i18n, LangPackKey } from "../lib/langPack"; import { ripple } from "./ripple"; -const Button = (className: string, options: Partial<{noRipple: true, onlyMobile: true, icon: string, rippleSquare: true, text: string, disabled: boolean}> = {}) => { +const Button = (className: string, options: Partial<{noRipple: true, onlyMobile: true, icon: string, rippleSquare: true, text: LangPackKey, disabled: boolean}> = {}) => { const button = document.createElement('button'); button.className = className + (options.icon ? ' tgico-' + options.icon : ''); @@ -21,7 +22,7 @@ const Button = (className: string, options: Partial<{noRipple: true, onlyMobile: } if(options.text) { - button.append(options.text); + button.append(i18n(options.text)); } return button; diff --git a/src/components/buttonMenu.ts b/src/components/buttonMenu.ts index 3e759d98..1f7e208c 100644 --- a/src/components/buttonMenu.ts +++ b/src/components/buttonMenu.ts @@ -1,11 +1,12 @@ import { attachClickEvent, AttachClickOptions, cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom"; import ListenerSetter from "../helpers/listenerSetter"; +import { i18n, LangPackKey } from "../lib/langPack"; import { closeBtnMenu } from "./misc"; import { ripple } from "./ripple"; export type ButtonMenuItemOptions = { icon: string, - text: string, + text: LangPackKey, onClick: (e: MouseEvent | TouchEvent) => void, element?: HTMLElement, options?: AttachClickOptions @@ -18,7 +19,7 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => { const {icon, text, onClick} = options; const el = document.createElement('div'); el.className = 'btn-menu-item tgico-' + icon; - el.innerText = text; + el.append(i18n(text)); ripple(el); diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 3dc9d139..fceef259 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -1,6 +1,7 @@ import { getRichValue, isInputEmpty } from "../helpers/dom"; import { debounce } from "../helpers/schedulers"; import { checkRTL } from "../helpers/string"; +import { i18n_, LangPackKey } from "../lib/langPack"; import RichTextProcessor from "../lib/richtextprocessor"; let init = () => { @@ -59,7 +60,7 @@ export enum InputState { export type InputFieldOptions = { placeholder?: string, - label?: string, + label?: LangPackKey, name?: string, maxLength?: number, showLengthOn?: number, @@ -97,7 +98,6 @@ class InputField { this.container.innerHTML = `
- ${label ? `` : ''} `; input = this.container.firstElementChild as HTMLElement; @@ -135,7 +135,6 @@ class InputField { } else { this.container.innerHTML = ` - ${label ? `` : ''} `; input = this.container.firstElementChild as HTMLElement; @@ -143,7 +142,9 @@ class InputField { } if(label) { - this.label = this.container.lastElementChild as HTMLLabelElement; + this.label = document.createElement('label'); + i18n_({element: this.label, key: label}); + this.container.append(this.label); } let processInput: () => void; diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index b1e21ce6..2df87c85 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -23,6 +23,7 @@ import AppNewChannelTab from "./tabs/newChannel"; import AppContactsTab from "./tabs/contacts"; import AppArchivedTab from "./tabs/archivedTab"; import AppAddMembersTab from "./tabs/addMembers"; +import { i18n_, LangPackKey } from "../../lib/langPack"; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; @@ -413,7 +414,7 @@ export class SettingSection { public caption: HTMLElement; constructor(options: { - name?: string, + name?: LangPackKey, caption?: string, noDelimiter?: boolean }) { @@ -432,7 +433,7 @@ export class SettingSection { if(options.name) { this.title = document.createElement('div'); this.title.classList.add('sidebar-left-h2', 'sidebar-left-section-name'); - this.title.innerHTML = options.name; + i18n_({element: this.title, key: options.name}); this.content.append(this.title); } @@ -451,7 +452,7 @@ export class SettingSection { } } -export const generateSection = (appendTo: Scrollable, name?: string, caption?: string) => { +export const generateSection = (appendTo: Scrollable, name?: LangPackKey, caption?: string) => { const section = new SettingSection({name, caption}); appendTo.append(section.container); return section.content; diff --git a/src/components/sidebarLeft/tabs/chatFolders.ts b/src/components/sidebarLeft/tabs/chatFolders.ts index 364208f6..3e857364 100644 --- a/src/components/sidebarLeft/tabs/chatFolders.ts +++ b/src/components/sidebarLeft/tabs/chatFolders.ts @@ -16,6 +16,7 @@ import rootScope from "../../../lib/rootScope"; import AppEditFolderTab from "./editFolder"; import Row from "../../row"; import { SettingSection } from ".."; +import { i18n_ } from "../../../lib/langPack"; export default class AppChatFoldersTab extends SliderSuperTab { private createFolderBtn: HTMLElement; @@ -101,7 +102,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { protected init() { this.container.classList.add('chat-folders-container'); - this.title.innerText = 'Chat Folders'; + this.setTitle('ChatList.Filter.List.Title'); this.scrollable.container.classList.add('chat-folders'); @@ -110,20 +111,20 @@ export default class AppChatFoldersTab extends SliderSuperTab { const caption = document.createElement('div'); caption.classList.add('caption'); - caption.innerHTML = `Create folders for different groups of chats
and quickly switch between them.`; + i18n_({element: caption, key: 'ChatList.Filter.Header'}); this.createFolderBtn = Button('btn-primary btn-color-primary btn-create-folder', { - text: 'Create Folder', + text: 'ChatList.Filter.NewTitle', icon: 'add' }); this.foldersSection = new SettingSection({ - name: 'Folders' + name: 'ChatList.Filter.List.Header' }); this.foldersSection.container.style.display = 'none'; this.suggestedSection = new SettingSection({ - name: 'Recommended folders' + name: 'ChatList.Filter.Recommended.Header' }); this.suggestedSection.container.style.display = 'none'; @@ -207,9 +208,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { suggestedFilters.forEach(filter => { const div = this.renderFolder(filter); - const button = document.createElement('button'); - button.classList.add('btn-primary', 'btn-color-primary'); - button.innerText = 'Add'; + const button = Button('btn-primary btn-color-primary', {text: 'ChatList.Filter.Recommended.Add'}); div.append(button); this.suggestedSection.content.append(div); diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index b8201de6..d1a7b98b 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -5,6 +5,7 @@ import { SliderSuperTab } from "../../slider"; import { attachClickEvent } from "../../../helpers/dom"; import EditPeer from "../../editPeer"; import { UsernameInputField } from "../../usernameInputField"; +import { i18n_ } from "../../../lib/langPack"; // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) @@ -21,7 +22,7 @@ export default class AppEditProfileTab extends SliderSuperTab { protected async init() { this.container.classList.add('edit-profile-container'); - this.title.innerText = 'Edit Profile'; + this.setTitle('EditAccount.Title'); const inputFields: InputField[] = []; @@ -30,17 +31,17 @@ export default class AppEditProfileTab extends SliderSuperTab { inputWrapper.classList.add('input-wrapper'); this.firstNameInputField = new InputField({ - label: 'Name', + label: 'Login.Register.FirstName.Placeholder', name: 'first-name', maxLength: 70 }); this.lastNameInputField = new InputField({ - label: 'Last Name', + label: 'Login.Register.LastName.Placeholder', name: 'last-name', maxLength: 64 }); this.bioInputField = new InputField({ - label: 'Bio (optional)', + label: 'AccountSettings.Bio', name: 'bio', maxLength: 70 }); @@ -49,7 +50,7 @@ export default class AppEditProfileTab extends SliderSuperTab { const caption = document.createElement('div'); caption.classList.add('caption'); - caption.innerHTML = 'Any details such as age, occupation or city. Example:
23 y.o. designer from San Francisco.'; + i18n_({element: caption, key: 'Bio.Description'}); inputFields.push(this.firstNameInputField, this.lastNameInputField, this.bioInputField); this.scrollable.append(inputWrapper, caption); @@ -68,14 +69,14 @@ export default class AppEditProfileTab extends SliderSuperTab { { const h2 = document.createElement('div'); h2.classList.add('sidebar-left-h2'); - h2.innerText = 'Username'; + i18n_({element: h2, key: 'EditAccount.Username'}); const inputWrapper = document.createElement('div'); inputWrapper.classList.add('input-wrapper'); this.usernameInputField = new UsernameInputField({ peerId: 0, - label: 'Username (optional)', + label: 'EditAccount.Username', name: 'username', plainText: true, listenerSetter: this.listenerSetter, diff --git a/src/components/sidebarLeft/tabs/settings.ts b/src/components/sidebarLeft/tabs/settings.ts index aaaea64b..221d32de 100644 --- a/src/components/sidebarLeft/tabs/settings.ts +++ b/src/components/sidebarLeft/tabs/settings.ts @@ -32,7 +32,7 @@ export default class AppSettingsTab extends SliderSuperTab { const btnMenu = ButtonMenuToggle({}, 'bottom-left', [{ icon: 'logout', - text: 'Log Out', + text: 'EditAccount.Logout', onClick: () => { apiManager.logOut(); } @@ -96,12 +96,12 @@ export default class AppSettingsTab extends SliderSuperTab { buttonsDiv.classList.add('profile-buttons'); const className = 'profile-button btn-primary btn-transparent'; - buttonsDiv.append(this.buttons.edit = Button(className, {icon: 'edit', text: 'Edit Profile'})); - buttonsDiv.append(this.buttons.folders = Button(className, {icon: 'folder', text: 'Chat Folders'})); - buttonsDiv.append(this.buttons.general = Button(className, {icon: 'settings', text: 'General Settings'})); - buttonsDiv.append(this.buttons.notifications = Button(className, {icon: 'unmute', text: 'Notifications'})); - buttonsDiv.append(this.buttons.privacy = Button(className, {icon: 'lock', text: 'Privacy and Security'})); - buttonsDiv.append(this.buttons.language = Button(className, {icon: 'language', text: 'Language', disabled: true})); + buttonsDiv.append(this.buttons.edit = Button(className, {icon: 'edit', text: 'EditAccount.Title'})); + buttonsDiv.append(this.buttons.folders = Button(className, {icon: 'folder', text: 'AccountSettings.Filters'})); + buttonsDiv.append(this.buttons.general = Button(className, {icon: 'settings', text: 'Telegram.GeneralSettingsViewController'})); + buttonsDiv.append(this.buttons.notifications = Button(className, {icon: 'unmute', text: 'AccountSettings.Notifications'})); + buttonsDiv.append(this.buttons.privacy = Button(className, {icon: 'lock', text: 'AccountSettings.PrivacyAndSecurity'})); + buttonsDiv.append(this.buttons.language = Button(className, {icon: 'language', text: 'AccountSettings.Language'})); this.scrollable.append(this.avatarElem, this.nameDiv, this.phoneDiv, buttonsDiv); this.scrollable.container.classList.add('profile-content-wrapper'); diff --git a/src/components/sliderTab.ts b/src/components/sliderTab.ts index f4372b92..ca4abaff 100644 --- a/src/components/sliderTab.ts +++ b/src/components/sliderTab.ts @@ -1,5 +1,6 @@ import EventListenerBase from "../helpers/eventListenerBase"; import ListenerSetter from "../helpers/listenerSetter"; +import { i18n, LangPackKey } from "../lib/langPack"; import ButtonIcon from "./buttonIcon"; import Scrollable from "./scrollable"; import SidebarSlider from "./slider"; @@ -92,6 +93,11 @@ export default class SliderSuperTab implements SliderTab { this.listenerSetter.removeAll(); } } + + protected setTitle(key: LangPackKey) { + this.title.innerHTML = ''; + this.title.append(i18n(key)); + } } export class SliderSuperTabEventable extends SliderSuperTab { diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 1b4c6eff..99950d4b 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -1,3 +1,4 @@ +import { MOUNT_CLASS_TO } from "../config/debug"; import { LangPackString } from "../layer"; import apiManager from "./mtproto/mtprotoworker"; @@ -31,8 +32,26 @@ export const langPack: {[actionType: string]: string} = { "messageActionBotAllowed": "You allowed this bot to message you when logged in {}" }; -export namespace Internationalization { - let strings: {[key: string]: LangPackString} = {}; +namespace Strings { + export type Bio = 'Bio.Description'; + + export type LoginRegister = 'Login.Register.FirstName.Placeholder' | 'Login.Register.LastName.Placeholder'; + + export type EditAccount = 'EditAccount.Logout' | 'EditAccount.Title' | 'EditAccount.Title' | 'EditAccount.Username'; + + export type AccountSettings = 'AccountSettings.Filters' | 'AccountSettings.Notifications' | 'AccountSettings.PrivacyAndSecurity' | 'AccountSettings.Language' | 'AccountSettings.Bio'; + + export type Telegram = 'Telegram.GeneralSettingsViewController'; + + export type ChatFilters = 'ChatList.Filter.Header' | 'ChatList.Filter.NewTitle' | 'ChatList.Filter.List.Header' | 'ChatList.Filter.Recommended.Header' | 'ChatList.Filter.Recommended.Add' | 'ChatList.Filter.List.Title'; + + export type LangPackKey = AccountSettings | EditAccount | Telegram | ChatFilters | LoginRegister | Bio | string; +} + +export type LangPackKey = Strings.LangPackKey; + +namespace I18n { + let strings: Partial<{[key in LangPackKey]: LangPackString}> = {}; export function getLangPack(langCode: string) { return apiManager.invokeApi('langpack.getLangPack', { @@ -41,14 +60,86 @@ export namespace Internationalization { }).then(langPack => { strings = {}; for(const string of langPack.strings) { - strings[string.key] = string; + strings[string.key as LangPackKey] = string; } + + const elements = Array.from(document.querySelectorAll(`.i18n`)) as HTMLElement[]; + elements.forEach(element => { + const instance = weakMap.get(element); + + if(instance) { + instance.update(); + } + }); }); } - export function _(key: keyof typeof strings, ...args: any[]) { - let str = strings[key]; + export function getString(key: LangPackKey, args?: any[]) { + const str = strings[key]; + let out = ''; + + if(str) { + if(str._ === 'langPackStringPluralized') { + out = str.one_value; + } else if(str._ === 'langPackString') { + out = str.value; + } else { + out = '[' + key + ']'; + } + } else { + out = '[' + key + ']'; + } + + return out; + } + + const weakMap: WeakMap = new WeakMap(); + + export type IntlElementOptions = { + element?: HTMLElement, + property?: 'innerHTML' | 'placeholder' + key: LangPackKey, + args?: any[] + }; + export class IntlElement { + public element: IntlElementOptions['element']; + public key: IntlElementOptions['key']; + public args: IntlElementOptions['args']; + public property: IntlElementOptions['property'] = 'innerHTML'; + + constructor(options: IntlElementOptions) { + this.element = options.element || document.createElement('span'); + this.element.classList.add('i18n'); + + this.update(options); + weakMap.set(this.element, this); + } + + public update(options?: IntlElementOptions) { + if(options) { + Object.assign(this, options); + } + + (this.element as any)[this.property] = getString(this.key, this.args); + } + } + + export function i18n(key: LangPackKey, args?: any[]) { + return new IntlElement({key, args}).element; + } - return str; + export function i18n_(options: IntlElementOptions) { + return new IntlElement(options).element; } } + +export {I18n}; +export default I18n; + +const i18n = I18n.i18n; +export {i18n}; + +const i18n_ = I18n.i18n_; +export {i18n_}; + +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.I18n = I18n);