1 /* LanguageUtils.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "LanguageUtils.h"
22 
23 #include "Utils/Utils.h"
24 #include "Utils/FileUtils.h"
25 #include "Utils/Algorithm.h"
26 #include "Utils/Settings/Settings.h"
27 #include "Utils/StandardPaths.h"
28 
29 #include <QFile>
30 #include <QRegExp>
31 #include <QDir>
32 #include <QMap>
33 #include <QSettings>
34 #include <QLocale>
35 #include <QLibraryInfo>
36 
37 static bool s_test_mode = false;
38 
39 class LanguageVersionHelper
40 {
41 		QSettings* mSettings = nullptr;
42 
LanguageVersionHelper()43 		LanguageVersionHelper()
44 		{
45 			const auto filepath = (s_test_mode == false)
46 			                ? QDir(Util::translationsPath()).absoluteFilePath("versions")
47 			                : Util::tempPath("versions");
48 
49 			mSettings = new QSettings(filepath, QSettings::NativeFormat);
50 		}
51 
52 	public:
~LanguageVersionHelper()53 		~LanguageVersionHelper()
54 		{
55 			delete mSettings;
56 		}
57 
instance()58 		static LanguageVersionHelper* instance()
59 		{
60 			static LanguageVersionHelper hlp;
61 			return &hlp;
62 		}
63 
setLanguageVersion(const QString & languageCode,const QString & version)64 		void setLanguageVersion(const QString& languageCode, const QString& version)
65 		{
66 			// never allow to downgrade a downloaded version
67 			if(isOutdated(languageCode))
68 			{
69 				mSettings->setValue(languageCode, version);
70 			}
71 		}
72 
getLanguageVersion(const QString & languageCode)73 		QString getLanguageVersion(const QString& languageCode)
74 		{
75 			return mSettings->value(languageCode).toString();
76 		}
77 
isOutdated(const QString & languageCode)78 		bool isOutdated(const QString& languageCode)
79 		{
80 			QString lv = getLanguageVersion(languageCode);
81 			QString pv = GetSetting(::Set::Player_Version);
82 
83 			if(lv.isEmpty())
84 			{
85 				return true;
86 			}
87 
88 			return (lv < pv);
89 		}
90 };
91 
92 namespace Language = Util::Language;
93 
checkLanguageCode(const QString & languageCode)94 static bool checkLanguageCode(const QString& languageCode)
95 {
96 	QRegExp re("^[a-z]{2}(_[A-Z]{2})?(\\.[A-Z0-9\\-]+[0-9])?$");
97 	int idx = re.indexIn(languageCode);
98 
99 	return (idx == 0);
100 }
101 
getSharePath(const QString & languageCode)102 QString Language::getSharePath(const QString& languageCode)
103 {
104 	if(!checkLanguageCode(languageCode))
105 	{
106 		return QString();
107 	}
108 	return Util::sharePath("translations") + "/" + QString("sayonara_lang_%1.qm").arg(languageCode);
109 }
110 
getFtpPath(const QString & languageCode)111 QString Language::getFtpPath(const QString& languageCode)
112 {
113 	if(!checkLanguageCode(languageCode))
114 	{
115 		return QString();
116 	}
117 	return QString("ftp://sayonara-player.com/translation/sayonara_lang_%1.qm").arg(languageCode);
118 }
119 
getHttpPath(const QString & languageCode)120 QString Language::getHttpPath(const QString& languageCode)
121 {
122 	if(!checkLanguageCode(languageCode))
123 	{
124 		return QString();
125 	}
126 
127 	return QString("https://sayonara-player.com/files/translation/sayonara_lang_%1.qm").arg(
128 		languageCode);
129 }
130 
getChecksumFtpPath()131 QString Language::getChecksumFtpPath()
132 {
133 	return "ftp://sayonara-player.com/translation/checksum";
134 }
135 
getChecksumHttpPath()136 QString Language::getChecksumHttpPath()
137 {
138 	return "https://sayonara-player.com/files/translation/checksum";
139 }
140 
getHomeTargetPath(const QString & languageCode)141 QString Language::getHomeTargetPath(const QString& languageCode)
142 {
143 	if(!checkLanguageCode(languageCode))
144 	{
145 		return QString();
146 	}
147 
148 	const auto translationDir = Util::translationsPath();
149 	const auto languagePath = QString("%1/sayonara_lang_%2.qm")
150 		.arg(translationDir)
151 		.arg(languageCode);
152 
153 	return languagePath;
154 }
155 
isOutdated(const QString & languageCode)156 bool Language::isOutdated(const QString& languageCode)
157 {
158 	return LanguageVersionHelper::instance()->isOutdated(languageCode);
159 }
160 
getSimilarLanguage4(const QString & languageCode)161 QString Language::getSimilarLanguage4(const QString& languageCode)
162 {
163 	if(!checkLanguageCode(languageCode))
164 	{
165 		return QString();
166 	}
167 
168 	const auto twoLetter = languageCode.left(2);
169 	const auto translationsPaths = QStringList
170 		{
171 			Util::translationsPath(),
172 			Util::sharePath("translations")
173 		};
174 
175 	for(const auto& translationPath : translationsPaths)
176 	{
177 		if(!Util::File::exists(translationPath))
178 		{
179 			continue;
180 		}
181 
182 		const auto dir = QDir(translationPath);
183 		const auto entries = dir.entryList(QDir::Files);
184 		const auto re = QRegExp("sayonara_lang_([a-z]{2})_.+qm");
185 
186 		for(const auto& entry : entries)
187 		{
188 			if(re.indexIn(entry) < 0)
189 			{
190 				continue;
191 			}
192 
193 			const auto entryTwoLetter = re.cap(1);
194 			if(entryTwoLetter == twoLetter)
195 			{
196 				return translationPath + "/" + entry;
197 			}
198 		}
199 	}
200 
201 	return QString();
202 }
203 
getUsedLanguageFile(const QString & languageCode)204 QString Language::getUsedLanguageFile(const QString& languageCode)
205 {
206 	if(!checkLanguageCode(languageCode))
207 	{
208 		return QString();
209 	}
210 
211 	{ // check if home path or share path version is better or exists
212 		if(isOutdated(languageCode)) // not available or older than in share path
213 		{
214 			const auto languageInSharePath = getSharePath(languageCode);
215 			if(Util::File::exists(languageInSharePath))
216 			{
217 				return languageInSharePath;
218 			}
219 		}
220 
221 		if(Util::File::exists(getHomeTargetPath(languageCode)))
222 		{
223 			return getHomeTargetPath(languageCode);
224 		}
225 	}
226 
227 	{ // try to find from other region
228 		return getSimilarLanguage4(languageCode);
229 	}
230 }
231 
extractLanguageCode(const QString & languageFile)232 QString Language::extractLanguageCode(const QString& languageFile)
233 {
234 	const auto re = QRegExp
235 		(
236 			".*sayonara_lang_"
237 			"([a-z]{2}(_[A-Z]{2})?(\\.[A-Z0-9\\-]+[0-9])?)\\.(ts|qm)$"
238 		);
239 
240 	const auto idx = re.indexIn(languageFile);
241 	if(idx < 0)
242 	{
243 		return QString();
244 	}
245 
246 	const auto languageCode = re.cap(1);
247 	if(!checkLanguageCode(languageCode))
248 	{
249 		return QString();
250 	}
251 
252 	return languageCode;
253 }
254 
getIconPath(const QString & languageCode)255 QString Language::getIconPath(const QString& languageCode)
256 {
257 	if(!checkLanguageCode(languageCode))
258 	{
259 		return QString();
260 	}
261 
262 	auto filename = Util::sharePath("translations/icons/%1.png").arg(languageCode);
263 	if(!QFile(filename).exists())
264 	{
265 		filename = Util::sharePath("translations/icons/%1.png").arg(languageCode.left(2));
266 		if(!QFile(filename).exists())
267 		{
268 			return QString();
269 		}
270 	}
271 
272 	return filename;
273 }
274 
getChecksum(const QString & languageCode)275 QString Language::getChecksum(const QString& languageCode)
276 {
277 	if(!checkLanguageCode(languageCode))
278 	{
279 		return QString();
280 	}
281 
282 	const auto path = getUsedLanguageFile(languageCode);
283 	return QString::fromUtf8(Util::File::getMD5Sum(path));
284 }
285 
importLanguageFile(const QString & filename)286 bool Language::importLanguageFile(const QString& filename)
287 {
288 	const auto languageCode = extractLanguageCode(filename);
289 	if(languageCode.isEmpty())
290 	{
291 		return false;
292 	}
293 
294 	const auto targetPath = Language::getHomeTargetPath(languageCode);
295 	auto file = QFile(filename);
296 	bool success = file.copy(targetPath);
297 	if(success)
298 	{
299 		updateLanguageVersion(languageCode);
300 	}
301 
302 	return success;
303 }
304 
getLanguageVersion(const QString & languageCode)305 QString Language::getLanguageVersion(const QString& languageCode)
306 {
307 	if(!Util::File::exists(getHomeTargetPath(languageCode)))
308 	{
309 		LanguageVersionHelper::instance()->setLanguageVersion(languageCode, QString());
310 		return QString();
311 	}
312 
313 	return LanguageVersionHelper::instance()->getLanguageVersion(languageCode);
314 }
315 
updateLanguageVersion(const QString & languageCode)316 void Language::updateLanguageVersion(const QString& languageCode)
317 {
318 	const auto version = Util::File::exists(getHomeTargetPath(languageCode))
319 	                     ? GetSetting(::Set::Player_Version)
320 	                     : QString();
321 
322 	LanguageVersionHelper::instance()->setLanguageVersion(languageCode, version);
323 }
324 
325 #ifdef SAYONARA_WITH_TESTS
326 
setTestMode()327 void Language::setTestMode()
328 {
329 	s_test_mode = true;
330 }
331 
setLanguageVersion(const QString & languageCode,const QString & version)332 void Language::setLanguageVersion(const QString& languageCode, const QString& version)
333 {
334 	LanguageVersionHelper::instance()->setLanguageVersion(languageCode, version);
335 }
336 
337 #endif
338 
getCurrentLocale()339 QLocale Language::getCurrentLocale()
340 {
341 	const auto languageCode = GetSetting(::Set::Player_Language);
342 	return QLocale(languageCode);
343 }
344 
getCurrentQtTranslationPaths()345 QStringList Language::getCurrentQtTranslationPaths()
346 {
347 	const auto languageCode = GetSetting(::Set::Player_Language);
348 	if(languageCode.size() < 2)
349 	{
350 		return QStringList();
351 	}
352 
353 	const auto twoLetter = languageCode.left(2);
354 	const auto filePrefixes = QStringList
355 		{
356 			"qt",
357 			"qtbase",
358 			"qtlocation"
359 		};
360 
361 	QStringList paths;
362 	Util::Algorithm::transform(filePrefixes, paths, [twoLetter](const QString& prefix) {
363 		return QString("%1/%2_%3.qm")
364 			.arg(QLibraryInfo::location(QLibraryInfo::TranslationsPath))
365 			.arg(prefix)
366 			.arg(twoLetter);
367 	});
368 
369 	QStringList ret;
370 	Util::Algorithm::copyIf(paths, ret, [](const auto& path) {
371 		return (Util::File::exists(path));
372 	});
373 
374 	return ret;
375 }
376 
377 
convertOldLanguage(const QString & oldLanguageName)378 QString Language::convertOldLanguage(const QString& oldLanguageName)
379 {
380 	const auto languages = availableLanguages().keys();
381 	const auto languageCode = extractLanguageCode(oldLanguageName);
382 	if(languageCode.size() != 2){
383 		return "en";
384 	}
385 
386 	const auto it = Util::Algorithm::find(languages, [&](const auto& language){
387 		return (language.startsWith(languageCode) && (languages.size() > 4));
388 	});
389 
390 	return (it != languages.end()) ? *it : "en";
391 }
392 
availableLanguages()393 QMap<QString, QLocale> Language::availableLanguages()
394 {
395 	const auto directories = QList<QDir>
396 		{
397 			QDir(Util::translationsSharePath()),
398 			QDir(Util::translationsPath())
399 		};
400 
401 	QMap<QString, QLocale> ret;
402 	for(const auto& directory : directories)
403 	{
404 		if(!directory.exists()) {
405 			continue;
406 		}
407 
408 		const auto entries = directory.entryList(QStringList{"*.qm"}, QDir::Files);
409 		for(const auto& entry : entries)
410 		{
411 			const auto key = extractLanguageCode(entry);
412 			if(!key.isEmpty()) {
413 				ret[key] = QLocale(key);
414 			}
415 		}
416 	}
417 
418 	ret.remove("en_US");
419 
420 	return ret;
421 }