1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "lang/lang_instance.h"
9 
10 #include "core/application.h"
11 #include "storage/serialize_common.h"
12 #include "storage/localstorage.h"
13 #include "ui/boxes/confirm_box.h"
14 #include "lang/lang_file_parser.h"
15 #include "base/platform/base_platform_info.h"
16 #include "base/qthelp_regex.h"
17 
18 namespace Lang {
19 namespace {
20 
21 const auto kSerializeVersionTag = qsl("#new");
22 constexpr auto kSerializeVersion = 1;
23 constexpr auto kDefaultLanguage = "en"_cs;
24 constexpr auto kCloudLangPackName = "tdesktop"_cs;
25 constexpr auto kCustomLanguage = "#custom"_cs;
26 constexpr auto kLangValuesLimit = 20000;
27 
PrepareDefaultValues()28 std::vector<QString> PrepareDefaultValues() {
29 	auto result = std::vector<QString>();
30 	result.reserve(kKeysCount);
31 	for (auto i = 0; i != kKeysCount; ++i) {
32 		result.emplace_back(GetOriginalValue(ushort(i)));
33 	}
34 	return result;
35 }
36 
37 class ValueParser {
38 public:
39 	ValueParser(
40 		const QByteArray &key,
41 		ushort keyIndex,
42 		const QByteArray &value);
43 
takeResult()44 	QString takeResult() {
45 		Expects(!_failed);
46 
47 		return std::move(_result);
48 	}
49 
50 	bool parse();
51 
52 private:
53 	void appendToResult(const char *nextBegin);
54 	bool logError(const QString &text);
55 	bool readTag();
56 
57 	const QByteArray &_key;
58 	ushort _keyIndex = kKeysCount;
59 
60 	QLatin1String _currentTag;
61 	ushort _currentTagIndex = 0;
62 	QString _currentTagReplacer;
63 
64 	bool _failed = true;
65 
66 	const char *_begin = nullptr;
67 	const char *_ch = nullptr;
68 	const char *_end = nullptr;
69 
70 	QString _result;
71 	OrderedSet<ushort> _tagsUsed;
72 
73 };
74 
ValueParser(const QByteArray & key,ushort keyIndex,const QByteArray & value)75 ValueParser::ValueParser(
76 	const QByteArray &key,
77 	ushort keyIndex,
78 	const QByteArray &value)
79 : _key(key)
80 , _keyIndex(keyIndex)
81 , _currentTag("")
82 , _begin(value.constData())
83 , _ch(_begin)
84 , _end(_begin + value.size()) {
85 }
86 
appendToResult(const char * nextBegin)87 void ValueParser::appendToResult(const char *nextBegin) {
88 	if (_ch > _begin) _result.append(QString::fromUtf8(_begin, _ch - _begin));
89 	_begin = nextBegin;
90 }
91 
logError(const QString & text)92 bool ValueParser::logError(const QString &text) {
93 	_failed = true;
94 	auto loggedKey = (_currentTag.size() > 0) ? (_key + QString(':') + _currentTag) : QString(_key);
95 	LOG(("Lang Error: %1 (key '%2')").arg(text, loggedKey));
96 	return false;
97 }
98 
readTag()99 bool ValueParser::readTag() {
100 	auto tagStart = _ch;
101 	auto isTagChar = [](QChar ch) {
102 		if (ch >= 'a' && ch <= 'z') {
103 			return true;
104 		} else if (ch >= 'A' && ch <= 'z') {
105 			return true;
106 		} else if (ch >= '0' && ch <= '9') {
107 			return true;
108 		}
109 		return (ch == '_');
110 	};
111 	while (_ch != _end && isTagChar(*_ch)) {
112 		++_ch;
113 	}
114 	if (_ch == tagStart) {
115 		return logError("Expected tag name");
116 	}
117 
118 	_currentTag = QLatin1String(tagStart, _ch - tagStart);
119 	if (_ch == _end || *_ch != '}') {
120 		return logError("Expected '}' after tag name");
121 	}
122 
123 	_currentTagIndex = GetTagIndex(_currentTag);
124 	if (_currentTagIndex == kTagsCount) {
125 		return logError("Unknown tag");
126 	}
127 	if (!IsTagReplaced(_keyIndex, _currentTagIndex)) {
128 		return logError("Unexpected tag");
129 	}
130 	if (_tagsUsed.contains(_currentTagIndex)) {
131 		return logError("Repeated tag");
132 	}
133 	_tagsUsed.insert(_currentTagIndex);
134 
135 	if (_currentTagReplacer.isEmpty()) {
136 		_currentTagReplacer = QString(4, TextCommand);
137 		_currentTagReplacer[1] = TextCommandLangTag;
138 	}
139 	_currentTagReplacer[2] = QChar(0x0020 + _currentTagIndex);
140 
141 	return true;
142 }
143 
parse()144 bool ValueParser::parse() {
145 	_failed = false;
146 	_result.reserve(_end - _begin);
147 	for (; _ch != _end; ++_ch) {
148 		if (*_ch == '{') {
149 			appendToResult(_ch);
150 
151 			++_ch;
152 			if (!readTag()) {
153 				return false;
154 			}
155 
156 			_result.append(_currentTagReplacer);
157 
158 			_begin = _ch + 1;
159 			_currentTag = QLatin1String("");
160 		}
161 	}
162 	appendToResult(_end);
163 	return true;
164 }
165 
PrepareTestValue(const QString & current,QChar filler)166 QString PrepareTestValue(const QString &current, QChar filler) {
167 	auto size = current.size();
168 	auto result = QString(size + 1, filler);
169 	auto inCommand = false;
170 	for (auto i = 0; i != size; ++i) {
171 		auto ch = current[i];
172 		auto newInCommand = (ch.unicode() == TextCommand) ? (!inCommand) : inCommand;
173 		if (inCommand || newInCommand || ch.isSpace()) {
174 			result[i + 1] = ch;
175 		}
176 		inCommand = newInCommand;
177 	}
178 	return result;
179 }
180 
PluralCodeForCustom(const QString & absolutePath,const QString & relativePath)181 QString PluralCodeForCustom(
182 	const QString &absolutePath,
183 	const QString &relativePath) {
184 	const auto path = !absolutePath.isEmpty()
185 		? absolutePath
186 		: relativePath;
187 	const auto name = QFileInfo(path).fileName();
188 	if (const auto match = qthelp::regex_match(
189 			"_([a-z]{2,3}_[A-Z]{2,3}|\\-[a-z]{2,3})?\\.",
190 			name)) {
191 		return match->captured(1);
192 	}
193 	return DefaultLanguageId();
194 }
195 
196 template <typename Save>
ParseKeyValue(const QByteArray & key,const QByteArray & value,Save && save)197 void ParseKeyValue(
198 		const QByteArray &key,
199 		const QByteArray &value,
200 		Save &&save) {
201 	const auto index = GetKeyIndex(QLatin1String(key));
202 	if (index != kKeysCount) {
203 		ValueParser parser(key, index, value);
204 		if (parser.parse()) {
205 			save(index, parser.takeResult());
206 		}
207 	} else if (!key.startsWith("cloud_")) {
208 		DEBUG_LOG(("Lang Warning: Unknown key '%1'"
209 			).arg(QString::fromLatin1(key)));
210 	}
211 }
212 
213 } // namespace
214 
DefaultLanguageId()215 QString DefaultLanguageId() {
216 	return kDefaultLanguage.utf16();
217 }
218 
LanguageIdOrDefault(const QString & id)219 QString LanguageIdOrDefault(const QString &id) {
220 	return !id.isEmpty() ? id : DefaultLanguageId();
221 }
222 
CloudLangPackName()223 QString CloudLangPackName() {
224 	return kCloudLangPackName.utf16();
225 }
226 
CustomLanguageId()227 QString CustomLanguageId() {
228 	return kCustomLanguage.utf16();
229 }
230 
DefaultLanguage()231 Language DefaultLanguage() {
232 	return Language{
233 		qsl("en"),
234 		QString(),
235 		QString(),
236 		qsl("English"),
237 		qsl("English"),
238 	};
239 }
240 
241 struct Instance::PrivateTag {
242 };
243 
Instance()244 Instance::Instance()
245 : _values(PrepareDefaultValues())
246 , _nonDefaultSet(kKeysCount, 0) {
247 }
248 
Instance(not_null<Instance * > derived,const PrivateTag &)249 Instance::Instance(not_null<Instance*> derived, const PrivateTag &)
250 : _derived(derived)
251 , _nonDefaultSet(kKeysCount, 0) {
252 }
253 
switchToId(const Language & data)254 void Instance::switchToId(const Language &data) {
255 	reset(data);
256 	if (_id == qstr("#TEST_X") || _id == qstr("#TEST_0")) {
257 		for (auto &value : _values) {
258 			value = PrepareTestValue(value, _id[5]);
259 		}
260 		if (!_derived) {
261 			_updated.fire({});
262 		}
263 	}
264 	updatePluralRules();
265 }
266 
setBaseId(const QString & baseId,const QString & pluralId)267 void Instance::setBaseId(const QString &baseId, const QString &pluralId) {
268 	if (baseId.isEmpty()) {
269 		_base = nullptr;
270 	} else {
271 		if (!_base) {
272 			_base = std::make_unique<Instance>(this, PrivateTag{});
273 		}
274 		_base->switchToId({ baseId, pluralId });
275 	}
276 }
277 
switchToCustomFile(const QString & filePath)278 void Instance::switchToCustomFile(const QString &filePath) {
279 	if (loadFromCustomFile(filePath)) {
280 		Local::writeLangPack();
281 		_updated.fire({});
282 	}
283 }
284 
reset(const Language & data)285 void Instance::reset(const Language &data) {
286 	const auto computedPluralId = !data.pluralId.isEmpty()
287 		? data.pluralId
288 		: !data.baseId.isEmpty()
289 		? data.baseId
290 		: data.id;
291 	setBaseId(data.baseId, computedPluralId);
292 	_id = LanguageIdOrDefault(data.id);
293 	_pluralId = computedPluralId;
294 	_name = data.name;
295 	_nativeName = data.nativeName;
296 
297 	_customFilePathAbsolute = QString();
298 	_customFilePathRelative = QString();
299 	_customFileContent = QByteArray();
300 	_version = 0;
301 	_nonDefaultValues.clear();
302 	for (auto i = 0, count = int(_values.size()); i != count; ++i) {
303 		_values[i] = GetOriginalValue(ushort(i));
304 	}
305 	ranges::fill(_nonDefaultSet, 0);
306 	updateChoosingStickerReplacement();
307 
308 	_idChanges.fire_copy(_id);
309 }
310 
systemLangCode() const311 QString Instance::systemLangCode() const {
312 	if (_systemLanguage.isEmpty()) {
313 		_systemLanguage = Platform::SystemLanguage();
314 		if (_systemLanguage.isEmpty()) {
315 			auto uiLanguages = QLocale::system().uiLanguages();
316 			if (!uiLanguages.isEmpty()) {
317 				_systemLanguage = uiLanguages.front();
318 			}
319 			if (_systemLanguage.isEmpty()) {
320 				_systemLanguage = DefaultLanguageId();
321 			}
322 		}
323 	}
324 	return _systemLanguage;
325 }
326 
cloudLangCode(Pack pack) const327 QString Instance::cloudLangCode(Pack pack) const {
328 	return (isCustom() || id().isEmpty())
329 		? DefaultLanguageId()
330 		: id(pack);
331 }
332 
id() const333 QString Instance::id() const {
334 	return id(Pack::Current);
335 }
336 
idChanges() const337 rpl::producer<QString> Instance::idChanges() const {
338 	return _idChanges.events();
339 }
340 
baseId() const341 QString Instance::baseId() const {
342 	return id(Pack::Base);
343 }
344 
name() const345 QString Instance::name() const {
346 	return _name.isEmpty()
347 		? getValue(tr::lng_language_name.base)
348 		: _name;
349 }
350 
nativeName() const351 QString Instance::nativeName() const {
352 	return _nativeName.isEmpty()
353 		? getValue(tr::lng_language_name.base)
354 		: _nativeName;
355 }
356 
id(Pack pack) const357 QString Instance::id(Pack pack) const {
358 	return (pack != Pack::Base)
359 		? _id
360 		: _base
361 		? _base->id(Pack::Current)
362 		: QString();
363 }
364 
isCustom() const365 bool Instance::isCustom() const {
366 	return (_id == CustomLanguageId())
367 		|| (_id == qstr("#TEST_X"))
368 		|| (_id == qstr("#TEST_0"));
369 }
370 
version(Pack pack) const371 int Instance::version(Pack pack) const {
372 	return (pack != Pack::Base)
373 		? _version
374 		: _base
375 		? _base->version(Pack::Current)
376 		: 0;
377 }
378 
langPackName() const379 QString Instance::langPackName() const {
380 	return isCustom() ? QString() : CloudLangPackName();
381 }
382 
serialize() const383 QByteArray Instance::serialize() const {
384 	auto size = Serialize::stringSize(kSerializeVersionTag)
385 		+ sizeof(qint32) // serializeVersion
386 		+ Serialize::stringSize(_id)
387 		+ Serialize::stringSize(_pluralId)
388 		+ Serialize::stringSize(_name)
389 		+ Serialize::stringSize(_nativeName)
390 		+ sizeof(qint32) // version
391 		+ Serialize::stringSize(_customFilePathAbsolute)
392 		+ Serialize::stringSize(_customFilePathRelative)
393 		+ Serialize::bytearraySize(_customFileContent)
394 		+ sizeof(qint32); // _nonDefaultValues.size()
395 	for (auto &nonDefault : _nonDefaultValues) {
396 		size += Serialize::bytearraySize(nonDefault.first)
397 			+ Serialize::bytearraySize(nonDefault.second);
398 	}
399 	const auto base = _base ? _base->serialize() : QByteArray();
400 	size += Serialize::bytearraySize(base);
401 
402 	auto result = QByteArray();
403 	result.reserve(size);
404 	{
405 		QDataStream stream(&result, QIODevice::WriteOnly);
406 		stream.setVersion(QDataStream::Qt_5_1);
407 		stream
408 			<< kSerializeVersionTag
409 			<< qint32(kSerializeVersion)
410 			<< _id
411 			<< _pluralId
412 			<< _name
413 			<< _nativeName
414 			<< qint32(_version)
415 			<< _customFilePathAbsolute
416 			<< _customFilePathRelative
417 			<< _customFileContent
418 			<< qint32(_nonDefaultValues.size());
419 		for (const auto &nonDefault : _nonDefaultValues) {
420 			stream << nonDefault.first << nonDefault.second;
421 		}
422 		stream << base;
423 	}
424 	return result;
425 }
426 
fillFromSerialized(const QByteArray & data,int dataAppVersion)427 void Instance::fillFromSerialized(
428 		const QByteArray &data,
429 		int dataAppVersion) {
430 	QDataStream stream(data);
431 	stream.setVersion(QDataStream::Qt_5_1);
432 	qint32 serializeVersion = 0;
433 	QString serializeVersionTag;
434 	QString id, pluralId, name, nativeName;
435 	qint32 version = 0;
436 	QString customFilePathAbsolute, customFilePathRelative;
437 	QByteArray customFileContent;
438 	qint32 nonDefaultValuesCount = 0;
439 	stream >> serializeVersionTag;
440 	const auto legacyFormat = (serializeVersionTag != kSerializeVersionTag);
441 	if (legacyFormat) {
442 		id = serializeVersionTag;
443 		stream
444 			>> version
445 			>> customFilePathAbsolute
446 			>> customFilePathRelative
447 			>> customFileContent
448 			>> nonDefaultValuesCount;
449 	} else {
450 		stream >> serializeVersion;
451 		if (serializeVersion == kSerializeVersion) {
452 			stream
453 				>> id
454 				>> pluralId
455 				>> name
456 				>> nativeName
457 				>> version
458 				>> customFilePathAbsolute
459 				>> customFilePathRelative
460 				>> customFileContent
461 				>> nonDefaultValuesCount;
462 		} else {
463 			LOG(("Lang Error: Unsupported serialize version."));
464 			return;
465 		}
466 	}
467 	if (stream.status() != QDataStream::Ok) {
468 		LOG(("Lang Error: Could not read data from serialized langpack."));
469 		return;
470 	}
471 	if (nonDefaultValuesCount > kLangValuesLimit) {
472 		LOG(("Lang Error: Values count limit exceeded: %1"
473 			).arg(nonDefaultValuesCount));
474 		return;
475 	}
476 
477 	if (!customFilePathAbsolute.isEmpty()) {
478 		id = CustomLanguageId();
479 		auto currentCustomFileContent = Lang::FileParser::ReadFile(
480 			customFilePathAbsolute,
481 			customFilePathRelative);
482 		if (!currentCustomFileContent.isEmpty()
483 			&& currentCustomFileContent != customFileContent) {
484 			fillFromCustomContent(
485 				customFilePathAbsolute,
486 				customFilePathRelative,
487 				currentCustomFileContent);
488 			Local::writeLangPack();
489 			return;
490 		}
491 	}
492 
493 	std::vector<QByteArray> nonDefaultStrings;
494 	nonDefaultStrings.reserve(2 * nonDefaultValuesCount);
495 	for (auto i = 0; i != nonDefaultValuesCount; ++i) {
496 		QByteArray key, value;
497 		stream >> key >> value;
498 		if (stream.status() != QDataStream::Ok) {
499 			LOG(("Lang Error: "
500 				"Could not read data from serialized langpack."));
501 			return;
502 		}
503 
504 		nonDefaultStrings.push_back(key);
505 		nonDefaultStrings.push_back(value);
506 	}
507 
508 	_base = nullptr;
509 	QByteArray base;
510 	if (legacyFormat) {
511 		if (!stream.atEnd()) {
512 			stream >> pluralId;
513 		} else {
514 			pluralId = id;
515 		}
516 		if (!stream.atEnd()) {
517 			stream >> base;
518 			if (base.isEmpty()) {
519 				stream.setStatus(QDataStream::ReadCorruptData);
520 			}
521 		}
522 		if (stream.status() != QDataStream::Ok) {
523 			LOG(("Lang Error: "
524 				"Could not read data from serialized langpack."));
525 			return;
526 		}
527 	} else {
528 		stream >> base;
529 	}
530 	if (!base.isEmpty()) {
531 		_base = std::make_unique<Instance>(this, PrivateTag{});
532 		_base->fillFromSerialized(base, dataAppVersion);
533 	}
534 
535 	_id = id;
536 	_pluralId = (id == CustomLanguageId())
537 		? PluralCodeForCustom(
538 			customFilePathAbsolute,
539 			customFilePathRelative)
540 		: pluralId;
541 	_name = name;
542 	_nativeName = nativeName;
543 	_version = version;
544 	_customFilePathAbsolute = customFilePathAbsolute;
545 	_customFilePathRelative = customFilePathRelative;
546 	_customFileContent = customFileContent;
547 	LOG(("Lang Info: Loaded cached, keys: %1").arg(nonDefaultValuesCount));
548 	for (auto i = 0, count = nonDefaultValuesCount * 2; i != count; i += 2) {
549 		applyValue(nonDefaultStrings[i], nonDefaultStrings[i + 1]);
550 	}
551 	updatePluralRules();
552 	updateChoosingStickerReplacement();
553 
554 	_idChanges.fire_copy(_id);
555 }
556 
loadFromContent(const QByteArray & content)557 void Instance::loadFromContent(const QByteArray &content) {
558 	Lang::FileParser loader(content, [this](QLatin1String key, const QByteArray &value) {
559 		applyValue(QByteArray(key.data(), key.size()), value);
560 	});
561 	if (!loader.errors().isEmpty()) {
562 		LOG(("Lang load errors: %1").arg(loader.errors()));
563 	} else if (!loader.warnings().isEmpty()) {
564 		LOG(("Lang load warnings: %1").arg(loader.warnings()));
565 	}
566 }
567 
fillFromCustomContent(const QString & absolutePath,const QString & relativePath,const QByteArray & content)568 void Instance::fillFromCustomContent(
569 		const QString &absolutePath,
570 		const QString &relativePath,
571 		const QByteArray &content) {
572 	setBaseId(QString(), QString());
573 	_id = CustomLanguageId();
574 	_pluralId = PluralCodeForCustom(absolutePath, relativePath);
575 	_name = _nativeName = QString();
576 	loadFromCustomContent(absolutePath, relativePath, content);
577 	updateChoosingStickerReplacement();
578 
579 	_idChanges.fire_copy(_id);
580 }
581 
loadFromCustomContent(const QString & absolutePath,const QString & relativePath,const QByteArray & content)582 void Instance::loadFromCustomContent(
583 		const QString &absolutePath,
584 		const QString &relativePath,
585 		const QByteArray &content) {
586 	_version = 0;
587 	_customFilePathAbsolute = absolutePath;
588 	_customFilePathRelative = relativePath;
589 	_customFileContent = content;
590 	loadFromContent(_customFileContent);
591 }
592 
loadFromCustomFile(const QString & filePath)593 bool Instance::loadFromCustomFile(const QString &filePath) {
594 	auto absolutePath = QFileInfo(filePath).absoluteFilePath();
595 	auto relativePath = QDir().relativeFilePath(filePath);
596 	auto content = Lang::FileParser::ReadFile(absolutePath, relativePath);
597 	if (!content.isEmpty()) {
598 		reset({
599 			CustomLanguageId(),
600 			PluralCodeForCustom(absolutePath, relativePath) });
601 		loadFromCustomContent(absolutePath, relativePath, content);
602 		updatePluralRules();
603 		return true;
604 	}
605 	return false;
606 }
607 
updateChoosingStickerReplacement()608 void Instance::updateChoosingStickerReplacement() {
609 	// A language changing in the runtime is not supported.
610 	const auto replacement = kChoosingStickerReplacement.utf8();
611 	const auto phrase = tr::lng_send_action_choose_sticker(tr::now);
612 	const auto first = phrase.indexOf(replacement);
613 	const auto support = (first != -1);
614 	const auto phraseNamed = tr::lng_user_action_choose_sticker(
615 		tr::now,
616 		lt_user,
617 		QString());
618 	const auto firstNamed = phraseNamed.indexOf(replacement);
619 	const auto supportNamed = (firstNamed != -1);
620 
621 	_choosingStickerReplacement.support = (supportNamed && support);
622 	_choosingStickerReplacement.rightIndex = phrase.size() - first;
623 	_choosingStickerReplacement.rightIndexNamed = phraseNamed.size()
624 		- firstNamed;
625 }
626 
supportChoosingStickerReplacement() const627 bool Instance::supportChoosingStickerReplacement() const {
628 	return _choosingStickerReplacement.support;
629 }
630 
rightIndexChoosingStickerReplacement(bool named) const631 int Instance::rightIndexChoosingStickerReplacement(bool named) const {
632 	return named
633 		? _choosingStickerReplacement.rightIndexNamed
634 		: _choosingStickerReplacement.rightIndex;
635 }
636 
637 // SetCallback takes two QByteArrays: key, value.
638 // It is called for all key-value pairs in string.
639 // ResetCallback takes one QByteArray: key.
640 template <typename SetCallback, typename ResetCallback>
HandleString(const MTPLangPackString & string,SetCallback setCallback,ResetCallback resetCallback)641 void HandleString(
642 		const MTPLangPackString &string,
643 		SetCallback setCallback,
644 		ResetCallback resetCallback) {
645 	string.match([&](const MTPDlangPackString &data) {
646 		setCallback(qba(data.vkey()), qba(data.vvalue()));
647 	}, [&](const MTPDlangPackStringPluralized &data) {
648 		const auto key = qba(data.vkey());
649 		setCallback(key + "#zero", data.vzero_value().value_or_empty());
650 		setCallback(key + "#one", data.vone_value().value_or_empty());
651 		setCallback(key + "#two", data.vtwo_value().value_or_empty());
652 		setCallback(key + "#few", data.vfew_value().value_or_empty());
653 		setCallback(key + "#many", data.vmany_value().value_or_empty());
654 		setCallback(key + "#other", qba(data.vother_value()));
655 	}, [&](const MTPDlangPackStringDeleted &data) {
656 		auto key = qba(data.vkey());
657 		resetCallback(key);
658 		const auto postfixes = {
659 			"#zero",
660 			"#one",
661 			"#two",
662 			"#few",
663 			"#many",
664 			"#other"
665 		};
666 		for (const auto plural : postfixes) {
667 			resetCallback(key + plural);
668 		}
669 	});
670 }
671 
applyDifference(Pack pack,const MTPDlangPackDifference & difference)672 void Instance::applyDifference(
673 		Pack pack,
674 		const MTPDlangPackDifference &difference) {
675 	switch (pack) {
676 	case Pack::Current:
677 		applyDifferenceToMe(difference);
678 		break;
679 	case Pack::Base:
680 		Assert(_base != nullptr);
681 		_base->applyDifference(Pack::Current, difference);
682 		break;
683 	default:
684 		Unexpected("Pack in Instance::applyDifference.");
685 	}
686 }
687 
applyDifferenceToMe(const MTPDlangPackDifference & difference)688 void Instance::applyDifferenceToMe(
689 		const MTPDlangPackDifference &difference) {
690 	Expects(LanguageIdOrDefault(_id) == qs(difference.vlang_code()));
691 	Expects(difference.vfrom_version().v <= _version);
692 
693 	_version = difference.vversion().v;
694 	for (const auto &string : difference.vstrings().v) {
695 		HandleString(string, [&](auto &&key, auto &&value) {
696 			applyValue(key, value);
697 		}, [&](auto &&key) {
698 			resetValue(key);
699 		});
700 	}
701 	if (!_derived) {
702 		_updated.fire({});
703 	} else {
704 		_derived->_updated.fire({});
705 	}
706 }
707 
ParseStrings(const MTPVector<MTPLangPackString> & strings)708 std::map<ushort, QString> Instance::ParseStrings(
709 		const MTPVector<MTPLangPackString> &strings) {
710 	auto result = std::map<ushort, QString>();
711 	for (const auto &string : strings.v) {
712 		HandleString(string, [&](auto &&key, auto &&value) {
713 			ParseKeyValue(key, value, [&](ushort key, QString &&value) {
714 				result[key] = std::move(value);
715 			});
716 		}, [&](auto &&key) {
717 			auto keyIndex = GetKeyIndex(QLatin1String(key));
718 			if (keyIndex != kKeysCount) {
719 				result.erase(keyIndex);
720 			}
721 		});
722 	}
723 	return result;
724 }
725 
getNonDefaultValue(const QByteArray & key) const726 QString Instance::getNonDefaultValue(const QByteArray &key) const {
727 	const auto i = _nonDefaultValues.find(key);
728 	return (i != end(_nonDefaultValues))
729 		? QString::fromUtf8(i->second)
730 		: _base
731 		? _base->getNonDefaultValue(key)
732 		: QString();
733 }
734 
applyValue(const QByteArray & key,const QByteArray & value)735 void Instance::applyValue(const QByteArray &key, const QByteArray &value) {
736 	_nonDefaultValues[key] = value;
737 	ParseKeyValue(key, value, [&](ushort key, QString &&value) {
738 		_nonDefaultSet[key] = 1;
739 		if (!_derived) {
740 			_values[key] = std::move(value);
741 		} else if (!_derived->_nonDefaultSet[key]) {
742 			_derived->_values[key] = std::move(value);
743 		}
744 		if (key == tr::lng_send_action_choose_sticker.base
745 			|| key == tr::lng_user_action_choose_sticker.base) {
746 			if (!_derived) {
747 				updateChoosingStickerReplacement();
748 			} else {
749 				_derived->updateChoosingStickerReplacement();
750 			}
751 		}
752 	});
753 }
754 
updatePluralRules()755 void Instance::updatePluralRules() {
756 	if (_pluralId.isEmpty()) {
757 		_pluralId = isCustom()
758 			? PluralCodeForCustom(
759 				_customFilePathAbsolute,
760 				_customFilePathRelative)
761 			: LanguageIdOrDefault(_id);
762 	}
763 	UpdatePluralRules(_pluralId);
764 }
765 
resetValue(const QByteArray & key)766 void Instance::resetValue(const QByteArray &key) {
767 	_nonDefaultValues.erase(key);
768 
769 	const auto keyIndex = GetKeyIndex(QLatin1String(key));
770 	if (keyIndex != kKeysCount) {
771 		_nonDefaultSet[keyIndex] = 0;
772 		if (!_derived) {
773 			const auto base = _base
774 				? _base->getNonDefaultValue(key)
775 				: QString();
776 			_values[keyIndex] = !base.isEmpty()
777 				? base
778 				: GetOriginalValue(keyIndex);
779 		} else if (!_derived->_nonDefaultSet[keyIndex]) {
780 			_derived->_values[keyIndex] = GetOriginalValue(keyIndex);
781 		}
782 		if (keyIndex == tr::lng_send_action_choose_sticker.base
783 			|| keyIndex == tr::lng_user_action_choose_sticker.base) {
784 			if (!_derived) {
785 				updateChoosingStickerReplacement();
786 			} else {
787 				_derived->updateChoosingStickerReplacement();
788 			}
789 		}
790 	}
791 }
792 
GetInstance()793 Instance &GetInstance() {
794 	return Core::App().langpack();
795 }
796 
Id()797 QString Id() {
798 	return GetInstance().id();
799 }
800 
Updated()801 rpl::producer<> Updated() {
802 	return GetInstance().updated();
803 }
804 
GetNonDefaultValue(const QByteArray & key)805 QString GetNonDefaultValue(const QByteArray &key) {
806 	return GetInstance().getNonDefaultValue(key);
807 }
808 
809 namespace details {
810 
Current(ushort key)811 QString Current(ushort key) {
812 	return GetInstance().getValue(key);
813 }
814 
Value(ushort key)815 rpl::producer<QString> Value(ushort key) {
816 	return rpl::single(
817 		Current(key)
818 	) | then(
819 		Updated() | rpl::map([=] { return Current(key); })
820 	);
821 }
822 
IsNonDefaultPlural(ushort keyBase)823 bool IsNonDefaultPlural(ushort keyBase) {
824 	return GetInstance().isNonDefaultPlural(keyBase);
825 }
826 
827 } // namespace details
828 } // namespace Lang
829