From 5fc4f4ed365c2c5ee80c8332ab872a3a1bcbeb7f Mon Sep 17 00:00:00 2001
From: John Preston <johnprestonmail@gmail.com>
Date: Tue, 30 May 2017 16:54:05 +0300
Subject: [PATCH] Realtime UI translation in Intro.

Also support realtime translation testing by F7-F6-F7-F8 keys.
---
 Telegram/SourceFiles/intro/introcode.cpp      | 43 +++++++----
 Telegram/SourceFiles/intro/introcode.h        |  4 +-
 Telegram/SourceFiles/intro/introphone.cpp     | 23 +++---
 Telegram/SourceFiles/intro/introphone.h       |  6 +-
 Telegram/SourceFiles/intro/intropwdcheck.cpp  | 42 ++++++++---
 Telegram/SourceFiles/intro/intropwdcheck.h    |  2 +
 Telegram/SourceFiles/intro/introsignup.cpp    | 34 ++++++---
 Telegram/SourceFiles/intro/introsignup.h      |  2 +
 Telegram/SourceFiles/intro/introstart.cpp     |  4 +-
 Telegram/SourceFiles/intro/introwidget.cpp    | 73 ++++++++++++++-----
 Telegram/SourceFiles/intro/introwidget.h      | 24 ++++--
 .../SourceFiles/lang/lang_cloud_manager.cpp   |  9 +--
 Telegram/SourceFiles/lang/lang_instance.cpp   | 24 +++++-
 Telegram/SourceFiles/lang/lang_instance.h     |  2 +-
 Telegram/SourceFiles/mainwindow.cpp           | 32 +++++++-
 .../SourceFiles/settings/settings_widget.cpp  |  4 +-
 .../SourceFiles/ui/widgets/input_fields.cpp   | 23 ++++++
 .../SourceFiles/ui/widgets/input_fields.h     |  4 +
 18 files changed, 266 insertions(+), 89 deletions(-)

diff --git a/Telegram/SourceFiles/intro/introcode.cpp b/Telegram/SourceFiles/intro/introcode.cpp
index f25231b83..4ebf58aca 100644
--- a/Telegram/SourceFiles/intro/introcode.cpp
+++ b/Telegram/SourceFiles/intro/introcode.cpp
@@ -88,6 +88,8 @@ CodeWidget::CodeWidget(QWidget *parent, Widget::Data *data) : Step(parent, data)
 , _callTimeout(getData()->callTimeout)
 , _callLabel(this, st::introDescription)
 , _checkRequest(this) {
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
+
 	connect(_code, SIGNAL(changed()), this, SLOT(onInputChange()));
 	connect(_callTimer, SIGNAL(timeout()), this, SLOT(onSendCall()));
 	connect(_checkRequest, SIGNAL(timeout()), this, SLOT(onCheckRequest()));
@@ -96,12 +98,19 @@ CodeWidget::CodeWidget(QWidget *parent, Widget::Data *data) : Step(parent, data)
 	_code->setDigitsCountMax(getData()->codeLength);
 	setErrorBelowLink(true);
 
-	setTitleText(App::formatPhone(getData()->phone));
+	setTitleText([text = App::formatPhone(getData()->phone)] { return text; });
 	updateDescText();
 }
 
+void CodeWidget::refreshLang() {
+	if (_noTelegramCode) _noTelegramCode->setText(lang(lng_code_no_telegram));
+	if (_code) _code->setPlaceholder(lang(lng_code_ph));
+	updateDescText();
+	updateControlsGeometry();
+}
+
 void CodeWidget::updateDescText() {
-	setDescriptionText(lang(getData()->codeByTelegram ? lng_code_telegram : lng_code_desc));
+	setDescriptionText([byTelegram = getData()->codeByTelegram] { return lang(byTelegram ? lng_code_telegram : lng_code_desc); });
 	if (getData()->codeByTelegram) {
 		_noTelegramCode->show();
 		_callTimer->stop();
@@ -140,15 +149,19 @@ void CodeWidget::updateCallText() {
 
 void CodeWidget::resizeEvent(QResizeEvent *e) {
 	Step::resizeEvent(e);
+	updateControlsGeometry();
+}
+
+void CodeWidget::updateControlsGeometry() {
 	_code->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);
 	auto linkTop = _code->y() + _code->height() + st::introLinkTop;
 	_noTelegramCode->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
 	_callLabel->moveToLeft(contentLeft() + st::buttonRadius, linkTop);
 }
 
-void CodeWidget::showCodeError(const QString &text) {
-	if (!text.isEmpty()) _code->showError();
-	showError(text);
+void CodeWidget::showCodeError(base::lambda<QString()> textFactory) {
+	if (textFactory) _code->showError();
+	showError(std::move(textFactory));
 }
 
 void CodeWidget::setInnerFocus() {
@@ -208,7 +221,7 @@ void CodeWidget::codeSubmitDone(const MTPauth_Authorization &result) {
 	_sentRequest = 0;
 	auto &d = result.c_auth_authorization();
 	if (d.vuser.type() != mtpc_user || !d.vuser.c_user().is_self()) { // wtf?
-		showCodeError(lang(lng_server_error));
+		showCodeError([] { return lang(lng_server_error); });
 		return;
 	}
 	cSetLoggedPhoneNumber(getData()->phone);
@@ -219,7 +232,7 @@ bool CodeWidget::codeSubmitFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
 		stopCheck();
 		_sentRequest = 0;
-		showCodeError(lang(lng_flood_error));
+		showCodeError([] { return lang(lng_flood_error); });
 		return true;
 	}
 	if (MTP::isDefaultHandledError(error)) return false;
@@ -231,7 +244,7 @@ bool CodeWidget::codeSubmitFail(const RPCError &error) {
 		goBack();
 		return true;
 	} else if (err == qstr("PHONE_CODE_EMPTY") || err == qstr("PHONE_CODE_INVALID")) {
-		showCodeError(lang(lng_bad_code));
+		showCodeError([] { return lang(lng_bad_code); });
 		return true;
 	} else if (err == qstr("PHONE_NUMBER_UNOCCUPIED")) { // success, need to signUp
 		getData()->code = _sentCode;
@@ -244,9 +257,10 @@ bool CodeWidget::codeSubmitFail(const RPCError &error) {
 		return true;
 	}
 	if (cDebug()) { // internal server error
-		showCodeError(err + ": " + error.description());
+		auto text = err + ": " + error.description();
+		showCodeError([text] { return text; });
 	} else {
-		showCodeError(lang(lng_server_error));
+		showCodeError([] { return lang(lng_server_error); });
 	}
 	return false;
 }
@@ -324,7 +338,7 @@ void CodeWidget::onNoTelegramCode() {
 
 void CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) {
 	if (result.type() != mtpc_auth_sentCode) {
-		showCodeError(lang(lng_server_error));
+		showCodeError([] { return lang(lng_server_error); });
 		return;
 	}
 
@@ -344,15 +358,16 @@ void CodeWidget::noTelegramCodeDone(const MTPauth_SentCode &result) {
 
 bool CodeWidget::noTelegramCodeFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
-		showCodeError(lang(lng_flood_error));
+		showCodeError([] { return lang(lng_flood_error); });
 		return true;
 	}
 	if (MTP::isDefaultHandledError(error)) return false;
 
 	if (cDebug()) { // internal server error
-		showCodeError(error.type() + ": " + error.description());
+		auto text = error.type() + ": " + error.description();
+		showCodeError([text] { return text; });
 	} else {
-		showCodeError(lang(lng_server_error));
+		showCodeError([] { return lang(lng_server_error); });
 	}
 	return false;
 }
diff --git a/Telegram/SourceFiles/intro/introcode.h b/Telegram/SourceFiles/intro/introcode.h
index 4c9ee071a..37f8f5ad8 100644
--- a/Telegram/SourceFiles/intro/introcode.h
+++ b/Telegram/SourceFiles/intro/introcode.h
@@ -78,11 +78,13 @@ private slots:
 
 private:
 	void updateCallText();
+	void refreshLang();
+	void updateControlsGeometry();
 
 	void codeSubmitDone(const MTPauth_Authorization &result);
 	bool codeSubmitFail(const RPCError &error);
 
-	void showCodeError(const QString &text);
+	void showCodeError(base::lambda<QString()> textFactory);
 	void callDone(const MTPauth_SentCode &v);
 	void gotPassword(const MTPaccount_Password &result);
 
diff --git a/Telegram/SourceFiles/intro/introphone.cpp b/Telegram/SourceFiles/intro/introphone.cpp
index 3665efcfa..9b69b361f 100644
--- a/Telegram/SourceFiles/intro/introphone.cpp
+++ b/Telegram/SourceFiles/intro/introphone.cpp
@@ -48,8 +48,8 @@ PhoneWidget::PhoneWidget(QWidget *parent, Widget::Data *data) : Step(parent, dat
 	connect(_code, SIGNAL(changed()), this, SLOT(onInputChange()));
 	connect(_checkRequest, SIGNAL(timeout()), this, SLOT(onCheckRequest()));
 
-	setTitleText(lang(lng_phone_title));
-	setDescriptionText(lang(lng_phone_desc));
+	setTitleText([] { return lang(lng_phone_title); });
+	setDescriptionText([] { return lang(lng_phone_desc); });
 	subscribe(getData()->updated, [this] { countryChanged(); });
 	setErrorCentered(true);
 
@@ -76,9 +76,9 @@ void PhoneWidget::updateSignupGeometry() {
 	}
 }
 
-void PhoneWidget::showPhoneError(const QString &text) {
+void PhoneWidget::showPhoneError(base::lambda<QString()> textFactory) {
 	_phone->showError();
-	showError(text);
+	showError(std::move(textFactory));
 }
 
 void PhoneWidget::hidePhoneError() {
@@ -90,7 +90,7 @@ void PhoneWidget::hidePhoneError() {
 }
 
 void PhoneWidget::showSignup() {
-	showPhoneError(lang(lng_bad_phone_noreg));
+	showPhoneError([] { return lang(lng_bad_phone_noreg); });
 	if (!_signup) {
 		auto signupText = lng_phone_notreg(lt_link_start, textcmdStartLink(1), lt_link_end, textcmdStopLink(), lt_signup_start, textcmdStartLink(2), lt_signup_end, textcmdStopLink());
 		auto inner = object_ptr<Ui::FlatLabel>(this, signupText, Ui::FlatLabel::InitType::Rich, st::introDescription);
@@ -121,7 +121,7 @@ void PhoneWidget::submit() {
 	if (_sentRequest || isHidden()) return;
 
 	if (!App::isValidPhone(fullNumber())) {
-		showPhoneError(lang(lng_bad_phone));
+		showPhoneError([] { return lang(lng_bad_phone); });
 		_phone->setFocus();
 		return;
 	}
@@ -172,7 +172,7 @@ void PhoneWidget::phoneSubmitDone(const MTPauth_SentCode &result) {
 	_sentRequest = 0;
 
 	if (result.type() != mtpc_auth_sentCode) {
-		showPhoneError(lang(lng_server_error));
+		showPhoneError([] { return lang(lng_server_error); });
 		return;
 	}
 
@@ -203,7 +203,7 @@ bool PhoneWidget::phoneSubmitFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
 		stopCheck();
 		_sentRequest = 0;
-		showPhoneError(lang(lng_flood_error));
+		showPhoneError([] { return lang(lng_flood_error); });
 		return true;
 	}
 	if (MTP::isDefaultHandledError(error)) return false;
@@ -215,13 +215,14 @@ bool PhoneWidget::phoneSubmitFail(const RPCError &error) {
 		Ui::show(Box<InformBox>(lang(lng_error_phone_flood)));
 		return true;
 	} else if (err == qstr("PHONE_NUMBER_INVALID")) { // show error
-		showPhoneError(lang(lng_bad_phone));
+		showPhoneError([] { return lang(lng_bad_phone); });
 		return true;
 	}
 	if (cDebug()) { // internal server error
-		showPhoneError(err + ": " + error.description());
+		auto text = err + ": " + error.description();
+		showPhoneError([text] { return text; });
 	} else {
-		showPhoneError(lang(lng_server_error));
+		showPhoneError([] { return lang(lng_server_error); });
 	}
 	return false;
 }
diff --git a/Telegram/SourceFiles/intro/introphone.h b/Telegram/SourceFiles/intro/introphone.h
index fd2c35857..2f1ae2ed4 100644
--- a/Telegram/SourceFiles/intro/introphone.h
+++ b/Telegram/SourceFiles/intro/introphone.h
@@ -53,6 +53,10 @@ public:
 protected:
 	void resizeEvent(QResizeEvent *e) override;
 
+	bool hasChangeLanguageNoCover() const override {
+		return true;
+	}
+
 private slots:
 	void onInputChange();
 	void onCheckRequest();
@@ -70,7 +74,7 @@ private:
 	QString fullNumber() const;
 	void stopCheck();
 
-	void showPhoneError(const QString &text);
+	void showPhoneError(base::lambda<QString()> textFactory);
 	void hidePhoneError();
 	void showSignup();
 
diff --git a/Telegram/SourceFiles/intro/intropwdcheck.cpp b/Telegram/SourceFiles/intro/intropwdcheck.cpp
index 9f25f4ce8..2b2debf48 100644
--- a/Telegram/SourceFiles/intro/intropwdcheck.cpp
+++ b/Telegram/SourceFiles/intro/intropwdcheck.cpp
@@ -43,13 +43,15 @@ PwdCheckWidget::PwdCheckWidget(QWidget *parent, Widget::Data *data) : Step(paren
 , _toRecover(this, lang(lng_signin_recover))
 , _toPassword(this, lang(lng_signin_try_password))
 , _checkRequest(this) {
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
+
 	connect(_checkRequest, SIGNAL(timeout()), this, SLOT(onCheckRequest()));
 	connect(_toRecover, SIGNAL(clicked()), this, SLOT(onToRecover()));
 	connect(_toPassword, SIGNAL(clicked()), this, SLOT(onToPassword()));
 	connect(_pwdField, SIGNAL(changed()), this, SLOT(onInputChange()));
 	connect(_codeField, SIGNAL(changed()), this, SLOT(onInputChange()));
 
-	setTitleText(lang(lng_signin_title));
+	setTitleText([] { return lang(lng_signin_title); });
 	updateDescriptionText();
 	setErrorBelowLink(true);
 
@@ -64,8 +66,20 @@ PwdCheckWidget::PwdCheckWidget(QWidget *parent, Widget::Data *data) : Step(paren
 	setMouseTracking(true);
 }
 
+void PwdCheckWidget::refreshLang() {
+	if (_pwdField) _pwdField->setPlaceholder(lang(lng_signin_password));
+	if (_codeField) _codeField->setPlaceholder(lang(lng_signin_code));
+	if (_toRecover) _toRecover->setText(lang(lng_signin_recover));
+	if (_toPassword) _toPassword->setText(lang(lng_signin_try_password));
+	updateControlsGeometry();
+}
+
 void PwdCheckWidget::resizeEvent(QResizeEvent *e) {
 	Step::resizeEvent(e);
+	updateControlsGeometry();
+}
+
+void PwdCheckWidget::updateControlsGeometry() {
 	_pwdField->moveToLeft(contentLeft(), contentTop() + st::introPasswordTop);
 	_pwdHint->moveToLeft(contentLeft() + st::buttonRadius, contentTop() + st::introPasswordHintTop);
 	_codeField->moveToLeft(contentLeft(), contentTop() + st::introStepFieldTop);
@@ -121,7 +135,7 @@ void PwdCheckWidget::pwdSubmitDone(bool recover, const MTPauth_Authorization &re
 	}
 	auto &d = result.c_auth_authorization();
 	if (d.vuser.type() != mtpc_user || !d.vuser.c_user().is_self()) { // wtf?
-		showError(lang(lng_server_error));
+		showError([] { return lang(lng_server_error); });
 		return;
 	}
 	finish(d.vuser);
@@ -131,7 +145,7 @@ bool PwdCheckWidget::pwdSubmitFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
 		_sentRequest = 0;
 		stopCheck();
-		showError(lang(lng_flood_error));
+		showError([] { return lang(lng_flood_error); });
 		_pwdField->showError();
 		return true;
 	}
@@ -141,7 +155,7 @@ bool PwdCheckWidget::pwdSubmitFail(const RPCError &error) {
 	stopCheck();
 	auto &err = error.type();
 	if (err == qstr("PASSWORD_HASH_INVALID")) {
-		showError(lang(lng_signin_bad_password));
+		showError([] { return lang(lng_signin_bad_password); });
 		_pwdField->selectAll();
 		_pwdField->showError();
 		return true;
@@ -149,9 +163,10 @@ bool PwdCheckWidget::pwdSubmitFail(const RPCError &error) {
 		goBack();
 	}
 	if (cDebug()) { // internal server error
-		showError(err + ": " + error.description());
+		auto text = err + ": " + error.description();
+		showError([text] { return text; });
 	} else {
-		showError(lang(lng_server_error));
+		showError([] { return lang(lng_server_error); });
 	}
 	_pwdField->setFocus();
 	return false;
@@ -159,7 +174,7 @@ bool PwdCheckWidget::pwdSubmitFail(const RPCError &error) {
 
 bool PwdCheckWidget::codeSubmitFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
-		showError(lang(lng_flood_error));
+		showError([] { return lang(lng_flood_error); });
 		_codeField->showError();
 		return true;
 	}
@@ -179,15 +194,16 @@ bool PwdCheckWidget::codeSubmitFail(const RPCError &error) {
 		onToPassword();
 		return true;
 	} else if (err == qstr("CODE_INVALID")) {
-		showError(lang(lng_signin_wrong_code));
+		showError([] { return lang(lng_signin_wrong_code); });
 		_codeField->selectAll();
 		_codeField->showError();
 		return true;
 	}
 	if (cDebug()) { // internal server error
-		showError(err + ": " + error.description());
+		auto text = err + ": " + error.description();
+		showError([text] { return text; });
 	} else {
-		showError(lang(lng_server_error));
+		showError([] { return lang(lng_server_error); });
 	}
 	_codeField->setFocus();
 	return false;
@@ -253,7 +269,11 @@ void PwdCheckWidget::showReset() {
 }
 
 void PwdCheckWidget::updateDescriptionText() {
-	setDescriptionText(_pwdField->isHidden() ? lng_signin_recover_desc(lt_email, _emailPattern) : lang(lng_signin_desc));
+	auto pwdHidden = _pwdField->isHidden();
+	auto emailPattern = _emailPattern;
+	setDescriptionText([pwdHidden, emailPattern] {
+		return pwdHidden ? lng_signin_recover_desc(lt_email, emailPattern) : lang(lng_signin_desc);
+	});
 }
 
 void PwdCheckWidget::onInputChange() {
diff --git a/Telegram/SourceFiles/intro/intropwdcheck.h b/Telegram/SourceFiles/intro/intropwdcheck.h
index 9998dd2b3..182649268 100644
--- a/Telegram/SourceFiles/intro/intropwdcheck.h
+++ b/Telegram/SourceFiles/intro/intropwdcheck.h
@@ -54,6 +54,8 @@ private slots:
 
 private:
 	void showReset();
+	void refreshLang();
+	void updateControlsGeometry();
 
 	void pwdSubmitDone(bool recover, const MTPauth_Authorization &result);
 	bool pwdSubmitFail(const RPCError &error);
diff --git a/Telegram/SourceFiles/intro/introsignup.cpp b/Telegram/SourceFiles/intro/introsignup.cpp
index 6dfa587de..3cf46afa6 100644
--- a/Telegram/SourceFiles/intro/introsignup.cpp
+++ b/Telegram/SourceFiles/intro/introsignup.cpp
@@ -40,6 +40,8 @@ SignupWidget::SignupWidget(QWidget *parent, Widget::Data *data) : Step(parent, d
 , _last(this, st::introName, lang(lng_signup_lastname))
 , _invertOrder(langFirstNameGoesSecond())
 , _checkRequest(this) {
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
+
 	connect(_checkRequest, SIGNAL(timeout()), this, SLOT(onCheckRequest()));
 
 	setupPhotoButton();
@@ -49,11 +51,17 @@ SignupWidget::SignupWidget(QWidget *parent, Widget::Data *data) : Step(parent, d
 	}
 	setErrorCentered(true);
 
-	setTitleText(lang(lng_signup_title));
-	setDescriptionText(lang(lng_signup_desc));
+	setTitleText([] { return lang(lng_signup_title); });
+	setDescriptionText([] { return lang(lng_signup_desc); });
 	setMouseTracking(true);
 }
 
+void SignupWidget::refreshLang() {
+	_first->setPlaceholder(lang(lng_signup_firstname));
+	_last->setPlaceholder(lang(lng_signup_lastname));
+	updateControlsGeometry();
+}
+
 void SignupWidget::setupPhotoButton() {
 	_photo->setClickedCallback(App::LambdaDelayed(st::defaultActiveButton.ripple.hideDuration, this, [this] {
 		auto imgExtensions = cImgExtensions();
@@ -71,7 +79,7 @@ void SignupWidget::setupPhotoButton() {
 			}
 
 			if (img.isNull() || img.width() > 10 * img.height() || img.height() > 10 * img.width()) {
-				showError(lang(lng_bad_photo));
+				showError([] { return lang(lng_bad_photo); });
 				return;
 			}
 			auto box = Ui::show(Box<PhotoCropBox>(img, PeerId(0)));
@@ -82,7 +90,10 @@ void SignupWidget::setupPhotoButton() {
 
 void SignupWidget::resizeEvent(QResizeEvent *e) {
 	Step::resizeEvent(e);
+	updateControlsGeometry();
+}
 
+void SignupWidget::updateControlsGeometry() {
 	auto photoRight = contentLeft() + st::introNextButton.width;
 	auto photoTop = contentTop() + st::introPhotoTop;
 	_photo->moveToLeft(photoRight - _photo->width(), photoTop);
@@ -144,7 +155,7 @@ void SignupWidget::nameSubmitDone(const MTPauth_Authorization &result) {
 	stopCheck();
 	auto &d = result.c_auth_authorization();
 	if (d.vuser.type() != mtpc_user || !d.vuser.c_user().is_self()) { // wtf?
-		showError(lang(lng_server_error));
+		showError([] { return lang(lng_server_error); });
 		return;
 	}
 	finish(d.vuser, _photoImage);
@@ -153,7 +164,7 @@ void SignupWidget::nameSubmitDone(const MTPauth_Authorization &result) {
 bool SignupWidget::nameSubmitFail(const RPCError &error) {
 	if (MTP::isFloodError(error)) {
 		stopCheck();
-		showError(lang(lng_flood_error));
+		showError([] { return lang(lng_flood_error); });
 		if (_invertOrder) {
 			_first->setFocus();
 		} else {
@@ -174,18 +185,19 @@ bool SignupWidget::nameSubmitFail(const RPCError &error) {
 		goBack();
 		return true;
 	} else if (err == "FIRSTNAME_INVALID") {
-		showError(lang(lng_bad_name));
+		showError([] { return lang(lng_bad_name); });
 		_first->setFocus();
 		return true;
 	} else if (err == "LASTNAME_INVALID") {
-		showError(lang(lng_bad_name));
+		showError([] { return lang(lng_bad_name); });
 		_last->setFocus();
 		return true;
 	}
 	if (cDebug()) { // internal server error
-		showError(err + ": " + error.description());
+		auto text = err + ": " + error.description();
+		showError([text] { return text; });
 	} else {
-		showError(lang(lng_server_error));
+		showError([] { return lang(lng_server_error); });
 	}
 	if (_invertOrder) {
 		_last->setFocus();
@@ -196,7 +208,7 @@ bool SignupWidget::nameSubmitFail(const RPCError &error) {
 }
 
 void SignupWidget::onInputChange() {
-	showError(QString());
+	hideError();
 }
 
 void SignupWidget::submit() {
@@ -219,7 +231,7 @@ void SignupWidget::submit() {
 		}
 	}
 
-	showError(QString());
+	hideError();
 
 	_firstName = _first->getLastText().trimmed();
 	_lastName = _last->getLastText().trimmed();
diff --git a/Telegram/SourceFiles/intro/introsignup.h b/Telegram/SourceFiles/intro/introsignup.h
index 3b87e373b..2cda87e86 100644
--- a/Telegram/SourceFiles/intro/introsignup.h
+++ b/Telegram/SourceFiles/intro/introsignup.h
@@ -52,6 +52,8 @@ private slots:
 
 private:
 	void setupPhotoButton();
+	void refreshLang();
+	void updateControlsGeometry();
 
 	void nameSubmitDone(const MTPauth_Authorization &result);
 	bool nameSubmitFail(const RPCError &error);
diff --git a/Telegram/SourceFiles/intro/introstart.cpp b/Telegram/SourceFiles/intro/introstart.cpp
index c98f4993f..6cba84bf0 100644
--- a/Telegram/SourceFiles/intro/introstart.cpp
+++ b/Telegram/SourceFiles/intro/introstart.cpp
@@ -30,8 +30,8 @@ namespace Intro {
 
 StartWidget::StartWidget(QWidget *parent, Widget::Data *data) : Step(parent, data, true) {
 	setMouseTracking(true);
-	setTitleText(qsl("Telegram Desktop"));
-	setDescriptionText(lang(lng_intro_about));
+	setTitleText([] { return qsl("Telegram Desktop"); });
+	setDescriptionText([] { return lang(lng_intro_about); });
 	show();
 }
 
diff --git a/Telegram/SourceFiles/intro/introwidget.cpp b/Telegram/SourceFiles/intro/introwidget.cpp
index f22992e25..72c501b0f 100644
--- a/Telegram/SourceFiles/intro/introwidget.cpp
+++ b/Telegram/SourceFiles/intro/introwidget.cpp
@@ -71,14 +71,17 @@ Widget::Widget(QWidget *parent) : TWidget(parent)
 
 	_settings->entity()->setClickedCallback([] { App::wnd()->showSettings(); });
 
-	subscribe(Lang::CurrentCloudManager().firstLanguageSuggestion(), [this] { createLanguageLink(); });
-	createLanguageLink();
-
 	getNearestDC();
 
 	appendStep(new StartWidget(this, getData()));
 	fixOrder();
 
+	subscribe(Lang::CurrentCloudManager().firstLanguageSuggestion(), [this] { createLanguageLink(); });
+	createLanguageLink();
+	if (_changeLanguage) _changeLanguage->finishAnimation();
+
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
+
 	show();
 	showControls();
 	getStep()->showFast();
@@ -94,14 +97,26 @@ Widget::Widget(QWidget *parent) : TWidget(parent)
 #endif // !TDESKTOP_DISABLE_AUTOUPDATE
 }
 
+void Widget::refreshLang() {
+	if (_settings) _settings->entity()->setText(lang(lng_menu_settings));
+	if (_update) _update->entity()->setText(lang(lng_menu_update));
+	if (_resetAccount) _resetAccount->entity()->setText(lang(lng_signin_reset_account));
+	if (_next) _next->setText(getStep()->nextButtonText());
+	updateControlsGeometry();
+}
+
 void Widget::createLanguageLink() {
 	if (_changeLanguage) return;
 
 	auto createLink = [this](const QString &text, const QString &languageId) {
 		_changeLanguage.create(this, object_ptr<Ui::LinkButton>(this, text), st::introCoverDuration);
+		_changeLanguage->show();
+		_changeLanguage->hideFast();
 		_changeLanguage->entity()->setClickedCallback([this, languageId] {
 			Lang::CurrentCloudManager().switchToLanguage(languageId);
 		});
+		_changeLanguage->toggleAnimated(getStep()->hasChangeLanguage());
+		updateControlsGeometry();
 	};
 
 	auto currentId = Lang::Current().id();
@@ -124,7 +139,7 @@ void Widget::createLanguageLink() {
 void Widget::onCheckUpdateStatus() {
 	if (Sandbox::updatingState() == Application::UpdatingReady) {
 		if (_update) return;
-		_update.create(this, object_ptr<Ui::RoundButton>(this, lang(lng_menu_update).toUpper(), st::defaultBoxButton), st::introCoverDuration);
+		_update.create(this, object_ptr<Ui::RoundButton>(this, lang(lng_menu_update), st::defaultBoxButton), st::introCoverDuration);
 		if (!_a_show.animating()) _update->show();
 		_update->entity()->setClickedCallback([] {
 			checkReadyUpdate();
@@ -176,7 +191,7 @@ void Widget::historyMove(Direction direction) {
 	auto stepHasCover = getStep()->hasCover();
 	_settings->toggleAnimated(!stepHasCover);
 	if (_update) _update->toggleAnimated(!stepHasCover);
-	if (_changeLanguage) _changeLanguage->toggleAnimated(stepHasCover);
+	if (_changeLanguage) _changeLanguage->toggleAnimated(getStep()->hasChangeLanguage());
 	_next->setText(getStep()->nextButtonText());
 	if (_resetAccount) _resetAccount->hideAnimated();
 	getStep()->showAnimated(direction);
@@ -259,7 +274,7 @@ void Widget::resetAccount() {
 				Ui::show(Box<InformBox>(lang(lng_signin_reset_cancelled)));
 			} else {
 				Ui::hideLayer();
-				getStep()->showError(lang(lng_server_error));
+				getStep()->showError([] { return lang(lng_server_error); });
 			}
 		}).send();
 	})));
@@ -285,7 +300,7 @@ void Widget::showControls() {
 	auto hasCover = getStep()->hasCover();
 	_settings->toggleFast(!hasCover);
 	if (_update) _update->toggleFast(!hasCover);
-	if (_changeLanguage) _changeLanguage->toggleFast(hasCover);
+	if (_changeLanguage) _changeLanguage->toggleFast(getStep()->hasChangeLanguage());
 	_back->toggleFast(getStep()->hasBack());
 }
 
@@ -372,9 +387,6 @@ void Widget::resizeEvent(QResizeEvent *e) {
 	updateControlsGeometry();
 }
 
-void Widget::moveControls() {
-}
-
 void Widget::updateControlsGeometry() {
 	auto shown = _coverShownAnimation.current(1.);
 
@@ -468,13 +480,30 @@ void Widget::Step::updateLabelsPosition() {
 	}
 }
 
-void Widget::Step::setTitleText(QString richText) {
-	_title->setRichText(richText);
+void Widget::Step::setTitleText(base::lambda<QString()> richTitleTextFactory) {
+	_titleTextFactory = std::move(richTitleTextFactory);
+	refreshTitle();
 	updateLabelsPosition();
 }
 
-void Widget::Step::setDescriptionText(QString richText) {
-	_description->entity()->setRichText(richText);
+void Widget::Step::refreshTitle() {
+	_title->setRichText(_titleTextFactory());
+}
+
+void Widget::Step::setDescriptionText(base::lambda<QString()> richDescriptionTextFactory) {
+	_descriptionTextFactory = std::move(richDescriptionTextFactory);
+	refreshDescription();
+	updateLabelsPosition();
+}
+
+void Widget::Step::refreshDescription() {
+	_description->entity()->setRichText(_descriptionTextFactory());
+}
+
+void Widget::Step::refreshLang() {
+	refreshTitle();
+	refreshDescription();
+	refreshError();
 	updateLabelsPosition();
 }
 
@@ -650,16 +679,21 @@ void Widget::Step::setErrorBelowLink(bool below) {
 	}
 }
 
-void Widget::Step::showError(const QString &text) {
-	_errorText = text;
-	if (_errorText.isEmpty()) {
+void Widget::Step::showError(base::lambda<QString()> textFactory) {
+	_errorTextFactory = std::move(textFactory);
+	refreshError();
+	updateLabelsPosition();
+}
+
+void Widget::Step::refreshError() {
+	if (!_errorTextFactory) {
 		if (_error) _error->hideAnimated();
 	} else {
 		if (!_error) {
 			_error.create(this, object_ptr<Ui::FlatLabel>(this, _errorCentered ? st::introErrorCentered : st::introError), st::introErrorDuration);
 			_error->hideFast();
 		}
-		_error->entity()->setText(text);
+		_error->entity()->setText(_errorTextFactory());
 		updateLabelsPosition();
 		_error->showAnimated();
 	}
@@ -679,6 +713,7 @@ Widget::Step::Step(QWidget *parent, Data *data, bool hasCover) : TWidget(parent)
 			}
 		}
 	});
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
 }
 
 void Widget::Step::prepareShowAnimated(Step *after) {
@@ -755,7 +790,7 @@ bool Widget::Step::hasBack() const {
 void Widget::Step::activate() {
 	_title->show();
 	_description->show();
-	if (!_errorText.isEmpty()) {
+	if (_errorTextFactory) {
 		_error->showFast();
 	}
 }
diff --git a/Telegram/SourceFiles/intro/introwidget.h b/Telegram/SourceFiles/intro/introwidget.h
index 4097b70d8..2fa647ab4 100644
--- a/Telegram/SourceFiles/intro/introwidget.h
+++ b/Telegram/SourceFiles/intro/introwidget.h
@@ -111,6 +111,9 @@ public:
 		bool animating() const;
 
 		bool hasCover() const;
+		bool hasChangeLanguage() const {
+			return hasCover() || hasChangeLanguageNoCover();
+		}
 		virtual bool hasBack() const;
 		virtual void activate();
 		virtual void cancelled();
@@ -124,9 +127,9 @@ public:
 
 		void setErrorCentered(bool centered);
 		void setErrorBelowLink(bool below);
-		void showError(const QString &text);
+		void showError(base::lambda<QString()> textFactory);
 		void hideError() {
-			showError(QString());
+			showError(base::lambda<QString()>());
 		}
 
 		~Step();
@@ -135,8 +138,8 @@ public:
 		void paintEvent(QPaintEvent *e) override;
 		void resizeEvent(QResizeEvent *e) override;
 
-		void setTitleText(QString richText);
-		void setDescriptionText(QString richText);
+		void setTitleText(base::lambda<QString()> richTitleTextFactory);
+		void setDescriptionText(base::lambda<QString()> richDescriptionTextFactory);
 		bool paintAnimated(Painter &p, QRect clip);
 
 		void fillSentCodeData(const MTPauth_SentCodeType &type);
@@ -161,6 +164,9 @@ public:
 		void showResetButton() {
 			if (_showResetCallback) _showResetCallback();
 		}
+		virtual bool hasChangeLanguageNoCover() const {
+			return false;
+		}
 
 	private:
 		struct CoverAnimation {
@@ -178,6 +184,10 @@ public:
 		};
 		void updateLabelsPosition();
 		void paintContentSnapshot(Painter &p, const QPixmap &snapshot, float64 alpha, float64 howMuchHidden);
+		void refreshError();
+		void refreshTitle();
+		void refreshDescription();
+		void refreshLang();
 
 		CoverAnimation prepareCoverAnimation(Step *step);
 		QPixmap prepareContentSnapshot();
@@ -193,11 +203,13 @@ public:
 		base::lambda<void()> _showResetCallback;
 
 		object_ptr<Ui::FlatLabel> _title;
+		base::lambda<QString()> _titleTextFactory;
 		object_ptr<Ui::WidgetFadeWrap<Ui::FlatLabel>> _description;
+		base::lambda<QString()> _descriptionTextFactory;
 
 		bool _errorCentered = false;
 		bool _errorBelowLink = false;
-		QString _errorText;
+		base::lambda<QString()> _errorTextFactory;
 		object_ptr<Ui::WidgetFadeWrap<Ui::FlatLabel>> _error = { nullptr };
 
 		Animation _a_show;
@@ -208,6 +220,7 @@ public:
 	};
 
 private:
+	void refreshLang();
 	void animationCallback();
 	void createLanguageLink();
 
@@ -219,7 +232,6 @@ private:
 	void fixOrder();
 	void showControls();
 	void hideControls();
-	void moveControls();
 	QRect calculateStepRect() const;
 
 	void showResetButton();
diff --git a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp
index e9426d04a..8410ba5bf 100644
--- a/Telegram/SourceFiles/lang/lang_cloud_manager.cpp
+++ b/Telegram/SourceFiles/lang/lang_cloud_manager.cpp
@@ -59,8 +59,7 @@ void CloudManager::requestLangPackDifference() {
 }
 
 void CloudManager::setSuggestedLanguage(const QString &langCode) {
-	if (_langpack.id().isEmpty()
-		&& !langCode.isEmpty()
+	if (!langCode.isEmpty()
 		&& langCode != Lang::DefaultLanguageId()) {
 		_suggestedLanguage = langCode;
 	} else {
@@ -71,7 +70,7 @@ void CloudManager::setSuggestedLanguage(const QString &langCode) {
 		_languageWasSuggested = true;
 		_firstLanguageSuggestion.notify();
 
-		if (AuthSession::Exists() && !_suggestedLanguage.isEmpty()) {
+		if (AuthSession::Exists() && _langpack.id().isEmpty() && !_suggestedLanguage.isEmpty()) {
 			_offerSwitchToId = _suggestedLanguage;
 			offerSwitchLangPack();
 		}
@@ -174,10 +173,6 @@ void CloudManager::applyLangPackData(const MTPDlangPackDifference &data) {
 	} else if (!data.vstrings.v.isEmpty()) {
 		_langpack.applyDifference(data);
 		Local::writeLangPack();
-		auto fullLangPackUpdated = (data.vfrom_version.v == 0);
-		if (fullLangPackUpdated) {
-			_langpack.updated().notify();
-		}
 	} else {
 		LOG(("Lang Info: Up to date."));
 	}
diff --git a/Telegram/SourceFiles/lang/lang_instance.cpp b/Telegram/SourceFiles/lang/lang_instance.cpp
index 052c3d9e9..53d57f0b0 100644
--- a/Telegram/SourceFiles/lang/lang_instance.cpp
+++ b/Telegram/SourceFiles/lang/lang_instance.cpp
@@ -230,6 +230,21 @@ bool ValueParser::parse() {
 	return true;
 }
 
+QString PrepareTestValue(const QString &current, QChar filler) {
+	auto size = current.size();
+	auto result = QString(size + 1, filler);
+	auto inCommand = false;
+	for (auto i = 0; i != size; ++i) {
+		auto ch = current[i];
+		auto newInCommand = (ch.unicode() == TextCommand) ? (!inCommand) : inCommand;
+		if (inCommand || newInCommand || ch.isSpace()) {
+			result[i + 1] = ch;
+		}
+		inCommand = newInCommand;
+	}
+	return result;
+}
+
 } // namespace
 
 QString DefaultLanguageId() {
@@ -239,6 +254,12 @@ QString DefaultLanguageId() {
 void Instance::switchToId(const QString &id) {
 	reset();
 	_id = id;
+	if (_id == qstr("TEST_X") || _id == qstr("TEST_0")) {
+		for (auto &value : _values) {
+			value = PrepareTestValue(value, _id[5]);
+		}
+		_updated.notify();
+	}
 }
 
 void Instance::chooseCustomFile() {
@@ -263,7 +284,7 @@ void Instance::switchToCustomFile(const QString &filePath) {
 	reset();
 	fillFromCustomFile(filePath);
 	Local::writeLangPack();
-	updated().notify();
+	_updated.notify();
 }
 
 void Instance::reset() {
@@ -488,6 +509,7 @@ void Instance::applyDifference(const MTPDlangPackDifference &difference) {
 			resetValue(key);
 		});
 	}
+	_updated.notify();
 }
 
 std::map<LangKey, QString> Instance::ParseStrings(const MTPVector<MTPLangPackString> &strings) {
diff --git a/Telegram/SourceFiles/lang/lang_instance.h b/Telegram/SourceFiles/lang/lang_instance.h
index 252c76cf6..f32b8a940 100644
--- a/Telegram/SourceFiles/lang/lang_instance.h
+++ b/Telegram/SourceFiles/lang/lang_instance.h
@@ -55,7 +55,7 @@ public:
 		return _id;
 	}
 	bool isCustom() const {
-		return id() == qstr("custom");
+		return (_id == qstr("custom") || _id == qstr("TEST_X") || _id == qstr("TEST_0"));
 	}
 	int version() const {
 		return _version;
diff --git a/Telegram/SourceFiles/mainwindow.cpp b/Telegram/SourceFiles/mainwindow.cpp
index 3472cc710..2aa41056b 100644
--- a/Telegram/SourceFiles/mainwindow.cpp
+++ b/Telegram/SourceFiles/mainwindow.cpp
@@ -27,6 +27,8 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "ui/widgets/popup_menu.h"
 #include "ui/widgets/buttons.h"
 #include "base/zlib_help.h"
+#include "lang/lang_cloud_manager.h"
+#include "lang/lang_instance.h"
 #include "lang/lang_keys.h"
 #include "shortcuts.h"
 #include "messenger.h"
@@ -54,6 +56,29 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
 #include "auth_session.h"
 #include "window/window_controller.h"
 
+namespace {
+
+// Code for testing languages is F7-F6-F7-F8
+void FeedLangTestingKey(int key) {
+	static auto codeState = 0;
+	if ((codeState == 0 && key == Qt::Key_F7)
+		|| (codeState == 1 && key == Qt::Key_F6)
+		|| (codeState == 2 && key == Qt::Key_F7)
+		|| (codeState == 3 && key == Qt::Key_F8)) {
+		++codeState;
+	} else {
+		codeState = 0;
+	}
+	if (codeState == 4) {
+		codeState = 0;
+
+		auto testLanguageId = (Lang::Current().id() == qstr("TEST_X")) ? qsl("TEST_0") : qsl("TEST_X");
+		Lang::CurrentCloudManager().switchToLanguage(testLanguageId);
+	}
+}
+
+} // namespace
+
 ConnectingWidget::ConnectingWidget(QWidget *parent, const QString &text, const QString &reconnect) : TWidget(parent)
 , _reconnect(this, QString()) {
 	set(text, reconnect);
@@ -568,8 +593,13 @@ void MainWindow::setInnerFocus() {
 
 bool MainWindow::eventFilter(QObject *object, QEvent *e) {
 	switch (e->type()) {
-	case QEvent::MouseButtonPress:
 	case QEvent::KeyPress:
+		if (cDebug() && e->type() == QEvent::KeyPress && object == windowHandle()) {
+			auto key = static_cast<QKeyEvent*>(e)->key();
+			FeedLangTestingKey(key);
+		}
+		[[fallthrough]];
+	case QEvent::MouseButtonPress:
 	case QEvent::TouchBegin:
 	case QEvent::Wheel:
 		psUserActionDone();
diff --git a/Telegram/SourceFiles/settings/settings_widget.cpp b/Telegram/SourceFiles/settings/settings_widget.cpp
index 6b1196807..62a1cb846 100644
--- a/Telegram/SourceFiles/settings/settings_widget.cpp
+++ b/Telegram/SourceFiles/settings/settings_widget.cpp
@@ -198,9 +198,7 @@ void codesFeedString(const QString &text) {
 
 Widget::Widget(QWidget *parent) {
 	refreshLang();
-	subscribe(Lang::Current().updated(), [this] {
-		refreshLang();
-	});
+	subscribe(Lang::Current().updated(), [this] { refreshLang(); });
 
 	_inner = setInnerWidget(object_ptr<InnerWidget>(this));
 	setCloseClickHandler([]() {
diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.cpp b/Telegram/SourceFiles/ui/widgets/input_fields.cpp
index 49703bb70..230b97c2e 100644
--- a/Telegram/SourceFiles/ui/widgets/input_fields.cpp
+++ b/Telegram/SourceFiles/ui/widgets/input_fields.cpp
@@ -1621,6 +1621,11 @@ void FlatInput::resizeEvent(QResizeEvent *e) {
 	return QLineEdit::resizeEvent(e);
 }
 
+void FlatInput::setPlaceholder(const QString &ph) {
+	_fullph = ph;
+	updatePlaceholderText();
+}
+
 void FlatInput::updatePlaceholderText() {
 	int32 availw = width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1;
 	if (_st.font->width(_fullph) > availw) {
@@ -2464,6 +2469,12 @@ void InputArea::createPlaceholderPath() {
 	}
 }
 
+void InputArea::setPlaceholder(const QString &ph) {
+	_placeholderFull = ph;
+	createPlaceholderPath();
+	update();
+}
+
 void InputArea::showError() {
 	setErrorShown(true);
 	if (!hasFocus()) {
@@ -3222,6 +3233,12 @@ void InputField::createPlaceholderPath() {
 	}
 }
 
+void InputField::setPlaceholder(const QString &ph) {
+	_placeholderFull = ph;
+	createPlaceholderPath();
+	update();
+}
+
 void InputField::showError() {
 	setErrorShown(true);
 	if (!hasFocus()) {
@@ -3505,6 +3522,12 @@ void MaskedInputField::createPlaceholderPath() {
 	}
 }
 
+void MaskedInputField::setPlaceholder(const QString &ph) {
+	_placeholderFull = ph;
+	createPlaceholderPath();
+	update();
+}
+
 void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
 	if (auto menu = createStandardContextMenu()) {
 		(new Ui::PopupMenu(nullptr, menu))->popup(e->globalPos());
diff --git a/Telegram/SourceFiles/ui/widgets/input_fields.h b/Telegram/SourceFiles/ui/widgets/input_fields.h
index e4012d328..3c0af6c39 100644
--- a/Telegram/SourceFiles/ui/widgets/input_fields.h
+++ b/Telegram/SourceFiles/ui/widgets/input_fields.h
@@ -238,6 +238,7 @@ public:
 	FlatInput(QWidget *parent, const style::FlatInput &st, const QString &ph = QString(), const QString &val = QString());
 
 	void updatePlaceholder();
+	void setPlaceholder(const QString &ph);
 	const QString &placeholder() const;
 	QRect placeholderRect() const;
 
@@ -325,6 +326,7 @@ public:
 	const QString &getLastText() const {
 		return _oldtext;
 	}
+	void setPlaceholder(const QString &ph);
 	void setDisplayFocused(bool focused);
 	void finishAnimations();
 	void setFocusFast() {
@@ -503,6 +505,7 @@ public:
 	const QString &getLastText() const {
 		return _oldtext;
 	}
+	void setPlaceholder(const QString &ph);
 	void setPlaceholderHidden(bool forcePlaceholderHidden);
 	void setDisplayFocused(bool focused);
 	void finishAnimations();
@@ -689,6 +692,7 @@ public:
 	const QString &getLastText() const {
 		return _oldtext;
 	}
+	void setPlaceholder(const QString &ph);
 	void setPlaceholderHidden(bool forcePlaceholderHidden);
 	void setDisplayFocused(bool focused);
 	void finishAnimations();