diff --git a/.gitignore b/.gitignore
index 7f53c4f..8168521 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,4 @@
*.qm
*.jar
*.zip
-*.exe
translations/
diff --git a/TranslationChecker.exe b/TranslationChecker.exe
new file mode 100644
index 0000000..3bb2231
Binary files /dev/null and b/TranslationChecker.exe differ
diff --git a/TranslationChecker/.gitignore b/TranslationChecker/.gitignore
new file mode 100644
index 0000000..2e6ffad
--- /dev/null
+++ b/TranslationChecker/.gitignore
@@ -0,0 +1,10 @@
+.vs/
+bin/
+obj/
+Release/
+Debug/
+x64/
+*.aps
+*.vcxproj.user
+packages/
+*.user
diff --git a/TranslationChecker/TranslationChecker.sln b/TranslationChecker/TranslationChecker.sln
new file mode 100644
index 0000000..0161a02
--- /dev/null
+++ b/TranslationChecker/TranslationChecker.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32616.157
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TranslationChecker", "TranslationChecker\TranslationChecker.csproj", "{7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {ED6D654E-2A4F-46AD-9001-5E0E306691A6}
+ EndGlobalSection
+EndGlobal
diff --git a/TranslationChecker/TranslationChecker/App.config b/TranslationChecker/TranslationChecker/App.config
new file mode 100644
index 0000000..5754728
--- /dev/null
+++ b/TranslationChecker/TranslationChecker/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TranslationChecker/TranslationChecker/LanguageCodes.cs b/TranslationChecker/TranslationChecker/LanguageCodes.cs
new file mode 100644
index 0000000..7f08f9b
--- /dev/null
+++ b/TranslationChecker/TranslationChecker/LanguageCodes.cs
@@ -0,0 +1,337 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TranslationChecker
+{
+ static class LanguageCodes
+ {
+ public static string GetLanguageName(string code)
+ {
+ if (Values.ContainsKey(code))
+ return Values[code];
+ return $"Unknown, {code}";
+ }
+
+ // https://developer.crowdin.com/language-codes/
+ public static Dictionary Values = new Dictionary
+ {
+ { "ach", "Acholi" },
+ { "aa", "Afar" },
+ { "af", "Afrikaans" },
+ { "ak", "Akan" },
+ { "tw", "Akan, Twi" },
+ { "sq", "Albanian" },
+ { "am", "Amharic" },
+ { "ar", "Arabic" },
+ { "ar-BH", "Arabic, Bahrain" },
+ { "ar-EG", "Arabic, Egypt" },
+ { "ar-SA", "Arabic, Saudi Arabia" },
+ { "ar-YE", "Arabic, Yemen" },
+ { "an", "Aragonese" },
+ { "hy-AM", "Armenian" },
+ { "frp", "Arpitan" },
+ { "as", "Assamese" },
+ { "ast", "Asturian" },
+ { "tay", "Atayal" },
+ { "av", "Avaric" },
+ { "ae", "Avestan" },
+ { "ay", "Aymara" },
+ { "az", "Azerbaijani" },
+ { "ban", "Balinese" },
+ { "bal", "Balochi" },
+ { "bm", "Bambara" },
+ { "ba", "Bashkir" },
+ { "eu", "Basque" },
+ { "be", "Belarusian" },
+ { "bn", "Bengali" },
+ { "bn-IN", "Bengali, India" },
+ { "ber", "Berber" },
+ { "bh", "Bihari" },
+ { "bfo", "Birifor" },
+ { "bi", "Bislama" },
+ { "bs", "Bosnian" },
+ { "br-FR", "Breton" },
+ { "bg", "Bulgarian" },
+ { "my", "Burmese" },
+ { "ca", "Catalan" },
+ { "ceb", "Cebuano" },
+ { "ch", "Chamorro" },
+ { "ce", "Chechen" },
+ { "chr", "Cherokee" },
+ { "ny", "Chewa" },
+ { "zh-CN", "Chinese Simplified" },
+ { "zh-TW", "Chinese Traditional" },
+ { "zh-HK", "Chinese Traditional, Hong Kong" },
+ { "zh-MO", "Chinese Traditional, Macau" },
+ { "zh-SG", "Chinese Traditional, Singapore" },
+ { "cv", "Chuvash" },
+ { "kw", "Cornish" },
+ { "co", "Corsican" },
+ { "cr", "Cree" },
+ { "hr", "Croatian" },
+ { "cs", "Czech" },
+ { "da", "Danish" },
+ { "fa-AF", "Dari" },
+ { "dv", "Dhivehi" },
+ { "nl", "Dutch" },
+ { "nl-BE", "Dutch, Belgium" },
+ { "nl-SR", "Dutch, Suriname" },
+ { "dz", "Dzongkha" },
+ { "en", "English" },
+ { "en-UD", "English (upside down)" },
+ { "en-AR", "English, Arabia" },
+ { "en-AU", "English, Australia" },
+ { "en-BZ", "English, Belize" },
+ { "en-CA", "English, Canada" },
+ { "en-CB", "English, Caribbean" },
+ { "en-CN", "English, China" },
+ { "en-DK", "English, Denmark" },
+ { "en-HK", "English, Hong Kong" },
+ { "en-IN", "English, India" },
+ { "en-ID", "English, Indonesia" },
+ { "en-IE", "English, Ireland" },
+ { "en-JM", "English, Jamaica" },
+ { "en-JA", "English, Japan" },
+ { "en-MY", "English, Malaysia" },
+ { "en-NZ", "English, New Zealand" },
+ { "en-NO", "English, Norway" },
+ { "en-PH", "English, Philippines" },
+ { "en-PR", "English, Puerto Rico" },
+ { "en-SG", "English, Singapore" },
+ { "en-ZA", "English, South Africa" },
+ { "en-SE", "English, Sweden" },
+ { "en-GB", "English, United Kingdom" },
+ { "en-US", "English, United States" },
+ { "en-ZW", "English, Zimbabwe" },
+ { "eo", "Esperanto" },
+ { "et", "Estonian" },
+ { "ee", "Ewe" },
+ { "fo", "Faroese" },
+ { "fj", "Fijian" },
+ { "fil", "Filipino" },
+ { "fi", "Finnish" },
+ { "vls-BE", "Flemish" },
+ { "fra-DE", "Franconian" },
+ { "fr", "French" },
+ { "fr-BE", "French, Belgium" },
+ { "fr-CA", "French, Canada" },
+ { "fr-LU", "French, Luxembourg" },
+ { "fr-QC", "French, Quebec" },
+ { "fr-CH", "French, Switzerland" },
+ { "fy-NL", "Frisian" },
+ { "fur-IT", "Friulian" },
+ { "ff", "Fula" },
+ { "gaa", "Ga" },
+ { "gl", "Galician" },
+ { "ka", "Georgian" },
+ { "de", "German" },
+ { "de-AT", "German, Austria" },
+ { "de-BE", "German, Belgium" },
+ { "de-LI", "German, Liechtenstein" },
+ { "de-LU", "German, Luxembourg" },
+ { "de-CH", "German, Switzerland" },
+ { "got", "Gothic" },
+ { "el", "Greek" },
+ { "el-CY", "Greek, Cyprus" },
+ { "kl", "Greenlandic" },
+ { "gn", "Guarani" },
+ { "gu-IN", "Gujarati" },
+ { "ht", "Haitian Creole" },
+ { "ha", "Hausa" },
+ { "haw", "Hawaiian" },
+ { "he", "Hebrew" },
+ { "hz", "Herero" },
+ { "hil", "Hiligaynon" },
+ { "hi", "Hindi" },
+ { "ho", "Hiri Motu" },
+ { "hmn", "Hmong" },
+ { "hu", "Hungarian" },
+ { "is", "Icelandic" },
+ { "ido", "Ido" },
+ { "ig", "Igbo" },
+ { "ilo", "Ilokano" },
+ { "id", "Indonesian" },
+ { "iu", "Inuktitut" },
+ { "ga-IE", "Irish" },
+ { "it", "Italian" },
+ { "it-CH", "Italian, Switzerland" },
+ { "ja", "Japanese" },
+ { "jv", "Javanese" },
+ { "quc", "K'iche'" },
+ { "kab", "Kabyle" },
+ { "kn", "Kannada" },
+ { "pam", "Kapampangan" },
+ { "ks", "Kashmiri" },
+ { "ks-PK", "Kashmiri, Pakistan" },
+ { "csb", "Kashubian" },
+ { "kk", "Kazakh" },
+ { "km", "Khmer" },
+ { "rw", "Kinyarwanda" },
+ { "tlh-AA", "Klingon" },
+ { "kv", "Komi" },
+ { "kg", "Kongo" },
+ { "kok", "Konkani" },
+ { "ko", "Korean" },
+ { "ku", "Kurdish" },
+ { "kmr", "Kurmanji (Kurdish)" },
+ { "kj", "Kwanyama" },
+ { "ky", "Kyrgyz" },
+ { "lol", "LOLCAT" },
+ { "lo", "Lao" },
+ { "la-LA", "Latin" },
+ { "lv", "Latvian" },
+ { "lij", "Ligurian" },
+ { "li", "Limburgish" },
+ { "ln", "Lingala" },
+ { "lt", "Lithuanian" },
+ { "jbo", "Lojban" },
+ { "nds", "Low German" },
+ { "dsb-DE", "Lower Sorbian" },
+ { "lg", "Luganda" },
+ { "luy", "Luhya" },
+ { "lb", "Luxembourgish" },
+ { "mk", "Macedonian" },
+ { "mai", "Maithili" },
+ { "mg", "Malagasy" },
+ { "ms", "Malay" },
+ { "ms-BN", "Malay, Brunei" },
+ { "ml-IN", "Malayalam" },
+ { "mt", "Maltese" },
+ { "gv", "Manx" },
+ { "mi", "Maori" },
+ { "arn", "Mapudungun" },
+ { "mr", "Marathi" },
+ { "mh", "Marshallese" },
+ { "moh", "Mohawk" },
+ { "mn", "Mongolian" },
+ { "sr-Cyrl-ME", "Montenegrin (Cyrillic)" },
+ { "me", "Montenegrin (Latin)" },
+ { "mos", "Mossi" },
+ { "na", "Nauru" },
+ { "ng", "Ndonga" },
+ { "ne-NP", "Nepali" },
+ { "ne-IN", "Nepali, India" },
+ { "pcm", "Nigerian Pidgin" },
+ { "se", "Northern Sami" },
+ { "nso", "Northern Sotho" },
+ { "no", "Norwegian" },
+ { "nb", "Norwegian Bokmal" },
+ { "nn-NO", "Norwegian Nynorsk" },
+ { "oc", "Occitan" },
+ { "or", "Odia" },
+ { "oj", "Ojibwe" },
+ { "om", "Oromo" },
+ { "os", "Ossetian" },
+ { "pi", "Pali" },
+ { "pap", "Papiamento" },
+ { "ps", "Pashto" },
+ { "fa", "Persian" },
+ { "en-PT", "Pirate English" },
+ { "pl", "Polish" },
+ { "pt-PT", "Portuguese" },
+ { "pt-BR", "Portuguese, Brazilian" },
+ { "pa-IN", "Punjabi" },
+ { "pa-PK", "Punjabi, Pakistan" },
+ { "qu", "Quechua" },
+ { "qya-AA", "Quenya" },
+ { "ro", "Romanian" },
+ { "rm-CH", "Romansh" },
+ { "rn", "Rundi" },
+ { "ru", "Russian" },
+ { "ru-BY", "Russian, Belarus" },
+ { "ru-MD", "Russian, Moldova" },
+ { "ru-UA", "Russian, Ukraine" },
+ { "ry-UA", "Rusyn" },
+ { "sah", "Sakha" },
+ { "sg", "Sango" },
+ { "sa", "Sanskrit" },
+ { "sat", "Santali" },
+ { "sc", "Sardinian" },
+ { "sco", "Scots" },
+ { "gd", "Scottish Gaelic" },
+ { "sr", "Serbian (Cyrillic)" },
+ { "sr-CS", "Serbian (Latin)" },
+ { "sh", "Serbo-Croatian" },
+ { "crs", "Seychellois Creole" },
+ { "sn", "Shona" },
+ { "ii", "Sichuan Yi" },
+ { "sd", "Sindhi" },
+ { "si-LK", "Sinhala" },
+ { "sk", "Slovak" },
+ { "sl", "Slovenian" },
+ { "so", "Somali" },
+ { "son", "Songhay" },
+ { "ckb", "Sorani (Kurdish)" },
+ { "nr", "Southern Ndebele" },
+ { "sma", "Southern Sami" },
+ { "st", "Southern Sotho" },
+ { "es-ES", "Spanish" },
+ { "es-EM", "Spanish (Modern)" },
+ { "es-AR", "Spanish, Argentina" },
+ { "es-BO", "Spanish, Bolivia" },
+ { "es-CL", "Spanish, Chile" },
+ { "es-CO", "Spanish, Colombia" },
+ { "es-CR", "Spanish, Costa Rica" },
+ { "es-DO", "Spanish, Dominican Republic" },
+ { "es-EC", "Spanish, Ecuador" },
+ { "es-SV", "Spanish, El Salvador" },
+ { "es-GT", "Spanish, Guatemala" },
+ { "es-HN", "Spanish, Honduras" },
+ { "es-419", "Spanish, Latin America" },
+ { "es-MX", "Spanish, Mexico" },
+ { "es-NI", "Spanish, Nicaragua" },
+ { "es-PA", "Spanish, Panama" },
+ { "es-PY", "Spanish, Paraguay" },
+ { "es-PE", "Spanish, Peru" },
+ { "es-PR", "Spanish, Puerto Rico" },
+ { "es-US", "Spanish, United States" },
+ { "es-UY", "Spanish, Uruguay" },
+ { "es-VE", "Spanish, Venezuela" },
+ { "su", "Sundanese" },
+ { "sw", "Swahili" },
+ { "sw-KE", "Swahili, Kenya" },
+ { "sw-TZ", "Swahili, Tanzania" },
+ { "ss", "Swati" },
+ { "sv-SE", "Swedish" },
+ { "sv-FI", "Swedish, Finland" },
+ { "syc", "Syriac" },
+ { "tl", "Tagalog" },
+ { "ty", "Tahitian" },
+ { "tg", "Tajik" },
+ { "tzl", "Talossan" },
+ { "ta", "Tamil" },
+ { "tt-RU", "Tatar" },
+ { "te", "Telugu" },
+ { "kdh", "Tem (Kotokoli)" },
+ { "th", "Thai" },
+ { "bo-BT", "Tibetan" },
+ { "ti", "Tigrinya" },
+ { "ts", "Tsonga" },
+ { "tn", "Tswana" },
+ { "tr", "Turkish" },
+ { "tr-CY", "Turkish, Cyprus" },
+ { "tk", "Turkmen" },
+ { "uk", "Ukrainian" },
+ { "hsb-DE", "Upper Sorbian" },
+ { "ur-IN", "Urdu (India)" },
+ { "ur-PK", "Urdu (Pakistan)" },
+ { "ug", "Uyghur" },
+ { "uz", "Uzbek" },
+ { "val-ES", "Valencian" },
+ { "ve", "Venda" },
+ { "vec", "Venetian" },
+ { "vi", "Vietnamese" },
+ { "wa", "Walloon" },
+ { "cy", "Welsh" },
+ { "wo", "Wolof" },
+ { "xh", "Xhosa" },
+ { "yi", "Yiddish" },
+ { "yo", "Yoruba" },
+ { "zea", "Zeelandic" },
+ { "zu", "Zulu" },
+ };
+ }
+}
diff --git a/TranslationChecker/TranslationChecker/Program.cs b/TranslationChecker/TranslationChecker/Program.cs
new file mode 100644
index 0000000..c7930aa
--- /dev/null
+++ b/TranslationChecker/TranslationChecker/Program.cs
@@ -0,0 +1,296 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Serialization;
+
+namespace TranslationChecker
+{
+ // https://json2csharp.com/code-converters/xml-to-csharp
+
+ [XmlRoot(ElementName = "translation")]
+ public class Translation
+ {
+ [XmlAttribute(AttributeName = "type")]
+ public string Type { get; set; }
+
+ [XmlElement(ElementName = "numerusform")]
+ public List Numerusforms { get; set; }
+
+ [XmlText]
+ public string Text { get; set; }
+ }
+
+ [XmlRoot(ElementName = "location")]
+ public class Location
+ {
+ [XmlAttribute(AttributeName = "filename")]
+ public string Filename { get; set; }
+
+ [XmlAttribute(AttributeName = "line")]
+ public int Line { get; set; }
+ }
+
+ [XmlRoot(ElementName = "message")]
+ public class Message
+ {
+ [XmlAttribute(AttributeName = "numerus")]
+ public string Numerus { get; set; }
+
+ [XmlElement(ElementName = "location")]
+ public List Locations { get; set; }
+
+ [XmlElement(ElementName = "source")]
+ public string Source { get; set; }
+
+ [XmlElement(ElementName = "translation")]
+ public Translation Translation { get; set; }
+ }
+
+ [XmlRoot(ElementName = "context")]
+ public class Context
+ {
+ [XmlElement(ElementName = "name")]
+ public string Name { get; set; }
+
+ [XmlElement(ElementName = "message")]
+ public List Messages { get; set; }
+ }
+
+ [XmlRoot(ElementName = "TS")]
+ public class QtTranslations
+ {
+ [XmlElement(ElementName = "context")]
+ public List Contexts { get; set; }
+
+ [XmlAttribute(AttributeName = "version")]
+ public string Version { get; set; }
+
+ [XmlAttribute(AttributeName = "language")]
+ public string Language { get; set; }
+
+ [XmlAttribute(AttributeName = "sourcelanguage")]
+ public string Sourcelanguage { get; set; }
+ }
+
+ public class Utf8StringWriter : StringWriter
+ {
+ public override Encoding Encoding { get { return new UTF8Encoding(false); } }
+ }
+
+ internal class Program
+ {
+ ///
+ /// Checks if the file serializes back from deserialized form without any differences.
+ ///
+ static bool CheckSerialize(string tsFile)
+ {
+ var utf8 = new UTF8Encoding(false);
+ var xml = File.ReadAllText(tsFile, utf8);
+
+ XmlSerializer serializer = new XmlSerializer(typeof(QtTranslations));
+ using (var reader = new StringReader(xml))
+ {
+ var test = (QtTranslations)serializer.Deserialize(reader);
+ using (var writer = new Utf8StringWriter())
+ using (var xw = XmlWriter.Create(writer, new XmlWriterSettings
+ {
+ Indent = true,
+ }))
+ {
+ xw.WriteDocType("TS", null, null, null);
+ var ns = new XmlSerializerNamespaces();
+ ns.Add("", "");
+ serializer.Serialize(xw, test, ns);
+ var xml2 = writer.ToString().Replace(" />", "/>").Replace("", "").Replace("\r", "") + "\n";
+ if (xml2 != xml)
+ {
+ File.WriteAllText(tsFile + ".serialized", xml2, utf8);
+ Console.WriteLine($"Serialization error with file: {tsFile}");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // References:
+ // - https://en.cppreference.com/w/cpp/io/c/fprintf
+ // - https://docs.microsoft.com/en-us/cpp/c-runtime-library/format-specification-syntax-printf-and-wprintf-functions?view=msvc-170#flags
+ static Regex FormatSpecifierRegex = new Regex(@"(%[-+ #]*(\d+|\*)?(.\d+|.\*)?(hh|h|l|ll|j|z|t|L|I|I32|I64|w)?[csdioxXufFeEaAgGp]|%n|%%)");
+
+ static Regex QtArgumentRegex = new Regex(@"(%\d+)");
+
+ static bool CheckTranslation(string original, string translation)
+ {
+ // This checks for context menu keys, disable for now because there is a lot of noise
+#if false
+ if (original.Count(c => c == '&') != translation.Count(c => c == '&'))
+ return false;
+#endif
+
+ // Check if original has the same format strings as the translation
+ var originalMatches = FormatSpecifierRegex.Matches(original);
+ var translationMatches = FormatSpecifierRegex.Matches(translation);
+ if (originalMatches.Count != translationMatches.Count)
+ {
+ return false;
+ }
+ for (var i = 0; i < originalMatches.Count; i++)
+ {
+ var originalMatch = originalMatches[i];
+ var translationMatch = translationMatches[i];
+ if (originalMatch.ToString() != translationMatch.ToString())
+ return false;
+ }
+ if (originalMatches.Count == 0)
+ {
+ originalMatches = QtArgumentRegex.Matches(original);
+ translationMatches = QtArgumentRegex.Matches(translation);
+ string SortedArgs(MatchCollection matches)
+ {
+ var sorted = new List();
+ for (var i = 0; i < matches.Count; i++)
+ sorted.Add(matches[i].ToString());
+ sorted.Sort();
+ return string.Join(" ", sorted);
+ }
+ return SortedArgs(originalMatches) == SortedArgs(translationMatches);
+ }
+ return true;
+ }
+
+ static string FixTranslation(string original, string translation)
+ {
+ // TODO: try to fix almost-correct format strings (like %hello -> %shello)
+ return original;
+ }
+
+ static int ErrorCount = 0;
+
+ static bool CheckFile(string tsFile, bool fix)
+ {
+ if (!CheckSerialize(tsFile))
+ {
+ return false;
+ }
+
+ var utf8 = new UTF8Encoding(false);
+ var xml = File.ReadAllText(tsFile, utf8);
+ var success = true;
+
+ void ReportError(Message message, string translation)
+ {
+ var location = message.Locations.First();
+ Console.WriteLine($" Format string error ({location.Filename}:{location.Line})\n Source:\n '{message.Source}'\n Translation:\n '{translation}'");
+ success = false;
+ ErrorCount++;
+ }
+
+ XmlSerializer serializer = new XmlSerializer(typeof(QtTranslations));
+ using (var reader = new StringReader(xml))
+ {
+ var ts = (QtTranslations)serializer.Deserialize(reader);
+ var url = $"https://crowdin.com/translate/x64dbg/1/en-{ts.Language.ToLower().Replace("-", "")}";
+ Console.WriteLine($"Checking {tsFile} ({LanguageCodes.GetLanguageName(ts.Language)}) => {url}");
+
+ foreach (var context in ts.Contexts)
+ {
+ foreach (var message in context.Messages)
+ {
+ var original = message.Source;
+ if (message.Translation.Type == "unfinished")
+ {
+ continue;
+ }
+
+ if (message.Numerus == "yes")
+ {
+ for (var i = 0; i < message.Translation.Numerusforms.Count; i++)
+ {
+ var translation = message.Translation.Numerusforms[i];
+ if (!CheckTranslation(original, translation))
+ {
+ ReportError(message, translation);
+ if (fix)
+ message.Translation.Numerusforms[i] = FixTranslation(original, translation);
+ }
+ }
+ }
+ else
+ {
+ var translation = message.Translation.Text;
+ if (!CheckTranslation(original, translation))
+ {
+ ReportError(message, translation);
+ if (fix)
+ message.Translation.Text = FixTranslation(original, translation);
+ }
+ }
+ }
+ }
+
+ if (!success && fix)
+ {
+ using (var writer = new Utf8StringWriter())
+ using (var xw = XmlWriter.Create(writer, new XmlWriterSettings
+ {
+ Indent = true,
+ }))
+ {
+ xw.WriteDocType("TS", null, null, null);
+ var ns = new XmlSerializerNamespaces();
+ ns.Add("", "");
+ serializer.Serialize(xw, ts, ns);
+ var xml2 = writer.ToString().Replace(" />", "/>").Replace("", "").Replace("\r", "") + "\n";
+ File.WriteAllText(tsFile, xml2, utf8);
+ }
+ }
+ }
+
+ return success;
+ }
+
+ static int Main(string[] args)
+ {
+ Console.OutputEncoding = Encoding.UTF8;
+
+ if (args.Length < 1)
+ {
+ Console.WriteLine("Usage: TranslationChecker x64dbg.ts [--fix]");
+ return 1;
+ }
+
+ var fix = false;
+ var folder = false;
+ for (var i = 1; i < args.Length; i++)
+ {
+ if (args[i] == "--fix")
+ fix = true;
+ else if (args[i] == "--folder")
+ folder = true;
+ }
+
+ var success = true;
+ if (folder)
+ {
+ foreach (var tsFile in Directory.EnumerateFiles(args[0], "*.ts", SearchOption.AllDirectories))
+ {
+ if (!CheckFile(tsFile, fix))
+ success = false;
+ }
+ }
+ else
+ {
+ success = CheckFile(args[0], fix);
+ }
+
+ Console.WriteLine($"\nTotal errors: {ErrorCount}");
+ return success ? 0 : 1;
+ }
+ }
+}
diff --git a/TranslationChecker/TranslationChecker/Properties/AssemblyInfo.cs b/TranslationChecker/TranslationChecker/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e5d9432
--- /dev/null
+++ b/TranslationChecker/TranslationChecker/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("TranslationChecker")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("TranslationChecker")]
+[assembly: AssemblyCopyright("Copyright © 2022")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("7d7eec8b-34ff-43e7-ac2b-cb0a68334cfa")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/TranslationChecker/TranslationChecker/TranslationChecker.csproj b/TranslationChecker/TranslationChecker/TranslationChecker.csproj
new file mode 100644
index 0000000..37a031d
--- /dev/null
+++ b/TranslationChecker/TranslationChecker/TranslationChecker.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {7D7EEC8B-34FF-43E7-AC2B-CB0A68334CFA}
+ Exe
+ TranslationChecker
+ TranslationChecker
+ v4.7.2
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/crowdin-sync.bat b/crowdin-sync.bat
index 6cf740c..87c9198 100644
--- a/crowdin-sync.bat
+++ b/crowdin-sync.bat
@@ -1,17 +1,22 @@
@echo off
if "%QT64PATH%"=="" set QT64PATH=c:\Qt\qt-5.6.2-x64-msvc2013\5.6\msvc2013_64\bin
SET PATH=%PATH%;%QT64PATH%;c:\Program Files (x86)\7-Zip
-del /S /Q *.qm
-curl -k https://api.crowdin.com/api/project/x64dbg/export?key=%CROWDIN_API_KEY%
-curl -k -o translations.zip https://api.crowdin.com/api/project/x64dbg/download/all.zip?key=%CROWDIN_API_KEY%
-rmdir /S /Q translations
-7z x -otranslations translations.zip
+del /S /Q *.qm >nul 2>&1
+curl -s -k https://api.crowdin.com/api/project/x64dbg/export?key=%CROWDIN_API_KEY%
+curl -s -k -o translations.zip https://api.crowdin.com/api/project/x64dbg/download/all.zip?key=%CROWDIN_API_KEY%
+rmdir /S /Q translations >nul 2>&1
+7z x -otranslations translations.zip >nul 2>&1
+TranslationChecker.exe translations --folder --fix
+set CHECKER_ERRORLEVEL=%ERRORLEVEL%
cd translations
for /D %%a in (*) do (set fname=%%a) & call :rename
-move *.qm ..\
+move *.qm ..\ >nul 2>&1
cd ..
+exit /b %CHECKER_ERRORLEVEL%
+
goto :eof
+
:rename
set trname=x64dbg_%fname:-=_%.ts
-copy %fname%\x64dbg.ts %trname%
+copy %fname%\x64dbg.ts %trname% >nul 2>&1
lrelease -nounfinished %trname%