diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_br.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_br.xml
index 178ce3f..07ed7a7 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_br.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_br.xml
@@ -1,186 +1,186 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cn.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cn.xml
new file mode 100644
index 0000000..7ff6237
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cn.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ct.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ct.xml
new file mode 100644
index 0000000..c58ea56
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ct.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cz.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cz.xml
index 5c83910..071ed08 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cz.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_cz.xml
@@ -2,186 +2,186 @@
SniperKittenCZ
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_da.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_da.xml
new file mode 100644
index 0000000..916303a
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_da.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_de.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_de.xml
index f6fe1c1..fd3e09f 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_de.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_de.xml
@@ -2,186 +2,186 @@
LSMT-elMatado
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_en.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_en.xml
index 14df2da..68402a3 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_en.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_en.xml
@@ -1,186 +1,186 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_es.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_es.xml
index 9edba07..bda5e4f 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_es.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_es.xml
@@ -2,186 +2,186 @@
RoberT162v
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fi.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fi.xml
new file mode 100644
index 0000000..76ba057
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fi.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fr.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fr.xml
index c471c90..b0db0e5 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fr.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_fr.xml
@@ -2,186 +2,186 @@
Squallqt
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_hu.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_hu.xml
index 834c788..e40da17 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_hu.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_hu.xml
@@ -1,186 +1,186 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_id.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_id.xml
new file mode 100644
index 0000000..4fa5f82
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_id.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_it.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_it.xml
index c80a48e..86b0d36 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_it.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_it.xml
@@ -2,186 +2,186 @@
caymann lo re, FirenzeIT
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_jp.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_jp.xml
new file mode 100644
index 0000000..99b7fdb
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_jp.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_kr.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_kr.xml
new file mode 100644
index 0000000..9104512
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_kr.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_nl.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_nl.xml
index e80d1ac..4b6611e 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_nl.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_nl.xml
@@ -3,186 +3,186 @@
Hilbert1982
NozemOil1982
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_no.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_no.xml
new file mode 100644
index 0000000..792bba9
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_no.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pl.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pl.xml
index 8467708..3a41a66 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pl.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pl.xml
@@ -2,188 +2,188 @@
dusieq95
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pt.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pt.xml
index cc81376..f248c79 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pt.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_pt.xml
@@ -2,187 +2,187 @@
etrigo1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ro.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ro.xml
new file mode 100644
index 0000000..27e2f18
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ro.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ru.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ru.xml
index 047c631..245de10 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ru.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_ru.xml
@@ -2,186 +2,186 @@
nagor
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_sv.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_sv.xml
new file mode 100644
index 0000000..7c46831
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_sv.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_tr.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_tr.xml
index 8ac3015..e428cee 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_tr.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_tr.xml
@@ -2,187 +2,187 @@
DoxoMatixo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_uk.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_uk.xml
index 0389a8e..1327ead 100644
--- a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_uk.xml
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_uk.xml
@@ -2,187 +2,187 @@
garik
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+MathiasHun" eh="a611a9ae" tag="format"/>
-
+
-
+
-
+
\ No newline at end of file
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_vi.xml b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_vi.xml
new file mode 100644
index 0000000..ec72630
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/l10n_vi.xml
@@ -0,0 +1,206 @@
+
+
+ XelaNull + Claude Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_gameplay_Real_Vehicle_Breakdowns/translations/translation_sync.js b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/translation_sync.js
new file mode 100644
index 0000000..65b1d96
--- /dev/null
+++ b/FS25_gameplay_Real_Vehicle_Breakdowns/translations/translation_sync.js
@@ -0,0 +1,1328 @@
+#!/usr/bin/env node
+/**
+ * ══════════════════════════════════════════════════════════════════════════════
+ * UNIVERSAL TRANSLATION SYNC TOOL v3.2.1
+ * For Farming Simulator 25 Mods
+ * ══════════════════════════════════════════════════════════════════════════════
+ *
+ * WHAT IS THIS?
+ * A portable tool that keeps your mod's translation files in sync.
+ * Drop this file into your translations folder and run it - that's it!
+ *
+ * THE PROBLEM IT SOLVES:
+ * When you add or CHANGE a text key in your English file, you need to know
+ * which translations need updating. This tool:
+ * - Adds missing keys to all language files automatically
+ * - Detects when English text changed but translation wasn't updated (STALE)
+ * - Uses embedded hashes for self-documenting XML files
+ * - Validates translations for data quality issues
+ *
+ * ══════════════════════════════════════════════════════════════════════════════
+ * INSTALLING NODE.JS (Required - One-Time Setup)
+ * ══════════════════════════════════════════════════════════════════════════════
+ *
+ * Windows:
+ * 1. Download from https://nodejs.org/ (LTS version recommended)
+ * 2. Run the installer, accept all defaults
+ * 3. Restart any open terminals/command prompts
+ * 4. Verify: Open cmd and type "node --version" (should show v18+ or v20+)
+ *
+ * macOS:
+ * brew install node
+ * OR download from https://nodejs.org/
+ *
+ * Linux:
+ * sudo apt install nodejs npm # Debian/Ubuntu
+ * sudo dnf install nodejs npm # Fedora
+ *
+ * ══════════════════════════════════════════════════════════════════════════════
+ * QUICK START
+ * ══════════════════════════════════════════════════════════════════════════════
+ *
+ * Step 1: Open a terminal/command prompt
+ * Step 2: Navigate to your translations folder:
+ *
+ * cd "C:\path\to\your\mod\translations"
+ *
+ * Step 3: Run a command:
+ *
+ * node translation_sync.js status # See overview of all languages
+ * node translation_sync.js sync # Sync missing keys to all files
+ * node translation_sync.js report # Detailed breakdown of issues
+ * node translation_sync.js help # Full documentation
+ *
+ * That's it! No npm install or dependencies required - just Node.js itself.
+ *
+ * ══════════════════════════════════════════════════════════════════════════════
+ * HOW IT WORKS
+ * ══════════════════════════════════════════════════════════════════════════════
+ *
+ * HOW HASH-BASED SYNC WORKS:
+ * Every entry has an embedded hash (eh) of its English source text:
+ *
+ * English:
+ * German: <- Same hash = OK
+ * French: <- Different = STALE!
+ *
+ * When you change English text:
+ * 1. Run sync - English hash auto-updates
+ * 2. Target hashes stay the same (they reflect what was translated FROM)
+ * 3. Hash mismatch = translation is STALE (needs re-translation)
+ *
+ * COMMANDS:
+ * sync - Add missing keys, update hashes, show what changed
+ * status - Quick table: translated/stale/missing per language
+ * report - Detailed lists of problem keys by language
+ * check - Report issues, exit code 1 if MISSING keys exist
+ * validate - CI-friendly: minimal output, exit codes only
+ * help - Show full help with all options
+ *
+ * WHAT IT DETECTS:
+ * ✓ Missing keys - Key in English but not in target language
+ * ~ Stale entries - Hash mismatch (English changed since translation)
+ * ? Untranslated - Has "[EN] " prefix or exact match to English
+ * !! Duplicates - Same key appears twice in file (data corruption!)
+ * x Orphaned - Key in target but NOT in English (safe to delete)
+ * 💥 Format errors - Wrong format specifiers (%s, %d, %.1f) - WILL CRASH GAME!
+ * ⚠ Empty values - Translation is empty string
+ * ⚠ Whitespace - Leading/trailing spaces in translation
+ *
+ * SUPPORTED XML FORMATS (auto-detected):
+ * (elements pattern - used by UsedPlus)
+ * (texts pattern - no hash support)
+ *
+ * VERSION HISTORY:
+ * v3.2.1 - Fixed format specifier regex (no false positives on "40% success")
+ * v3.2.0 - Added format specifier validation, empty/whitespace detection
+ * v3.1.0 - Added duplicate and orphan detection
+ * v3.0.0 - Hash-based sync system
+ *
+ * Author: FS25_UsedPlus Team
+ * License: MIT - Free to use, modify, and distribute in any mod
+ * ══════════════════════════════════════════════════════════════════════════════
+ */
+
+const VERSION = '3.2.1';
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+// ══════════════════════════════════════════════════════════════════════════════
+// CONFIGURATION
+// ══════════════════════════════════════════════════════════════════════════════
+
+const CONFIG = {
+ // Source language (the "master" file all others sync from)
+ sourceLanguage: 'en',
+
+ // Prefix added to untranslated entries (so translators know what needs work)
+ untranslatedPrefix: '[EN] ',
+
+ // File naming pattern: 'auto', 'translation', or 'l10n'
+ filePrefix: 'auto',
+
+ // XML format: 'auto', 'texts', or 'elements'
+ xmlFormat: 'auto',
+};
+
+// ══════════════════════════════════════════════════════════════════════════════
+// LANGUAGE NAME MAPPINGS
+// ══════════════════════════════════════════════════════════════════════════════
+
+const LANGUAGE_NAMES = {
+ en: 'English',
+ de: 'German',
+ fr: 'French',
+ es: 'Spanish',
+ it: 'Italian',
+ pl: 'Polish',
+ ru: 'Russian',
+ br: 'Portuguese (BR)',
+ pt: 'Portuguese (PT)',
+ cz: 'Czech',
+ cs: 'Czech',
+ uk: 'Ukrainian',
+ nl: 'Dutch',
+ da: 'Danish',
+ sv: 'Swedish',
+ no: 'Norwegian',
+ fi: 'Finnish',
+ hu: 'Hungarian',
+ ro: 'Romanian',
+ tr: 'Turkish',
+ ja: 'Japanese',
+ jp: 'Japanese',
+ ko: 'Korean',
+ zh: 'Chinese (Simplified)',
+ tw: 'Chinese (Traditional)',
+};
+
+// ══════════════════════════════════════════════════════════════════════════════
+// END OF CONFIGURATION
+// ══════════════════════════════════════════════════════════════════════════════
+
+// Change to script directory
+process.chdir(__dirname);
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Utility Functions
+// ──────────────────────────────────────────────────────────────────────────────
+
+function getHash(text) {
+ // 8-character MD5 hash - short but sufficient for change detection
+ return crypto.createHash('md5').update(text, 'utf8').digest('hex').substring(0, 8);
+}
+
+function escapeRegex(str) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+function escapeXml(str) {
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Validation Functions (v3.2.0)
+// ──────────────────────────────────────────────────────────────────────────────
+
+/**
+ * Extract format specifiers from a string
+ * Matches: %s, %d, %i, %f, %.1f, %.2f, %ld, etc.
+ * Returns sorted array for comparison
+ *
+ * NOTE: Excludes space flag to avoid false positives like "40% success"
+ * where "% s" looks like a specifier but is just a percentage followed by text.
+ * Real format specifiers don't have space between % and the type letter.
+ */
+function extractFormatSpecifiers(str) {
+ // Pattern breakdown:
+ // % - literal percent sign
+ // [-+0#]* - optional flags (NO space - that causes false positives)
+ // (\d+)? - optional width
+ // (\.\d+)? - optional precision
+ // (hh?|ll?|L|z|j|t)? - optional length modifier
+ // [diouxXeEfFgGaAcspn] - type specifier (NOTE: excludes % - that's an escape, not a specifier)
+ //
+ // IMPORTANT: %% is an escape sequence that produces a literal %, NOT a format specifier.
+ // We don't include % in the final character class because %% doesn't need to match
+ // between source and target - both "50%" and "50%%" display the same thing.
+ const pattern = /%[-+0#]*(\d+)?(\.\d+)?(hh?|ll?|L|z|j|t)?[diouxXeEfFgGaAcspn]/g;
+ const matches = str.match(pattern) || [];
+ return matches.sort();
+}
+
+/**
+ * Compare format specifiers between source and target
+ * Returns null if OK, or error object if mismatch
+ */
+function checkFormatSpecifiers(sourceValue, targetValue, key) {
+ const sourceSpecs = extractFormatSpecifiers(sourceValue);
+ const targetSpecs = extractFormatSpecifiers(targetValue);
+
+ // Quick check: same count?
+ if (sourceSpecs.length !== targetSpecs.length) {
+ return {
+ key,
+ type: 'count',
+ source: sourceSpecs,
+ target: targetSpecs,
+ message: `Expected ${sourceSpecs.length} format specifier(s), found ${targetSpecs.length}`
+ };
+ }
+
+ // Detailed check: same specifiers?
+ for (let i = 0; i < sourceSpecs.length; i++) {
+ if (sourceSpecs[i] !== targetSpecs[i]) {
+ return {
+ key,
+ type: 'mismatch',
+ source: sourceSpecs,
+ target: targetSpecs,
+ message: `Format specifier mismatch: expected "${sourceSpecs[i]}", found "${targetSpecs[i]}"`
+ };
+ }
+ }
+
+ return null; // OK
+}
+
+/**
+ * Check if a string is "format-only" (no translatable text content)
+ * These are strings like "%s %%", "%d km", "%s:%s" that are identical in all languages
+ */
+function isFormatOnlyString(value) {
+ if (!value) return false;
+ // Remove all format specifiers: %s, %d, %02d, %.1f, %%, etc.
+ // Remove common units that are international: km, m, %, etc.
+ // Remove punctuation and whitespace
+ const stripped = value
+ .replace(/%[-+0-9]*\.?[0-9]*[sdfeEgGoxXuc%]/g, '') // format specifiers
+ .replace(/\b(km|m|kg|l|h|s|ms|px|pcs)\b/gi, '') // common units
+ .replace(/[:\s.,\-\/()[\]{}]+/g, ''); // punctuation & whitespace
+
+ // If nothing remains, it's format-only
+ return stripped.length === 0;
+}
+
+/**
+ * Check for empty value
+ */
+function isEmptyValue(value) {
+ return value === '' || value === null || value === undefined;
+}
+
+/**
+ * Check for whitespace issues (leading/trailing)
+ */
+function hasWhitespaceIssues(value) {
+ if (!value) return false;
+ return value !== value.trim();
+}
+
+/**
+ * Validate a translation entry against its source
+ * Returns array of issues found
+ */
+function validateEntry(key, sourceValue, targetValue, skipUntranslated = true) {
+ const issues = [];
+
+ // Skip entries that are still untranslated (have [EN] prefix)
+ if (skipUntranslated && targetValue.startsWith(CONFIG.untranslatedPrefix)) {
+ return issues;
+ }
+
+ // Check for empty value
+ if (isEmptyValue(targetValue)) {
+ issues.push({ key, type: 'empty', message: 'Empty translation value' });
+ }
+
+ // Check for whitespace issues
+ if (hasWhitespaceIssues(targetValue)) {
+ issues.push({
+ key,
+ type: 'whitespace',
+ message: `Whitespace issue: "${targetValue.substring(0, 20)}..."`,
+ value: targetValue
+ });
+ }
+
+ // Check format specifiers (most critical!)
+ const formatIssue = checkFormatSpecifiers(sourceValue, targetValue, key);
+ if (formatIssue) {
+ issues.push(formatIssue);
+ }
+
+ return issues;
+}
+
+function getEnabledLanguages() {
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) return [];
+
+ const files = fs.readdirSync('.');
+ const pattern = new RegExp(`^${filePrefix}_([a-z]{2})\\.xml$`, 'i');
+ const languages = [];
+
+ for (const file of files) {
+ const match = file.match(pattern);
+ if (match) {
+ const code = match[1].toLowerCase();
+ if (code !== CONFIG.sourceLanguage) {
+ languages.push({
+ code,
+ name: LANGUAGE_NAMES[code] || code.toUpperCase()
+ });
+ }
+ }
+ }
+
+ return languages.sort((a, b) => a.code.localeCompare(b.code));
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Auto-Detection Functions
+// ──────────────────────────────────────────────────────────────────────────────
+
+function autoDetectFilePrefix() {
+ if (CONFIG.filePrefix !== 'auto') return CONFIG.filePrefix;
+
+ if (fs.existsSync(`translation_${CONFIG.sourceLanguage}.xml`)) return 'translation';
+ if (fs.existsSync(`l10n_${CONFIG.sourceLanguage}.xml`)) return 'l10n';
+
+ const files = fs.readdirSync('.');
+ for (const file of files) {
+ if (file.match(/^translation_[a-z]{2}\.xml$/i)) return 'translation';
+ if (file.match(/^l10n_[a-z]{2}\.xml$/i)) return 'l10n';
+ }
+
+ return null;
+}
+
+function autoDetectXmlFormat(content) {
+ if (CONFIG.xmlFormat !== 'auto') return CONFIG.xmlFormat;
+
+ if (content.includes(' - handles any attribute order
+ pattern = /]*)\s*\/>/g;
+ } else {
+ //
+ pattern = //g;
+ }
+
+ let match;
+ while ((match = pattern.exec(content)) !== null) {
+ const key = match[1];
+ const value = match[2];
+ // Extract hash from remaining attributes (handles tag="format" eh="hash" in any order)
+ const attrs = match[3] || '';
+ const hashMatch = attrs.match(/eh="([^"]*)"/);
+ const hash = hashMatch ? hashMatch[1] : null;
+
+ // Track duplicates
+ if (entries.has(key)) {
+ duplicates.push(key);
+ }
+
+ entries.set(key, { value, hash });
+ orderedKeys.push(key);
+ }
+
+ return { entries, orderedKeys, duplicates, rawContent: content };
+}
+
+function formatEntry(key, value, hash, format) {
+ const escapedValue = escapeXml(value);
+ if (format === 'elements') {
+ return ``;
+ } else {
+ return ``;
+ }
+}
+
+function findInsertPosition(content, key, enOrderedKeys, langKeys, format) {
+ const enIndex = enOrderedKeys.indexOf(key);
+
+ // Look for the nearest preceding key that exists in this language
+ for (let i = enIndex - 1; i >= 0; i--) {
+ const prevKey = enOrderedKeys[i];
+ if (langKeys.has(prevKey)) {
+ let pattern;
+ if (format === 'elements') {
+ pattern = new RegExp(``, 'g');
+ } else {
+ pattern = new RegExp(``, 'g');
+ }
+ const match = pattern.exec(content);
+ if (match) {
+ return match.index + match[0].length;
+ }
+ }
+ }
+
+ // Fallback: insert before closing container tag
+ const containerTag = format === 'elements' ? 'elements' : 'texts';
+ const closeTagIndex = content.indexOf(`${containerTag}>`);
+ if (closeTagIndex !== -1) {
+ return closeTagIndex;
+ }
+
+ return -1;
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Update English Source File with Hashes
+// ──────────────────────────────────────────────────────────────────────────────
+
+function updateSourceHashes(sourceFile, format) {
+ let content = fs.readFileSync(sourceFile, 'utf8');
+ const { entries } = parseTranslationFile(sourceFile, format);
+
+ let updated = 0;
+
+ for (const [key, data] of entries) {
+ const correctHash = getHash(data.value);
+
+ if (data.hash !== correctHash) {
+ // Need to update or add the hash
+ // Match entry with any combination of eh= and tag= attributes
+ const oldPattern = new RegExp(
+ `]*)\\s*/>`,
+ 'g'
+ );
+
+ content = content.replace(oldPattern, (match, value, attrs) => {
+ // Remove any existing eh= attribute
+ const cleanAttrs = attrs.replace(/\s*eh="[^"]*"/g, '');
+ // Preserve tag="format" if present
+ const hasTag = cleanAttrs.includes('tag="format"');
+ if (hasTag) {
+ return ``;
+ } else {
+ return ``;
+ }
+ });
+
+ updated++;
+ }
+ }
+
+ if (updated > 0) {
+ fs.writeFileSync(sourceFile, content, 'utf8');
+ }
+
+ return updated;
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// SYNC Command
+// ──────────────────────────────────────────────────────────────────────────────
+
+function syncTranslations() {
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log(`TRANSLATION SYNC v${VERSION} - Hash-Based Synchronization`);
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log();
+
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) {
+ console.error("ERROR: Could not find source translation file.");
+ console.error(`Looking for: translation_${CONFIG.sourceLanguage}.xml or l10n_${CONFIG.sourceLanguage}.xml`);
+ process.exit(1);
+ }
+
+ const sourceFile = getSourceFilePath(filePrefix);
+ if (!fs.existsSync(sourceFile)) {
+ console.error(`ERROR: Source file not found: ${sourceFile}`);
+ process.exit(1);
+ }
+
+ const sourceContent = fs.readFileSync(sourceFile, 'utf8');
+ const format = autoDetectXmlFormat(sourceContent);
+
+ if (!format) {
+ console.error("ERROR: Could not detect XML format from source file.");
+ process.exit(1);
+ }
+
+ // Step 1: Update hashes in the English source file
+ console.log(`[1/3] Updating hashes in source file...`);
+
+ if (format === 'elements') {
+ const hashesUpdated = updateSourceHashes(sourceFile, format);
+ if (hashesUpdated > 0) {
+ console.log(` Updated ${hashesUpdated} hash(es) in ${sourceFile}`);
+ } else {
+ console.log(` All hashes current in ${sourceFile}`);
+ }
+ } else {
+ console.log(` Skipped (hash embedding only supported for 'elements' format)`);
+ }
+
+ // Re-parse source after hash update
+ const { entries: sourceEntries, orderedKeys: sourceOrderedKeys } = parseTranslationFile(sourceFile, format);
+
+ // Compute hashes for comparison
+ const sourceHashes = new Map();
+ for (const [key, data] of sourceEntries) {
+ sourceHashes.set(key, getHash(data.value));
+ }
+
+ console.log();
+ console.log(`[2/3] Source: ${sourceFile} (${sourceEntries.size} keys)`);
+ console.log(` Format: ${format}`);
+ console.log();
+
+ // Step 2: Sync to all target languages
+ console.log(`[3/3] Syncing to target languages...`);
+ console.log();
+
+ const enabledLangs = getEnabledLanguages();
+ const results = [];
+
+ for (const { code: langCode, name: langName } of enabledLangs) {
+ const langFile = getLangFilePath(filePrefix, langCode);
+
+ if (!fs.existsSync(langFile)) {
+ console.log(` ${langName.padEnd(18)}: FILE NOT FOUND - skipping`);
+ results.push({ lang: langName, missing: -1, stale: 0, added: 0 });
+ continue;
+ }
+
+ let { entries: langEntries, orderedKeys: langKeys, duplicates: langDuplicates, rawContent: content } = parseTranslationFile(langFile, format);
+ const langKeySet = new Set(langKeys);
+
+ const missing = [];
+ const stale = [];
+ const duplicates = langDuplicates || [];
+ const orphaned = [];
+ const formatErrors = []; // v3.2.0: Format specifier mismatches (CRITICAL)
+ const emptyValues = []; // v3.2.0: Empty translation values
+ const whitespaceIssues = []; // v3.2.0: Leading/trailing whitespace
+ let added = 0;
+
+ // Find missing and stale keys (source → target)
+ for (const sourceKey of sourceOrderedKeys) {
+ const sourceHash = sourceHashes.get(sourceKey);
+
+ if (!langEntries.has(sourceKey)) {
+ missing.push(sourceKey);
+ } else if (format === 'elements') {
+ const langData = langEntries.get(sourceKey);
+ // Stale = hash doesn't match AND not already marked as untranslated
+ if (langData.hash !== sourceHash && !langData.value.startsWith(CONFIG.untranslatedPrefix)) {
+ stale.push(sourceKey);
+ }
+ }
+ }
+
+ // Find orphaned keys (in target but NOT in source)
+ for (const langKey of langKeys) {
+ if (!sourceEntries.has(langKey)) {
+ orphaned.push(langKey);
+ }
+ }
+
+ // v3.2.0: Validate translations for format specifiers, empty values, whitespace
+ for (const [key, sourceData] of sourceEntries) {
+ if (langEntries.has(key)) {
+ const langData = langEntries.get(key);
+ const validationIssues = validateEntry(key, sourceData.value, langData.value);
+
+ for (const issue of validationIssues) {
+ if (issue.type === 'count' || issue.type === 'mismatch') {
+ formatErrors.push(issue);
+ } else if (issue.type === 'empty') {
+ emptyValues.push(issue);
+ } else if (issue.type === 'whitespace') {
+ whitespaceIssues.push(issue);
+ }
+ }
+ }
+ }
+
+ // Add missing keys
+ for (const key of missing) {
+ const sourceData = sourceEntries.get(key);
+ const sourceHash = sourceHashes.get(key);
+ const placeholderValue = CONFIG.untranslatedPrefix + sourceData.value;
+ const newEntry = `\n ${formatEntry(key, placeholderValue, sourceHash, format)}`;
+
+ const insertPos = findInsertPosition(content, key, sourceOrderedKeys, langKeySet, format);
+
+ if (insertPos !== -1) {
+ content = content.substring(0, insertPos) + newEntry + content.substring(insertPos);
+ langKeySet.add(key);
+ added++;
+ }
+ }
+
+ // Update hashes for existing entries to match source (elements format only)
+ if (format === 'elements') {
+ for (const [key, sourceData] of sourceEntries) {
+ if (langEntries.has(key) && !missing.includes(key)) {
+ const sourceHash = sourceHashes.get(key);
+ const langData = langEntries.get(key);
+
+ // Add hash to entry if:
+ // 1. Translation is current (not stale) - normal case
+ // 2. OR entry has no hash yet AND is not marked as untranslated (first-time adoption)
+ // This handles the chicken-and-egg problem when first adding hashes to a repo
+ const hasNoHash = !langData.hash;
+ const isUntranslated = langData.value.startsWith(CONFIG.untranslatedPrefix);
+ const shouldAddHash = !stale.includes(key) || (hasNoHash && !isUntranslated);
+
+ if (shouldAddHash) {
+ // Match entry with any combination of eh= and tag= attributes
+ // Captures: value, optional existing attributes (eh, tag, etc.)
+ const pattern = new RegExp(
+ `]*)\\s*/>`,
+ 'g'
+ );
+ content = content.replace(pattern, (match, v, attrs) => {
+ // Remove any existing eh= attribute
+ const cleanAttrs = attrs.replace(/\s*eh="[^"]*"/g, '');
+ // Preserve tag="format" if present
+ const hasTag = cleanAttrs.includes('tag="format"');
+ if (hasTag) {
+ return ``;
+ } else {
+ return ``;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ fs.writeFileSync(langFile, content, 'utf8');
+
+ // Report
+ const issues = [];
+ if (added > 0) issues.push(`+${added} added`);
+ if (stale.length > 0) issues.push(`${stale.length} stale`);
+ if (duplicates.length > 0) issues.push(`${duplicates.length} duplicates`);
+ if (orphaned.length > 0) issues.push(`${orphaned.length} orphaned`);
+ // v3.2.0: Add validation issues to report
+ if (formatErrors.length > 0) issues.push(`${formatErrors.length} FORMAT ERRORS`);
+ if (emptyValues.length > 0) issues.push(`${emptyValues.length} empty`);
+ if (whitespaceIssues.length > 0) issues.push(`${whitespaceIssues.length} whitespace`);
+
+ if (issues.length === 0) {
+ console.log(` ${langName.padEnd(18)}: ✓ OK`);
+ } else {
+ console.log(` ${langName.padEnd(18)}: ${issues.join(', ')}`);
+
+ // v3.2.0: Show format errors FIRST (most critical!)
+ if (formatErrors.length > 0) {
+ console.log(` 🔴 FORMAT SPECIFIER ERRORS (will crash game!):`);
+ for (const err of formatErrors.slice(0, 5)) {
+ console.log(` 💥 ${err.key}: ${err.message}`);
+ }
+ if (formatErrors.length > 5) {
+ console.log(` ... and ${formatErrors.length - 5} more format errors`);
+ }
+ }
+
+ if (added > 0) {
+ for (const key of missing.slice(0, 3)) {
+ console.log(` + ${key}`);
+ }
+ if (missing.length > 3) {
+ console.log(` ... and ${missing.length - 3} more`);
+ }
+ }
+
+ if (stale.length > 0 && stale.length <= 5) {
+ console.log(` Stale (English changed):`);
+ for (const key of stale) {
+ console.log(` ~ ${key}`);
+ }
+ } else if (stale.length > 5) {
+ console.log(` Stale: ${stale.slice(0, 3).join(', ')} ... +${stale.length - 3} more`);
+ }
+
+ if (duplicates.length > 0 && duplicates.length <= 5) {
+ console.log(` Duplicates (same key appears twice - remove one!):`);
+ for (const key of duplicates) {
+ console.log(` !! ${key}`);
+ }
+ } else if (duplicates.length > 5) {
+ console.log(` Duplicates: ${duplicates.slice(0, 3).join(', ')} ... +${duplicates.length - 3} more`);
+ }
+
+ if (orphaned.length > 0 && orphaned.length <= 5) {
+ console.log(` Orphaned (not in English - can delete):`);
+ for (const key of orphaned) {
+ console.log(` x ${key}`);
+ }
+ } else if (orphaned.length > 5) {
+ console.log(` Orphaned: ${orphaned.slice(0, 3).join(', ')} ... +${orphaned.length - 3} more`);
+ }
+
+ // v3.2.0: Show empty and whitespace issues
+ if (emptyValues.length > 0) {
+ console.log(` Empty values: ${emptyValues.slice(0, 3).map(e => e.key).join(', ')}${emptyValues.length > 3 ? ` ... +${emptyValues.length - 3} more` : ''}`);
+ }
+ if (whitespaceIssues.length > 0) {
+ console.log(` Whitespace issues: ${whitespaceIssues.slice(0, 3).map(e => e.key).join(', ')}${whitespaceIssues.length > 3 ? ` ... +${whitespaceIssues.length - 3} more` : ''}`);
+ }
+ }
+
+ results.push({
+ lang: langName,
+ missing: missing.length,
+ stale: stale.length,
+ duplicates: duplicates.length,
+ orphaned: orphaned.length,
+ formatErrors: formatErrors.length,
+ emptyValues: emptyValues.length,
+ whitespaceIssues: whitespaceIssues.length,
+ added
+ });
+ }
+
+ console.log();
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log("SYNC COMPLETE");
+ console.log();
+ console.log("Hash-based tracking is now embedded in your XML files:");
+ console.log(" - English entries have eh=\"hash\" showing current text hash");
+ console.log(" - Target entries have eh=\"hash\" showing what they were translated from");
+ console.log(" - When hashes don't match = translation is STALE (needs update)");
+ console.log();
+ console.log(`New entries have "${CONFIG.untranslatedPrefix}" prefix - they need translation!`);
+ console.log("When translator updates an entry, update its eh= to match English.");
+ console.log("══════════════════════════════════════════════════════════════════════");
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// CHECK Command
+// ──────────────────────────────────────────────────────────────────────────────
+
+function checkSync() {
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log(`TRANSLATION CHECK v${VERSION}`);
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log();
+
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) {
+ console.error("ERROR: Could not find source translation file.");
+ process.exit(1);
+ }
+
+ const sourceFile = getSourceFilePath(filePrefix);
+ if (!fs.existsSync(sourceFile)) {
+ console.error(`ERROR: Source file not found: ${sourceFile}`);
+ process.exit(1);
+ }
+
+ const sourceContent = fs.readFileSync(sourceFile, 'utf8');
+ const format = autoDetectXmlFormat(sourceContent);
+ const { entries: sourceEntries } = parseTranslationFile(sourceFile, format);
+
+ // Compute current hashes
+ const sourceHashes = new Map();
+ for (const [key, data] of sourceEntries) {
+ sourceHashes.set(key, getHash(data.value));
+ }
+
+ console.log(`Source: ${sourceFile} (${sourceEntries.size} keys)\n`);
+
+ let hasProblems = false;
+ const summary = [];
+ const enabledLangs = getEnabledLanguages();
+
+ for (const { code: langCode, name: langName } of enabledLangs) {
+ const langFile = getLangFilePath(filePrefix, langCode);
+
+ if (!fs.existsSync(langFile)) {
+ console.log(` ${langName.padEnd(18)}: FILE NOT FOUND`);
+ hasProblems = true;
+ summary.push({ name: langName, total: 0, missing: -1, stale: 0, untranslated: 0 });
+ continue;
+ }
+
+ const { entries: langEntries, orderedKeys: langKeys, duplicates: langDuplicates } = parseTranslationFile(langFile, format);
+
+ const missing = [];
+ const stale = [];
+ const untranslated = [];
+ const duplicates = langDuplicates || [];
+ const orphaned = [];
+
+ for (const [key, sourceData] of sourceEntries) {
+ const sourceHash = sourceHashes.get(key);
+
+ if (!langEntries.has(key)) {
+ missing.push(key);
+ } else {
+ const langData = langEntries.get(key);
+
+ if (langData.value.startsWith(CONFIG.untranslatedPrefix)) {
+ untranslated.push(key);
+ } else if (langData.value === sourceData.value && !isFormatOnlyString(sourceData.value)) {
+ // Exact match = untranslated, UNLESS it's a format-only string (like "%s %%")
+ untranslated.push(key);
+ } else if (format === 'elements' && langData.hash && langData.hash !== sourceHash) {
+ stale.push(key);
+ }
+ }
+ }
+
+ // Find orphaned keys (in target but NOT in source)
+ for (const langKey of langKeys) {
+ if (!sourceEntries.has(langKey)) {
+ orphaned.push(langKey);
+ }
+ }
+
+ const issues = [];
+ if (missing.length > 0) issues.push(`${missing.length} MISSING`);
+ if (stale.length > 0) issues.push(`${stale.length} stale`);
+ if (untranslated.length > 0) issues.push(`${untranslated.length} untranslated`);
+ if (duplicates.length > 0) issues.push(`${duplicates.length} duplicates`);
+ if (orphaned.length > 0) issues.push(`${orphaned.length} orphaned`);
+
+ if (issues.length === 0) {
+ console.log(` ${langName.padEnd(18)}: ✓ OK (${langEntries.size} keys)`);
+ } else {
+ if (missing.length > 0 || duplicates.length > 0 || orphaned.length > 0) hasProblems = true;
+ console.log(` ${langName.padEnd(18)}: ${issues.join(', ')}`);
+ }
+
+ summary.push({
+ name: langName,
+ total: langEntries.size,
+ missing: missing.length,
+ stale: stale.length,
+ untranslated: untranslated.length,
+ duplicates: duplicates.length,
+ orphaned: orphaned.length
+ });
+ }
+
+ console.log();
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────────────");
+ console.log("SUMMARY:");
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────────────");
+ console.log("Language | Total | Missing | Stale | Untranslated | Duplicates | Orphaned");
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────────────");
+
+ for (const s of summary) {
+ const status = (s.missing > 0 || s.duplicates > 0 || s.orphaned > 0) ? '!!' : ' ';
+ const totalStr = s.missing === -1 ? ' N/A' : String(s.total).padStart(6);
+ const missingStr = s.missing === -1 ? ' N/A' : String(s.missing).padStart(7);
+ const dupsStr = s.duplicates !== undefined ? String(s.duplicates).padStart(10) : ' N/A';
+ const orphStr = s.orphaned !== undefined ? String(s.orphaned).padStart(8) : ' N/A';
+ console.log(`${status}${s.name.padEnd(18)} | ${totalStr} | ${missingStr} | ${String(s.stale).padStart(5)} | ${String(s.untranslated).padStart(12)} | ${dupsStr} | ${orphStr}`);
+ }
+
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────────────");
+
+ if (hasProblems) {
+ console.log();
+ const totalMissing = summary.reduce((sum, s) => sum + (s.missing > 0 ? s.missing : 0), 0);
+ const totalDuplicates = summary.reduce((sum, s) => sum + (s.duplicates || 0), 0);
+ const totalOrphaned = summary.reduce((sum, s) => sum + (s.orphaned || 0), 0);
+ if (totalMissing > 0) {
+ console.log("CRITICAL: Missing keys detected! Run 'node translation_sync.js sync' to fix.");
+ }
+ if (totalDuplicates > 0) {
+ console.log(`CRITICAL: ${totalDuplicates} duplicate keys found! Manually remove duplicate entries from XML files.`);
+ }
+ if (totalOrphaned > 0) {
+ console.log(`WARNING: ${totalOrphaned} orphaned keys found (in target but not in English). Safe to delete.`);
+ }
+ process.exit(1);
+ } else {
+ console.log();
+ const totalStale = summary.reduce((sum, s) => sum + s.stale, 0);
+ const totalUntranslated = summary.reduce((sum, s) => sum + s.untranslated, 0);
+
+ if (totalStale > 0) {
+ console.log(`Note: ${totalStale} stale entries need re-translation (English text changed).`);
+ }
+ if (totalUntranslated > 0) {
+ console.log(`Note: ${totalUntranslated} entries have "${CONFIG.untranslatedPrefix}" prefix and need translation.`);
+ }
+ if (totalStale === 0 && totalUntranslated === 0) {
+ console.log("All translations are complete and up to date!");
+ }
+ process.exit(0);
+ }
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// STATUS Command
+// ──────────────────────────────────────────────────────────────────────────────
+
+function showStatus() {
+ console.log();
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log(`TRANSLATION STATUS v${VERSION}`);
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log();
+
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) {
+ console.error("ERROR: Could not find translation files.");
+ process.exit(1);
+ }
+
+ const sourceFile = getSourceFilePath(filePrefix);
+ const sourceContent = fs.readFileSync(sourceFile, 'utf8');
+ const format = autoDetectXmlFormat(sourceContent);
+ const { entries: sourceEntries } = parseTranslationFile(sourceFile, format);
+
+ const sourceHashes = new Map();
+ for (const [key, data] of sourceEntries) {
+ sourceHashes.set(key, getHash(data.value));
+ }
+
+ console.log(`Source: ${sourceFile} (${sourceEntries.size} keys)`);
+ console.log(`Format: ${format}${format === 'elements' ? ' (hash-enabled)' : ''}`);
+ console.log();
+
+ console.log("Language | Translated | Stale | Untranslated | Missing | Dups | Orphaned");
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────");
+
+ const enabledLangs = getEnabledLanguages();
+
+ for (const { code: langCode, name: langName } of enabledLangs) {
+ const langFile = getLangFilePath(filePrefix, langCode);
+
+ if (!fs.existsSync(langFile)) {
+ console.log(`${langName.padEnd(20)}| N/A | N/A | N/A | N/A | N/A | N/A`);
+ continue;
+ }
+
+ const { entries: langEntries, orderedKeys: langKeys, duplicates: langDuplicates } = parseTranslationFile(langFile, format);
+
+ let translated = 0, stale = 0, untranslated = 0, missing = 0, orphaned = 0, formatErrs = 0;
+ const duplicates = langDuplicates ? langDuplicates.length : 0;
+
+ for (const [key, sourceData] of sourceEntries) {
+ const sourceHash = sourceHashes.get(key);
+
+ if (!langEntries.has(key)) {
+ missing++;
+ } else {
+ const langData = langEntries.get(key);
+
+ if (langData.value.startsWith(CONFIG.untranslatedPrefix) ||
+ (langData.value === sourceData.value && !isFormatOnlyString(sourceData.value))) {
+ untranslated++;
+ } else if (format === 'elements' && langData.hash && langData.hash !== sourceHash) {
+ stale++;
+ } else {
+ translated++;
+ }
+
+ // v3.2.0: Check format specifiers
+ const formatIssue = checkFormatSpecifiers(sourceData.value, langData.value, key);
+ if (formatIssue && !langData.value.startsWith(CONFIG.untranslatedPrefix)) {
+ formatErrs++;
+ }
+ }
+ }
+
+ // Count orphaned keys
+ for (const langKey of langKeys) {
+ if (!sourceEntries.has(langKey)) {
+ orphaned++;
+ }
+ }
+
+ // v3.2.0: Show format errors prominently
+ const fmtStr = formatErrs > 0 ? ` 🔴${formatErrs}` : '';
+ console.log(`${langName.padEnd(20)}| ${String(translated).padStart(10)} | ${String(stale).padStart(7)} | ${String(untranslated).padStart(12)} | ${String(missing).padStart(7)} | ${String(duplicates).padStart(4)} | ${String(orphaned).padStart(8)}${fmtStr}`);
+ }
+
+ console.log("──────────────────────────────────────────────────────────────────────────────────────────");
+ console.log("🔴 = Format specifier errors (CRITICAL - will crash game!)");
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// REPORT Command
+// ──────────────────────────────────────────────────────────────────────────────
+
+function generateReport() {
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log(`TRANSLATION DETAILED REPORT v${VERSION}`);
+ console.log("══════════════════════════════════════════════════════════════════════");
+ console.log();
+
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) {
+ console.error("ERROR: Could not find source translation file.");
+ process.exit(1);
+ }
+
+ const sourceFile = getSourceFilePath(filePrefix);
+ const sourceContent = fs.readFileSync(sourceFile, 'utf8');
+ const format = autoDetectXmlFormat(sourceContent);
+ const { entries: sourceEntries } = parseTranslationFile(sourceFile, format);
+
+ const sourceHashes = new Map();
+ for (const [key, data] of sourceEntries) {
+ sourceHashes.set(key, getHash(data.value));
+ }
+
+ console.log(`Source: ${sourceFile} (${sourceEntries.size} keys)\n`);
+
+ const enabledLangs = getEnabledLanguages();
+
+ for (const { code: langCode, name: langName } of enabledLangs) {
+ const langFile = getLangFilePath(filePrefix, langCode);
+
+ if (!fs.existsSync(langFile)) {
+ console.log(`${langName} (${langCode.toUpperCase()}): FILE NOT FOUND\n`);
+ continue;
+ }
+
+ const { entries: langEntries, orderedKeys: langKeys, duplicates: langDuplicates } = parseTranslationFile(langFile, format);
+
+ const translated = [];
+ const missing = [];
+ const stale = [];
+ const untranslated = [];
+ const duplicates = langDuplicates || [];
+ const orphaned = [];
+
+ for (const [key, sourceData] of sourceEntries) {
+ const sourceHash = sourceHashes.get(key);
+
+ if (!langEntries.has(key)) {
+ missing.push({ key, enValue: sourceData.value });
+ } else {
+ const langData = langEntries.get(key);
+
+ if (langData.value.startsWith(CONFIG.untranslatedPrefix)) {
+ untranslated.push({ key, reason: 'has [EN] prefix' });
+ } else if (langData.value === sourceData.value && !isFormatOnlyString(sourceData.value)) {
+ untranslated.push({ key, reason: 'exact match' });
+ } else if (format === 'elements' && langData.hash && langData.hash !== sourceHash) {
+ stale.push({
+ key,
+ oldHash: langData.hash,
+ newHash: sourceHash,
+ enValue: sourceData.value
+ });
+ } else {
+ translated.push(key);
+ }
+ }
+ }
+
+ // Find orphaned keys
+ for (const langKey of langKeys) {
+ if (!sourceEntries.has(langKey)) {
+ orphaned.push(langKey);
+ }
+ }
+
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
+ console.log(`${langName} (${langCode.toUpperCase()})`);
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
+ console.log(` Translated: ${translated.length}`);
+ console.log(` Missing: ${missing.length}`);
+ console.log(` Stale: ${stale.length}`);
+ console.log(` Untranslated: ${untranslated.length}`);
+ console.log(` Duplicates: ${duplicates.length}`);
+ console.log(` Orphaned: ${orphaned.length}`);
+
+ if (missing.length > 0) {
+ console.log(`\n ── MISSING KEYS ──`);
+ for (const { key } of missing.slice(0, 10)) {
+ console.log(` - ${key}`);
+ }
+ if (missing.length > 10) {
+ console.log(` ... and ${missing.length - 10} more`);
+ }
+ }
+
+ if (stale.length > 0) {
+ console.log(`\n ── STALE (English changed since translation) ──`);
+ for (const { key, oldHash, newHash } of stale.slice(0, 10)) {
+ console.log(` ~ ${key} (${oldHash} → ${newHash})`);
+ }
+ if (stale.length > 10) {
+ console.log(` ... and ${stale.length - 10} more`);
+ }
+ }
+
+ if (untranslated.length > 0 && untranslated.length <= 10) {
+ console.log(`\n ── UNTRANSLATED ──`);
+ for (const { key, reason } of untranslated) {
+ console.log(` ? ${key} (${reason})`);
+ }
+ } else if (untranslated.length > 10) {
+ console.log(`\n ── UNTRANSLATED (showing first 10) ──`);
+ for (const { key, reason } of untranslated.slice(0, 10)) {
+ console.log(` ? ${key} (${reason})`);
+ }
+ console.log(` ... and ${untranslated.length - 10} more`);
+ }
+
+ if (duplicates.length > 0 && duplicates.length <= 10) {
+ console.log(`\n ── DUPLICATES (same key appears twice - remove one!) ──`);
+ for (const key of duplicates) {
+ console.log(` !! ${key}`);
+ }
+ } else if (duplicates.length > 10) {
+ console.log(`\n ── DUPLICATES (showing first 10) ──`);
+ for (const key of duplicates.slice(0, 10)) {
+ console.log(` !! ${key}`);
+ }
+ console.log(` ... and ${duplicates.length - 10} more`);
+ }
+
+ if (orphaned.length > 0 && orphaned.length <= 10) {
+ console.log(`\n ── ORPHANED (not in English - safe to delete) ──`);
+ for (const key of orphaned) {
+ console.log(` x ${key}`);
+ }
+ } else if (orphaned.length > 10) {
+ console.log(`\n ── ORPHANED (showing first 10) ──`);
+ for (const key of orphaned.slice(0, 10)) {
+ console.log(` x ${key}`);
+ }
+ console.log(` ... and ${orphaned.length - 10} more`);
+ }
+
+ console.log();
+ }
+
+ console.log("══════════════════════════════════════════════════════════════════════");
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// VALIDATE Command (CI-friendly)
+// ──────────────────────────────────────────────────────────────────────────────
+
+function validateSync() {
+ const filePrefix = autoDetectFilePrefix();
+ if (!filePrefix) {
+ console.log("FAIL: No translation files found");
+ process.exit(1);
+ }
+
+ const sourceFile = getSourceFilePath(filePrefix);
+ if (!fs.existsSync(sourceFile)) {
+ console.log("FAIL: Source file not found");
+ process.exit(1);
+ }
+
+ const sourceContent = fs.readFileSync(sourceFile, 'utf8');
+ const format = autoDetectXmlFormat(sourceContent);
+ const { entries: sourceEntries } = parseTranslationFile(sourceFile, format);
+
+ let hasProblems = false;
+ const enabledLangs = getEnabledLanguages();
+
+ for (const { code: langCode } of enabledLangs) {
+ const langFile = getLangFilePath(filePrefix, langCode);
+ if (!fs.existsSync(langFile)) {
+ hasProblems = true;
+ break;
+ }
+
+ const { entries: langEntries } = parseTranslationFile(langFile, format);
+
+ for (const [key] of sourceEntries) {
+ if (!langEntries.has(key)) {
+ hasProblems = true;
+ break;
+ }
+ }
+
+ if (hasProblems) break;
+ }
+
+ if (hasProblems) {
+ console.log("FAIL: Translation files out of sync");
+ process.exit(1);
+ } else {
+ console.log("OK: All translation files have required keys");
+ process.exit(0);
+ }
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Help
+// ──────────────────────────────────────────────────────────────────────────────
+
+function showHelp() {
+ console.log(`
+══════════════════════════════════════════════════════════════════════════════
+UNIVERSAL TRANSLATION SYNC TOOL v${VERSION}
+══════════════════════════════════════════════════════════════════════════════
+
+A hash-based translation synchronization tool for Farming Simulator 25 mods.
+
+HOW HASH-BASED SYNC WORKS:
+ Every entry embeds a hash of its English source text:
+
+ English:
+ German:
+
+ When English changes, its hash changes. Target entries keep their old hash
+ until the translator updates them. Hash mismatch = STALE translation.
+
+COMMANDS:
+ sync - Add missing keys, update source hashes, report stale entries
+ check - Report all issues, exit code 1 if MISSING keys exist
+ status - Quick overview: translated/stale/missing per language
+ report - Detailed breakdown by language with lists of problem keys
+ validate - CI-friendly: minimal output, exit codes only
+ help - Show this help
+
+USAGE:
+ node translation_sync.js sync # Sync all languages, update hashes
+ node translation_sync.js check # Verify sync status
+ node translation_sync.js report # See detailed stale/missing lists
+
+WORKFLOW:
+ 1. Add/change text in translation_${CONFIG.sourceLanguage}.xml
+ 2. Run: node translation_sync.js sync
+ 3. Script updates English hashes, adds missing keys to other languages
+ 4. Report shows which entries are STALE (English changed, needs re-translation)
+ 5. Translator updates entry and sets eh= to match English
+
+STATUS MEANINGS:
+ ✓ Translated - Entry exists and hash matches (up to date)
+ ~ Stale - Hash mismatch (English changed since translation)
+ ? Untranslated - Has "[EN] " prefix or exact match to English
+ - Missing - Key doesn't exist in target file
+ !! Duplicate - Same key appears more than once (data quality issue!)
+ x Orphaned - Key in target file but NOT in English (safe to delete)
+
+VALIDATION (v3.2.0):
+ 💥 Format Error - Missing/wrong format specifiers (%s, %.1f, etc.) - WILL CRASH!
+ ⚠ Empty Value - Translation is empty string
+ ⚠ Whitespace - Leading/trailing spaces in translation
+
+══════════════════════════════════════════════════════════════════════════════
+`);
+}
+
+// ──────────────────────────────────────────────────────────────────────────────
+// Main
+// ──────────────────────────────────────────────────────────────────────────────
+
+const command = process.argv[2]?.toLowerCase();
+
+switch (command) {
+ case 'sync':
+ syncTranslations();
+ break;
+ case 'check':
+ checkSync();
+ break;
+ case 'status':
+ showStatus();
+ break;
+ case 'report':
+ generateReport();
+ break;
+ case 'validate':
+ validateSync();
+ break;
+ case 'help':
+ case '--help':
+ case '-h':
+ showHelp();
+ break;
+ default:
+ showHelp();
+}