mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 15:07:32 +00:00
+104









sh
thedmdim
Hosted Weblate user 107176
Artem Vorotnikov
Paul
А О
whoami11
Skyward Copied
Evgeny
Игорь Линчевский
Name Lastname
Ophiushi
khalidbelk
Mqlx
Evgeny Poberezkin
J. Lavoie
Anonymous
Ranakan
Karl Rinneberg
indiandoc
J R
cedev-1
Deleted User
Random
sith-on-mars
Float
Deleted User
BlacAmDK
re me
Aaron H
Loping151
cllih
Zhiyounihao
M Sarmad Qadeer
Albert Bob
小连招
Yay
Frank Yang
Citrus
Ross Li
No name
Luis Morillo Najarro
Darío Hereñú
Víctor
kudebug
Carlos Perez
Isaac ALejandro Lopez
gacarel
John m
M1K4
a4318
Miyu Sakatsuki
ikyhtr
小林照幸
proudmuslim-dev
Ikko Eltociear Ashimine
Rei
yokoba0413
ryokky3
koreirozzu
daydreamer-json
acevif
ActiveTK.
Dima Sivan
zenobit
Mehmed
mslr8
Pluto
idk
Nenad
Magorak Magorov
Maksym Lukashenko
Max
ghost
rider of the storm
Deleted User
Hotripak
Groovy394
aquaticexpectancy
Andrii Sudak
sr sr
petri
Jiri Grönroos
jaeone
B.O.S.S
Jester Hodl
Kszyche
Wiesław Fijołek
Arturro43
Omikorin
Titapa Chaiyakiturajai
elgratea
109247019824
Kaanito
Abdullah Koyuncu
goknarbahceli
Dennis
cervezagua
Deleted User
Egehan KAHRAMAN
Omer
xe1st
echoloji
U.S.
summoner001
Ghost of Sparta
d4f5409d
Mireli5656
Hosted Weblate
mlanp
jonnysemon
SanctumSaturn
Fábio Pereira
Narasimha-sc
0d608e3e47
* Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Russian) Currently translated at 99.5% (2501 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.6% (2453 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/ * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 96.4% (2424 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hant/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Dutch) Currently translated at 81.3% (2044 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Dutch) Currently translated at 81.3% (2044 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Dutch) Currently translated at 81.3% (2044 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Dutch) Currently translated at 81.3% (2044 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Dutch) Currently translated at 81.3% (2044 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Japanese) Currently translated at 48.7% (1224 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Czech) Currently translated at 50.2% (1264 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 35.6% (896 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pt_BR/ * Translated using Weblate (Croatian) Currently translated at 1.8% (47 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hr/ * Translated using Weblate (Croatian) Currently translated at 1.8% (47 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hr/ * Translated using Weblate (Arabic) Currently translated at 29.2% (735 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ar/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 85.6% (2152 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Lithuanian) Currently translated at 1.5% (38 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/lt/ * Translated using Weblate (Finnish) Currently translated at 44.2% (1113 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fi/ * Translated using Weblate (Finnish) Currently translated at 44.2% (1113 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fi/ * Translated using Weblate (Finnish) Currently translated at 44.2% (1113 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fi/ * Translated using Weblate (Korean) Currently translated at 17.5% (441 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ko/ * Translated using Weblate (Korean) Currently translated at 17.5% (441 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ko/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Polish) Currently translated at 86.2% (2168 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pl/ * Translated using Weblate (Portuguese) Currently translated at 6.0% (153 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pt/ * Translated using Weblate (Greek) Currently translated at 1.0% (27 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/el/ * Translated using Weblate (Hebrew) Currently translated at 26.3% (661 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/he/ * Translated using Weblate (Thai) Currently translated at 42.9% (1079 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/th/ * Translated using Weblate (Thai) Currently translated at 42.9% (1079 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/th/ * Translated using Weblate (Bengali) Currently translated at 0.1% (3 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bn/ * Translated using Weblate (Bulgarian) Currently translated at 61.4% (1544 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bg/ * Translated using Weblate (Bulgarian) Currently translated at 61.4% (1544 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bg/ * Translated using Weblate (Bulgarian) Currently translated at 61.4% (1544 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bg/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 85.4% (2147 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2513 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Azerbaijani) Currently translated at 47.0% (1183 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/az/ * Translated using Weblate (Azerbaijani) Currently translated at 47.0% (1183 of 2513 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/az/ * Translated using Weblate (Bulgarian) Currently translated at 80.9% (2430 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/ * Translated using Weblate (Russian) Currently translated at 93.9% (2820 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (German) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Lithuanian) Currently translated at 56.6% (1701 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/lt/ * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 92.0% (2763 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hant/ * Translated using Weblate (Thai) Currently translated at 42.2% (1269 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/th/ * Translated using Weblate (Polish) Currently translated at 82.0% (2463 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/ * Translated using Weblate (Greek) Currently translated at 81.7% (2454 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/el/ * Translated using Weblate (Croatian) Currently translated at 48.5% (1457 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hr/ * Translated using Weblate (Czech) Currently translated at 89.4% (2685 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (Hebrew) Currently translated at 69.1% (2074 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/he/ * Translated using Weblate (Persian) Currently translated at 81.1% (2434 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/ * Translated using Weblate (Dutch) Currently translated at 78.0% (2342 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Indonesian) Currently translated at 88.3% (2650 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Bengali) Currently translated at 1.7% (53 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bn/ * Translated using Weblate (Arabic) Currently translated at 94.2% (2827 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Korean) Currently translated at 49.5% (1488 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Italian) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Hungarian) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Finnish) Currently translated at 47.9% (1438 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fi/ * Translated using Weblate (French) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Romanian) Currently translated at 82.0% (2463 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 96.5% (2897 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Azerbaijani) Currently translated at 0.3% (10 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/az/ * Translated using Weblate (Ukrainian) Currently translated at 86.3% (2590 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Japanese) Currently translated at 91.1% (2735 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Turkish) Currently translated at 92.8% (2787 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Spanish) Currently translated at 96.5% (2898 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Portuguese) Currently translated at 39.3% (1180 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Catalan) Currently translated at 81.3% (2440 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ca/ * Translated using Weblate (Danish) Currently translated at 30.8% (927 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/da/ * Translated using Weblate (Hindi) Currently translated at 10.2% (307 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hi/ * Translated using Weblate (Kurdish) Currently translated at 36.6% (1101 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ku/ * Translated using Weblate (Latvian) Currently translated at 80.9% (2428 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/lv/ * Translated using Weblate (Malayalam) Currently translated at 13.7% (412 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ml/ * Translated using Weblate (Norwegian Bokmål) Currently translated at 7.4% (225 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nb/ * Translated using Weblate (Slovak) Currently translated at 47.9% (1438 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/ * Translated using Weblate (Swedish) Currently translated at 0.3% (10 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sv/ * Translated using Weblate (Vietnamese) Currently translated at 81.6% (2450 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ * Translated using Weblate (German) Currently translated at 97.9% (2938 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Italian) Currently translated at 99.0% (2971 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 99.1% (2975 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 99.2% (2651 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 99.8% (2996 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 99.8% (2668 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3001 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Italian) Currently translated at 99.9% (2998 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Arabic) Currently translated at 94.4% (2833 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 97.8% (2936 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (Arabic) Currently translated at 94.6% (2839 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3001 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Portuguese) Currently translated at 39.4% (1185 of 3001 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (French) Currently translated at 96.4% (2577 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (Portuguese) Currently translated at 6.2% (167 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pt/ * Translated using Weblate (French) Currently translated at 98.5% (2631 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 98.6% (2634 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (Italian) Currently translated at 99.9% (2670 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Italian) Currently translated at 99.9% (3003 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3006 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (French) Currently translated at 99.2% (2652 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (German) Currently translated at 98.0% (2948 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Italian) Currently translated at 99.9% (3005 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (French) Currently translated at 99.9% (2670 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (French) Currently translated at 96.4% (2899 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (3006 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (3006 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (German) Currently translated at 98.5% (2962 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (German) Currently translated at 97.8% (2614 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (German) Currently translated at 100.0% (3006 of 3006 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (German) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (German) Currently translated at 100.0% (3007 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Italian) Currently translated at 99.9% (3006 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3007 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (3007 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.7% (2969 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (French) Currently translated at 97.8% (2941 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3007 of 3007 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Italian) Currently translated at 99.9% (3011 of 3012 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (French) Currently translated at 97.7% (2943 of 3012 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ * Translated using Weblate (Italian) Currently translated at 100.0% (2671 of 2671 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (3051 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Hungarian) Currently translated at 98.7% (3013 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3051 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Portuguese) Currently translated at 99.1% (3024 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (French) Currently translated at 96.7% (2953 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (German) Currently translated at 100.0% (3051 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (French) Currently translated at 97.6% (2980 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Portuguese) Currently translated at 99.6% (3041 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Portuguese) Currently translated at 100.0% (3051 of 3051 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * ios: add localization round-trip script * ios: list each localization problem once * translations: import/export ios * android, ios: check kotlin and removed strings * translations: fix issues * translations: fix broken links * android: apply adjustFormatting to translations * android: port adjustFormatting rewrite to script * translations: fix review findings in PR #7944 (#7950) pt: render "link" as "link" instead of "endereço" (address), so SimpleX links, 1-time links and the SimpleX address stay distinct; "badge" is "distintivo" everywhere; fix payment footers (keep, buy), typos, actions shown as states, contributor, migrate label, ratchet sync, address change. pt iOS: "passphrase" is "frase-chave". de: To purchase key, signing alert, % off, TEE, Credential, Balance start. fr: Balance start, proofs. hu: SimpleX typo, (chat), Balance start, badge ended, Credential. it: renews on date, Paga, article. az iOS: restore the "Mark ..." actions, Forward, Keep conversation, Lock after, bold markup. cs: 1 day, Test relay. tr iOS: delivery receipts. pt-rBR: marked for deletion. ar: badge shown, host names. --------- Co-authored-by: thedmdim <thedmdim@users.noreply.hosted.weblate.org> Co-authored-by: Hosted Weblate user 107176 <zhuk2@users.noreply.hosted.weblate.org> Co-authored-by: Artem Vorotnikov <vorot93@users.noreply.hosted.weblate.org> Co-authored-by: Paul <acornacre@users.noreply.hosted.weblate.org> Co-authored-by: А О <twofish85@users.noreply.hosted.weblate.org> Co-authored-by: whoami11 <whoami021992@users.noreply.hosted.weblate.org> Co-authored-by: Skyward Copied <skywardcopied@users.noreply.hosted.weblate.org> Co-authored-by: Evgeny <esp@users.noreply.hosted.weblate.org> Co-authored-by: Игорь Линчевский <insrtnqme@users.noreply.hosted.weblate.org> Co-authored-by: Name Lastname <simplex-edit0r@users.noreply.hosted.weblate.org> Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com> Co-authored-by: khalidbelk <khalidbelk@users.noreply.hosted.weblate.org> Co-authored-by: Mqlx <mqlx@users.noreply.hosted.weblate.org> Co-authored-by: Evgeny Poberezkin <epoberezkin@users.noreply.hosted.weblate.org> Co-authored-by: J. Lavoie <edanas@users.noreply.hosted.weblate.org> Co-authored-by: Anonymous <anonymous@users.noreply.hosted.weblate.org> Co-authored-by: Ranakan <ranak4n@users.noreply.hosted.weblate.org> Co-authored-by: Karl Rinneberg <hundertwasser@users.noreply.hosted.weblate.org> Co-authored-by: indiandoc <indiandoc@users.noreply.hosted.weblate.org> Co-authored-by: J R <jrs@users.noreply.hosted.weblate.org> Co-authored-by: cedev-1 <121963179+cedev-1@users.noreply.github.com> Co-authored-by: Deleted User <deleted-6980@users.noreply.hosted.weblate.org> Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org> Co-authored-by: sith-on-mars <sith-on-mars@users.noreply.hosted.weblate.org> Co-authored-by: Float <float@users.noreply.hosted.weblate.org> Co-authored-by: Deleted User <dnstxqet@users.noreply.hosted.weblate.org> Co-authored-by: BlacAmDK <blacamdk@users.noreply.hosted.weblate.org> Co-authored-by: re me <jisizhang001@users.noreply.hosted.weblate.org> Co-authored-by: Aaron H <chieferino@users.noreply.hosted.weblate.org> Co-authored-by: Loping151 <loping151@users.noreply.hosted.weblate.org> Co-authored-by: cllih <cllih@users.noreply.hosted.weblate.org> Co-authored-by: Zhiyounihao <zhiyouni@users.noreply.hosted.weblate.org> Co-authored-by: M Sarmad Qadeer <msarmadqadeer@users.noreply.hosted.weblate.org> Co-authored-by: Albert Bob <vicdorke@users.noreply.hosted.weblate.org> Co-authored-by: 小连招 <onecombo666@users.noreply.hosted.weblate.org> Co-authored-by: Yay <yay9hbx5otn5@users.noreply.hosted.weblate.org> Co-authored-by: Frank Yang <frankyang96@users.noreply.hosted.weblate.org> Co-authored-by: Citrus <citrus@users.noreply.hosted.weblate.org> Co-authored-by: Ross Li <ross89223@users.noreply.hosted.weblate.org> Co-authored-by: No name <certainbot@users.noreply.hosted.weblate.org> Co-authored-by: Luis Morillo Najarro <lsmor@users.noreply.hosted.weblate.org> Co-authored-by: Darío Hereñú <kant@users.noreply.hosted.weblate.org> Co-authored-by: Víctor <vzamanillo@users.noreply.hosted.weblate.org> Co-authored-by: kudebug <kudebug@users.noreply.hosted.weblate.org> Co-authored-by: Carlos Perez <charlieper@users.noreply.hosted.weblate.org> Co-authored-by: Isaac ALejandro Lopez <justanormalguy@users.noreply.hosted.weblate.org> Co-authored-by: gacarel <gacarel@users.noreply.hosted.weblate.org> Co-authored-by: John m <jvanmanen@users.noreply.hosted.weblate.org> Co-authored-by: M1K4 <mika-nl@users.noreply.hosted.weblate.org> Co-authored-by: a4318 <a4318@users.noreply.hosted.weblate.org> Co-authored-by: Miyu Sakatsuki <miyu-dev@users.noreply.hosted.weblate.org> Co-authored-by: ikyhtr <ikyhtr@users.noreply.hosted.weblate.org> Co-authored-by: 小林照幸 <koba-ninkigumi@users.noreply.hosted.weblate.org> Co-authored-by: proudmuslim-dev <proudmuslim-dev@users.noreply.hosted.weblate.org> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: Rei <rei_@users.noreply.hosted.weblate.org> Co-authored-by: yokoba0413 <yokoba0413@users.noreply.hosted.weblate.org> Co-authored-by: ryokky3 <ryokky3@users.noreply.hosted.weblate.org> Co-authored-by: koreirozzu <koreirozzu@users.noreply.hosted.weblate.org> Co-authored-by: daydreamer-json <daydreamer-json@users.noreply.hosted.weblate.org> Co-authored-by: acevif <acevif@users.noreply.hosted.weblate.org> Co-authored-by: ActiveTK. <webmaster@activetk.jp> Co-authored-by: Dima Sivan <dimasivan@users.noreply.hosted.weblate.org> Co-authored-by: zenobit <zenobit@disroot.org> Co-authored-by: Mehmed <don43@users.noreply.hosted.weblate.org> Co-authored-by: mslr8 <hosted.weblate.org.poncho089@passmail.net> Co-authored-by: Pluto <offplutonium@users.noreply.hosted.weblate.org> Co-authored-by: idk <ciganzelezo@users.noreply.hosted.weblate.org> Co-authored-by: Nenad <nenadmirkovic00@gmail.com> Co-authored-by: Magorak Magorov <magorakmagorov@users.noreply.hosted.weblate.org> Co-authored-by: Maksym Lukashenko <prototypem995@users.noreply.hosted.weblate.org> Co-authored-by: Max <prototypem95@users.noreply.hosted.weblate.org> Co-authored-by: ghost <ghosthamster@users.noreply.hosted.weblate.org> Co-authored-by: rider of the storm <passat-b6-tdi@users.noreply.hosted.weblate.org> Co-authored-by: Deleted User <mgorpinic2005@gmail.com> Co-authored-by: Hotripak <hotr1pak@users.noreply.hosted.weblate.org> Co-authored-by: Groovy394 <groovy394@users.noreply.hosted.weblate.org> Co-authored-by: aquaticexpectancy <aquaticexpectancy@users.noreply.hosted.weblate.org> Co-authored-by: Andrii Sudak <andrii-sudak@users.noreply.hosted.weblate.org> Co-authored-by: sr sr <sr1919os@users.noreply.hosted.weblate.org> Co-authored-by: petri <petrik@users.noreply.hosted.weblate.org> Co-authored-by: Jiri Grönroos <artnay@users.noreply.hosted.weblate.org> Co-authored-by: jaeone <jaeone22@users.noreply.hosted.weblate.org> Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com> Co-authored-by: Jester Hodl <jesterhodl@users.noreply.hosted.weblate.org> Co-authored-by: Kszyche <hostedweblate001user@users.noreply.hosted.weblate.org> Co-authored-by: Wiesław Fijołek <percival@users.noreply.hosted.weblate.org> Co-authored-by: Arturro43 <arturro43@users.noreply.hosted.weblate.org> Co-authored-by: Omikorin <git@omikor.in> Co-authored-by: Titapa (PunPun) Chaiyakiturajai <titapapunne@users.noreply.hosted.weblate.org> Co-authored-by: elgratea <flantito@users.noreply.hosted.weblate.org> Co-authored-by: 109247019824 <109247019824@users.noreply.hosted.weblate.org> Co-authored-by: Kaanito <kaanito@users.noreply.hosted.weblate.org> Co-authored-by: Abdullah Koyuncu <wiseweb-works@users.noreply.hosted.weblate.org> Co-authored-by: goknarbahceli <goknarbahceli@users.noreply.hosted.weblate.org> Co-authored-by: Dennis <dennnis@users.noreply.hosted.weblate.org> Co-authored-by: cervezagua <cervezagua@users.noreply.hosted.weblate.org> Co-authored-by: Deleted User <deleted-65564@users.noreply.hosted.weblate.org> Co-authored-by: Egehan KAHRAMAN <vastsea0@users.noreply.hosted.weblate.org> Co-authored-by: Omer <abulomer@users.noreply.hosted.weblate.org> Co-authored-by: xe1st <xe1st@users.noreply.hosted.weblate.org> Co-authored-by: echoloji <echoloji@users.noreply.hosted.weblate.org> Co-authored-by: U.S. <usnetiz@users.noreply.hosted.weblate.org> Co-authored-by: summoner001 <summoner@disroot.org> Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me> Co-authored-by: d4f5409d <d4f5409d@users.noreply.hosted.weblate.org> Co-authored-by: Mireli5656 <mireli5656@users.noreply.hosted.weblate.org> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: mlanp <mlanp@users.noreply.hosted.weblate.org> Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org> Co-authored-by: SanctumSaturn <sanctumsaturn@users.noreply.hosted.weblate.org> Co-authored-by: Fábio Pereira (Serzedo) <fpereira-84@users.noreply.hosted.weblate.org> Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
1136 lines
45 KiB
Python
1136 lines
45 KiB
Python
#!/usr/bin/env python3
|
||
"""Round-trip the iOS translations between Weblate's XLIFF files and the app, and check them.
|
||
|
||
update import Weblate's XLIFF files into the app and export them back with Xcode,
|
||
then lint and check the result (macOS with Xcode)
|
||
check lint and check the iOS XLIFF and .strings files (any platform)
|
||
check-kotlin check the Android and desktop strings.xml files with the rules of
|
||
adjustFormatting in apps/multiplatform/common/build.gradle.kts (any platform)
|
||
update-kotlin rewrite the strings.xml files as adjustFormatting does, then check them (any platform)
|
||
|
||
Run from the repository root. Exits with 1 when problems are found, 2 on errors.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import bisect
|
||
import contextlib
|
||
import functools
|
||
import html
|
||
import os
|
||
import platform
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import xml.etree.ElementTree as ET
|
||
from collections.abc import Iterator
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
from typing import Union
|
||
|
||
IOS_DIR = Path("apps/ios")
|
||
PROJECT = IOS_DIR / "SimpleX.xcodeproj"
|
||
PBXPROJ = PROJECT / "project.pbxproj"
|
||
LOCALIZATIONS_DIR = IOS_DIR / "SimpleX Localizations"
|
||
UPDATE_PBXPROJ = Path("scripts/ios/update-pbxproj.sh")
|
||
SOURCE_LANGUAGE = "en"
|
||
XLIFF_NS = "{urn:oasis:names:tc:xliff:document:1.2}"
|
||
LOG_TAIL_LINES = 40
|
||
GROUP_EXAMPLES = 3
|
||
KEY_PREVIEW_LENGTH = 100
|
||
SUMMARY_FIXED_COLUMNS = ("language", "in app", "translated")
|
||
|
||
UnitKey = tuple[str, str]
|
||
|
||
UNIT_START = re.compile(r"^\s*<trans-unit[\s>]")
|
||
UNIT_END = "</trans-unit>"
|
||
TARGET_START = re.compile(r"<target[\s>/]")
|
||
TARGET_END = re.compile(r"</target>|<target[^>]*/>")
|
||
EMPTY_TARGET = re.compile(r"^\s*<target(?:\s[^>]*)?(?:/>|></target>)\s*$")
|
||
SOURCE_END = "</source>"
|
||
|
||
SPECIFIER = re.compile(
|
||
r"%(?:(?P<position>[1-9][0-9]*)\$)?"
|
||
r"(?P<spec>[-+ #0']*[0-9]*(?:\.[0-9]+)?(?:hh|h|ll|l|q|L|z|t|j)?[@dDiuUxXoOfFeEgGaAcCsSp%])"
|
||
)
|
||
ESCAPED_PERCENT = "%"
|
||
# Link targets are percent-encoded, so they are compared verbatim instead of parsed for placeholders.
|
||
LINK_TARGET = re.compile(r"\]\(([^)]*)\)")
|
||
# Valid URLs are ASCII, so a bare URL ends before full-width punctuation such as ")".
|
||
BARE_URL = re.compile(r"\b(?:https?|simplex):/(?:(?![)\]])[!-~])*")
|
||
LINK_MASK = "<link>"
|
||
# Every markdown link target contains "](" and every bare URL ":/".
|
||
LINK_MARKERS = ("](", ":/")
|
||
LINK_PREVIEW_LENGTH = 60
|
||
STRINGS_ESCAPES = {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\", "'": "'", "0": "\0"}
|
||
UNICODE_ESCAPE = re.compile(r"[uU]([0-9a-fA-F]{4})")
|
||
SWIFT_LITERAL = re.compile(r'"((?:[^"\\\n]|\\.)*)"')
|
||
SWIFT_ESCAPE = re.compile(r"\\(u\{[0-9a-fA-F]{1,8}\}|.)")
|
||
SWIFT_INTERPOLATION = "\\("
|
||
NEWLINE = re.compile("\n")
|
||
|
||
KOTLIN_RESOURCES = Path("apps/multiplatform/common/src/commonMain/moko-resources")
|
||
KOTLIN_BASE = "base"
|
||
KOTLIN_BUILD_SCRIPT = Path("apps/multiplatform/common/build.gradle.kts")
|
||
# The fileRegex of adjustFormatting; lv/strings.xml, which adjustFormatting skips, is checked too.
|
||
KOTLIN_LOCALE = re.compile(r"^(?:..|..-...|..-..)$")
|
||
POSSIBLE_FORMAT = re.compile(r"val possibleFormat = listOf\(([^)]*)\)")
|
||
KOTLIN_STRING = re.compile(r'<string\s+name="(?P<name>[^"]*)"[^>]*>(?P<value>.*?)</string>', re.S)
|
||
KOTLIN_ESCAPE = re.compile(r"\\(u[0-9a-fA-F]{4}|.)")
|
||
CDATA = re.compile(r"<!\[CDATA\[(.*?)\]\]>", re.S)
|
||
UNESCAPED_APOSTROPHE = re.compile(r"(?<!\\)'")
|
||
# The HTML rules of addCDATA in adjustFormatting; "." does not cross lines there either.
|
||
ANY_HTML = re.compile(r"[^>]*>.*(<|>).*</string>|[^>]*>.*(<|>).*</string>")
|
||
FONT_LT_GT = re.compile(r"[^>]*>.*<font[^>]*>.*</font>.*</string>")
|
||
UNBRACKETED_COLOR = re.compile(r"color=#[abcdefABCDEF0-9]{3,6}")
|
||
CORRECT_HTML = re.compile(
|
||
r"[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>"
|
||
)
|
||
KOTLIN_POSITIONS = ("1$", "2$", "3$", "4$")
|
||
# The line handling of adjustFormatting: stringRegex, startStringRegex and endStringRegex.
|
||
KOTLIN_STRING_LINE = re.compile(r".*<string .*</string>.*")
|
||
KOTLIN_START_STRING = re.compile(r"<string [^>]*>")
|
||
KOTLIN_END_STRING = re.compile(r"</string>[ ]*")
|
||
# Kotlin's useLines splits lines like BufferedReader.readLine.
|
||
KOTLIN_LINE_BREAK = re.compile(r"\r\n|\r|\n")
|
||
KOTLIN_UNADJUSTED_LOCALES = {"lv"}
|
||
KOTLIN_STRING_TAG_BRACKETS = 4
|
||
KOTLIN_HTML_ERROR = "HTML other than balanced <b>, <i>, <u> or <font> tags, adjustFormatting fails the build"
|
||
|
||
PLACEHOLDER = "placeholder"
|
||
BLANK = "blank"
|
||
LOST = "lost"
|
||
LINK = "link"
|
||
HTML = "html"
|
||
IN_CODE = "in code"
|
||
UNKNOWN = "unknown"
|
||
CHANGED = "changed"
|
||
SYNTAX = "syntax"
|
||
DUPLICATE = "duplicate"
|
||
STALE = "stale"
|
||
PROBLEM_KINDS = [PLACEHOLDER, LINK, HTML, BLANK, LOST, IN_CODE, UNKNOWN, CHANGED, SYNTAX, DUPLICATE, STALE]
|
||
WARNING_KINDS = {DUPLICATE, STALE, IN_CODE}
|
||
KOTLIN_BUILD_FAILURES = {PLACEHOLDER, HTML, UNKNOWN}
|
||
GROUPED_KINDS = {BLANK, STALE}
|
||
|
||
BOLD = "1"
|
||
DIM = "2"
|
||
RED = "31"
|
||
GREEN = "32"
|
||
YELLOW = "33"
|
||
CYAN = "36"
|
||
KIND_COLORS = {
|
||
PLACEHOLDER: RED,
|
||
LINK: RED,
|
||
HTML: RED,
|
||
BLANK: RED,
|
||
IN_CODE: YELLOW,
|
||
UNKNOWN: RED,
|
||
LOST: RED,
|
||
CHANGED: YELLOW,
|
||
SYNTAX: RED,
|
||
DUPLICATE: YELLOW,
|
||
STALE: YELLOW,
|
||
}
|
||
# https://no-color.org
|
||
USE_COLOR = sys.stdout.isatty() and "NO_COLOR" not in os.environ and os.environ.get("TERM") != "dumb"
|
||
|
||
|
||
def paint(text: str, *codes: str) -> str:
|
||
return f"\033[{';'.join(codes)}m{text}\033[0m" if USE_COLOR and codes else text
|
||
|
||
|
||
class UpdateError(Exception):
|
||
pass
|
||
|
||
|
||
class StringsSyntaxError(ValueError):
|
||
pass
|
||
|
||
|
||
class KotlinFormatError(ValueError):
|
||
pass
|
||
|
||
|
||
@dataclass
|
||
class Unit:
|
||
source: str
|
||
target: str | None
|
||
|
||
@property
|
||
def translated(self) -> bool:
|
||
return bool(self.target)
|
||
|
||
|
||
@dataclass
|
||
class UnitBlock:
|
||
"""One <trans-unit>: its lines kept verbatim, so edits leave the rest of the file intact."""
|
||
|
||
key: UnitKey
|
||
unit: Unit
|
||
lines: list[str]
|
||
|
||
def target_lines(self) -> list[str]:
|
||
start = next(i for i, line in enumerate(self.lines) if TARGET_START.search(line))
|
||
end = next(i for i in range(start, len(self.lines)) if TARGET_END.search(self.lines[i]))
|
||
return self.lines[start : end + 1]
|
||
|
||
|
||
Segment = Union[str, UnitBlock]
|
||
|
||
|
||
@dataclass
|
||
class SavedTranslation:
|
||
source: str
|
||
target: str
|
||
lines: list[str]
|
||
|
||
|
||
@dataclass
|
||
class Placeholders:
|
||
arguments: dict[int, str] = field(default_factory=dict)
|
||
invalid: list[str] = field(default_factory=list)
|
||
mixed: bool = False
|
||
escaped_percent: bool = False
|
||
|
||
@property
|
||
def is_format(self) -> bool:
|
||
return bool(self.arguments) or self.escaped_percent
|
||
|
||
|
||
@dataclass
|
||
class Problem:
|
||
kind: str
|
||
path: Path
|
||
key: str
|
||
detail: str
|
||
text: str = ""
|
||
|
||
@property
|
||
def language(self) -> str:
|
||
return language_of(self.path)
|
||
|
||
|
||
@dataclass
|
||
class LanguageStats:
|
||
translated: int = 0
|
||
restored: int = 0
|
||
removed: int = 0
|
||
|
||
|
||
def read_file(path: Path) -> str:
|
||
with path.open(encoding="utf-8", newline="") as file:
|
||
return file.read()
|
||
|
||
|
||
def write_file(path: Path, text: str) -> None:
|
||
with path.open("w", encoding="utf-8", newline="") as file:
|
||
file.write(text)
|
||
|
||
|
||
def element_text(element: ET.Element | None) -> str | None:
|
||
return None if element is None else "".join(element.itertext())
|
||
|
||
|
||
def parse_units(text: str) -> list[tuple[UnitKey, Unit]]:
|
||
units = []
|
||
for file in ET.fromstring(text).iter(f"{XLIFF_NS}file"):
|
||
original = file.get("original", "")
|
||
for element in file.iter(f"{XLIFF_NS}trans-unit"):
|
||
units.append(
|
||
(
|
||
(original, element.get("id", "")),
|
||
Unit(
|
||
source=element_text(element.find(f"{XLIFF_NS}source")) or "",
|
||
target=element_text(element.find(f"{XLIFF_NS}target")),
|
||
),
|
||
)
|
||
)
|
||
return units
|
||
|
||
|
||
def split_xliff(text: str) -> list[Segment]:
|
||
"""Split into verbatim lines and unit blocks; the XML is parsed once and matched to the blocks in order."""
|
||
units = iter(parse_units(text))
|
||
segments: list[Segment] = []
|
||
block_lines: list[str] | None = None
|
||
for line in text.splitlines(keepends=True):
|
||
if block_lines is None:
|
||
if not UNIT_START.match(line):
|
||
segments.append(line)
|
||
continue
|
||
block_lines = []
|
||
block_lines.append(line)
|
||
if UNIT_END in line:
|
||
key, unit = next(units)
|
||
segments.append(UnitBlock(key, unit, block_lines))
|
||
block_lines = None
|
||
if block_lines is not None or next(units, None) is not None:
|
||
raise ValueError("<trans-unit> elements must start and end on their own lines")
|
||
return segments
|
||
|
||
|
||
def join_xliff(segments: list[Segment]) -> str:
|
||
return "".join(s if isinstance(s, str) else "".join(s.lines) for s in segments)
|
||
|
||
|
||
def unit_blocks(segments: list[Segment]) -> Iterator[UnitBlock]:
|
||
return (s for s in segments if isinstance(s, UnitBlock))
|
||
|
||
|
||
def strip_empty_targets(segments: list[Segment]) -> int:
|
||
stripped = 0
|
||
for block in unit_blocks(segments):
|
||
lines = [line for line in block.lines if not EMPTY_TARGET.match(line)]
|
||
if len(lines) != len(block.lines):
|
||
stripped += 1
|
||
block.lines = lines
|
||
block.unit.target = None
|
||
return stripped
|
||
|
||
|
||
def drop_untranslated_units(segments: list[Segment]) -> list[Segment]:
|
||
"""Remove units without a translation, as Weblate's "Remove blank strings" add-on does."""
|
||
return [s for s in segments if isinstance(s, str) or s.unit.translated]
|
||
|
||
|
||
def saved_translations(segments: list[Segment]) -> dict[UnitKey, SavedTranslation]:
|
||
saved: dict[UnitKey, SavedTranslation] = {}
|
||
for block in unit_blocks(segments):
|
||
if block.unit.translated:
|
||
saved.setdefault(block.key, SavedTranslation(block.unit.source, block.unit.target or "", block.target_lines()))
|
||
return saved
|
||
|
||
|
||
def restore_translations(segments: list[Segment], saved: dict[UnitKey, SavedTranslation]) -> int:
|
||
"""Put back translations that Xcode dropped, such as drafts, unless their source text changed."""
|
||
blocks = list(unit_blocks(segments))
|
||
translated = {block.key for block in blocks if block.unit.translated}
|
||
restored = 0
|
||
for block in blocks:
|
||
translation = saved.get(block.key)
|
||
if block.key in translated or translation is None or translation.source != block.unit.source:
|
||
continue
|
||
lines = [line for line in block.lines if not EMPTY_TARGET.match(line)]
|
||
source_end = next(i for i, line in enumerate(lines) if SOURCE_END in line)
|
||
block.lines = lines[: source_end + 1] + translation.lines + lines[source_end + 1 :]
|
||
block.unit.target = translation.target
|
||
translated.add(block.key)
|
||
restored += 1
|
||
return restored
|
||
|
||
|
||
def parse_placeholders(text: str) -> Placeholders:
|
||
result = Placeholders()
|
||
positional = sequential = False
|
||
next_position = 1
|
||
index = text.find("%")
|
||
while index != -1:
|
||
match = SPECIFIER.match(text, index)
|
||
if match is None:
|
||
result.invalid.append(text[index : index + 2])
|
||
index = text.find("%", index + 1)
|
||
continue
|
||
spec = match["spec"]
|
||
if spec == ESCAPED_PERCENT:
|
||
result.escaped_percent = True
|
||
elif spec.endswith(ESCAPED_PERCENT):
|
||
result.invalid.append(match.group(0))
|
||
else:
|
||
if match["position"]:
|
||
positional = True
|
||
position = int(match["position"])
|
||
else:
|
||
sequential = True
|
||
position = next_position
|
||
next_position += 1
|
||
if result.arguments.setdefault(position, spec) != spec:
|
||
result.invalid.append(match.group(0))
|
||
index = text.find("%", match.end())
|
||
result.mixed = positional and sequential
|
||
return result
|
||
|
||
|
||
def describe_arguments(arguments: dict[int, str]) -> str:
|
||
return ", ".join(f"%{position}${spec}" for position, spec in sorted(arguments.items())) or "none"
|
||
|
||
|
||
@functools.cache
|
||
def links(text: str) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
||
"""Markdown link targets, and bare URLs outside them."""
|
||
return tuple(LINK_TARGET.findall(text)), tuple(BARE_URL.findall(LINK_TARGET.sub("", text)))
|
||
|
||
|
||
def mask_links(text: str) -> str:
|
||
return BARE_URL.sub(LINK_MASK, LINK_TARGET.sub(f"]({LINK_MASK})", text))
|
||
|
||
|
||
def link_problem(source: str, target: str) -> str | None:
|
||
if not any(marker in text for text in (source, target) for marker in LINK_MARKERS):
|
||
return None
|
||
for kind, expected, found in zip(("markdown link", "URL"), links(source), links(target)):
|
||
missing = [link for link in expected if link not in found]
|
||
if missing:
|
||
return f"{kind} changed or missing: {missing[0][:LINK_PREVIEW_LENGTH]!r}"
|
||
extra = [link for link in found if link not in expected]
|
||
if extra or sorted(expected) != sorted(found):
|
||
return f"{kind} not in the source: {(extra or found)[0][:LINK_PREVIEW_LENGTH]!r}"
|
||
return None
|
||
|
||
|
||
def placeholder_problem(source: str, target: str) -> str | None:
|
||
"""Each placeholder must be spelled as in the source; only its argument number may differ."""
|
||
expected = parse_placeholders(mask_links(source))
|
||
found = parse_placeholders(mask_links(target))
|
||
if not expected.is_format:
|
||
if found.arguments:
|
||
return f"placeholders not in the source: {describe_arguments(found.arguments)}"
|
||
return None
|
||
if found.invalid:
|
||
return "invalid placeholders " + ", ".join(repr(s) for s in found.invalid)
|
||
if found.mixed:
|
||
return "positional and non-positional placeholders mixed"
|
||
if found.arguments != expected.arguments:
|
||
return f"expected {describe_arguments(expected.arguments)}, found {describe_arguments(found.arguments)}"
|
||
return None
|
||
|
||
|
||
def skip_strings_trivia(text: str, index: int) -> int:
|
||
while index < len(text):
|
||
if text[index].isspace():
|
||
index += 1
|
||
elif text.startswith("/*", index):
|
||
end = text.find("*/", index + 2)
|
||
if end == -1:
|
||
raise StringsSyntaxError(f"unterminated comment at offset {index}")
|
||
index = end + 2
|
||
elif text.startswith("//", index):
|
||
end = text.find("\n", index)
|
||
index = len(text) if end == -1 else end + 1
|
||
else:
|
||
break
|
||
return index
|
||
|
||
|
||
def read_strings_literal(text: str, index: int) -> tuple[str, int]:
|
||
if not text.startswith('"', index):
|
||
raise StringsSyntaxError(f'expected " at offset {index}')
|
||
chars = []
|
||
index += 1
|
||
while index < len(text):
|
||
char = text[index]
|
||
if char == '"':
|
||
return "".join(chars), index + 1
|
||
if char == "\\" and index + 1 < len(text):
|
||
unicode = UNICODE_ESCAPE.match(text, index + 1)
|
||
if unicode:
|
||
chars.append(chr(int(unicode.group(1), 16)))
|
||
index = unicode.end()
|
||
else:
|
||
chars.append(STRINGS_ESCAPES.get(text[index + 1], text[index + 1]))
|
||
index += 2
|
||
continue
|
||
chars.append(char)
|
||
index += 1
|
||
raise StringsSyntaxError("unterminated string literal")
|
||
|
||
|
||
def expect_strings_token(text: str, index: int, token: str) -> int:
|
||
index = skip_strings_trivia(text, index)
|
||
if not text.startswith(token, index):
|
||
raise StringsSyntaxError(f"expected {token!r} at offset {index}")
|
||
return index + len(token)
|
||
|
||
|
||
def parse_strings(text: str) -> list[tuple[str, str]]:
|
||
entries = []
|
||
index = skip_strings_trivia(text, 0)
|
||
while index < len(text):
|
||
key, index = read_strings_literal(text, index)
|
||
index = expect_strings_token(text, index, "=")
|
||
value, index = read_strings_literal(text, skip_strings_trivia(text, index))
|
||
index = expect_strings_token(text, index, ";")
|
||
entries.append((key, value))
|
||
index = skip_strings_trivia(text, index)
|
||
return entries
|
||
|
||
|
||
def app_languages() -> list[str]:
|
||
names = sorted(p.name[: -len(".lproj")] for p in IOS_DIR.glob("*.lproj") if p.is_dir())
|
||
return sorted(names, key=lambda name: name != SOURCE_LANGUAGE)
|
||
|
||
|
||
def language_of(path: Path) -> str:
|
||
if path.suffix == ".strings":
|
||
return path.parent.name[: -len(".lproj")]
|
||
return path.parent.name if path.suffix == ".xml" else path.stem
|
||
|
||
|
||
def xcloc_path(language: str) -> Path:
|
||
return LOCALIZATIONS_DIR / f"{language}.xcloc"
|
||
|
||
|
||
def xliff_path(language: str) -> Path:
|
||
return xcloc_path(language) / "Localized Contents" / f"{language}.xliff"
|
||
|
||
|
||
def translation_xliffs() -> list[Path]:
|
||
return [
|
||
path
|
||
for path in sorted(LOCALIZATIONS_DIR.glob("*.xcloc/Localized Contents/*.xliff"))
|
||
if path.stem != SOURCE_LANGUAGE
|
||
]
|
||
|
||
|
||
def translation_strings() -> list[Path]:
|
||
return [
|
||
path
|
||
for path in sorted(IOS_DIR.glob("**/*.lproj/*.strings"))
|
||
if language_of(path) != SOURCE_LANGUAGE and LOCALIZATIONS_DIR not in path.parents
|
||
]
|
||
|
||
|
||
def lint_with_plutil(paths: list[Path]) -> list[Problem]:
|
||
"""Apple's own parser, as the app build uses; only on macOS."""
|
||
if shutil.which("plutil") is None or not paths:
|
||
return []
|
||
result = subprocess.run(["plutil", "-lint", *map(str, paths)], capture_output=True, text=True)
|
||
problems = []
|
||
for line in result.stdout.splitlines() + result.stderr.splitlines():
|
||
name, _, message = line.partition(": ")
|
||
if message and message != "OK":
|
||
problems.append(Problem(SYNTAX, Path(name), "", f"plutil: {message}"))
|
||
return problems
|
||
|
||
|
||
def translation_problems(path: Path, key: str, source: str, target: str) -> list[Problem]:
|
||
link = link_problem(source, target)
|
||
if link:
|
||
# A broken link leaves its percent-encoded URL unmasked, which would also read as placeholders.
|
||
return [Problem(LINK, path, key, link, target)]
|
||
placeholder = placeholder_problem(source, target)
|
||
return [Problem(PLACEHOLDER, path, key, placeholder, target)] if placeholder else []
|
||
|
||
|
||
def unescape_swift(literal: str) -> str:
|
||
def replace(match: re.Match[str]) -> str:
|
||
escaped = match.group(1)
|
||
if escaped.startswith("u{"):
|
||
return chr(int(escaped[2:-1], 16))
|
||
return STRINGS_ESCAPES.get(escaped, escaped)
|
||
|
||
return SWIFT_ESCAPE.sub(replace, literal)
|
||
|
||
|
||
def swift_string_literals() -> dict[str, str]:
|
||
"""Text of every string literal in the app's Swift code, with the first place it appears."""
|
||
literals: dict[str, str] = {}
|
||
for path in sorted(IOS_DIR.rglob("*.swift")):
|
||
text = read_file(path)
|
||
line_starts = [match.start() for match in NEWLINE.finditer(text)]
|
||
for match in SWIFT_LITERAL.finditer(text):
|
||
if SWIFT_INTERPOLATION in match.group(1):
|
||
continue
|
||
literal = unescape_swift(match.group(1))
|
||
# Literals without letters, such as " (" or "\n", are punctuation rather than translatable text.
|
||
if not any(char.isalpha() for char in literal):
|
||
continue
|
||
line = bisect.bisect_left(line_starts, match.start()) + 1
|
||
literals.setdefault(literal, f"{path}:{line}")
|
||
return literals
|
||
|
||
|
||
def removed_but_used(path: Path, key: str, literals: dict[str, str]) -> Problem | None:
|
||
location = literals.get(key)
|
||
if location is None:
|
||
return None
|
||
return Problem(IN_CODE, path, key, f"not in {SOURCE_LANGUAGE}.xliff, but the Swift code has this string at {location}")
|
||
|
||
|
||
def check_xliff(path: Path, source_keys: set[UnitKey], literals: dict[str, str]) -> tuple[list[Problem], int]:
|
||
try:
|
||
units = parse_units(read_file(path))
|
||
except ET.ParseError as error:
|
||
return [Problem(SYNTAX, path, "", f"invalid XML: {error}")], 0
|
||
problems = []
|
||
translated = 0
|
||
occurrences: dict[UnitKey, int] = {}
|
||
for unit_key, _unit in units:
|
||
occurrences[unit_key] = occurrences.get(unit_key, 0) + 1
|
||
for unit_key, count in occurrences.items():
|
||
if count > 1:
|
||
problems.append(Problem(DUPLICATE, path, unit_key[1], f"{count} units with this id in {unit_key[0]}"))
|
||
if source_keys and unit_key not in source_keys:
|
||
used = removed_but_used(path, unit_key[1], literals)
|
||
problems.append(used or Problem(STALE, path, unit_key[1], f"not in {SOURCE_LANGUAGE}.xliff, Xcode's export removes it"))
|
||
for (_file, key), unit in units:
|
||
if unit.target == "":
|
||
problems.append(Problem(BLANK, path, key, "empty target, the import would show it blank"))
|
||
elif unit.target is not None:
|
||
translated += 1
|
||
problems += translation_problems(path, key, unit.source, unit.target)
|
||
return problems, translated
|
||
|
||
|
||
def check_strings(path: Path) -> list[Problem]:
|
||
try:
|
||
entries = parse_strings(read_file(path))
|
||
except StringsSyntaxError as error:
|
||
return [Problem(SYNTAX, path, "", str(error))]
|
||
problems = []
|
||
for key, value in entries:
|
||
if value == "":
|
||
problems.append(Problem(BLANK, path, key, "empty value, shown blank in the app"))
|
||
else:
|
||
problems += translation_problems(path, key, key, value)
|
||
return problems
|
||
|
||
|
||
def check_files(stats: dict[str, LanguageStats], literals: dict[str, str]) -> list[Problem]:
|
||
problems = []
|
||
source_xliff = xliff_path(SOURCE_LANGUAGE)
|
||
source_keys = {key for key, _ in parse_units(read_file(source_xliff))} if source_xliff.is_file() else set()
|
||
for path in translation_xliffs():
|
||
xliff_problems, translated = check_xliff(path, source_keys, literals)
|
||
stats.setdefault(path.stem, LanguageStats()).translated = translated
|
||
problems += xliff_problems
|
||
reported = {(p.language, p.key, p.kind, p.detail) for p in problems}
|
||
strings = translation_strings()
|
||
for path in strings:
|
||
# The import copies XLIFF translations into .strings, so only problems of their own are listed.
|
||
problems += [p for p in check_strings(path) if (p.language, p.key, p.kind, p.detail) not in reported]
|
||
return problems + lint_with_plutil(strings)
|
||
|
||
|
||
def translation_changes(
|
||
path: Path,
|
||
saved: dict[UnitKey, SavedTranslation],
|
||
after: list[Segment],
|
||
source_keys: set[UnitKey],
|
||
literals: dict[str, str],
|
||
) -> tuple[list[Problem], int]:
|
||
"""Translations lost or changed by the round trip, and the count removed together with their source."""
|
||
translated_after: dict[UnitKey, Unit] = {}
|
||
for block in unit_blocks(after):
|
||
if block.unit.translated:
|
||
translated_after.setdefault(block.key, block.unit)
|
||
problems = []
|
||
removed = 0
|
||
for key, translation in saved.items():
|
||
result = translated_after.get(key)
|
||
if result is None:
|
||
if key in source_keys:
|
||
problems.append(Problem(LOST, path, key[1], "translation lost", translation.target))
|
||
else:
|
||
removed += 1
|
||
used = removed_but_used(path, key[1], literals)
|
||
if used:
|
||
used.text = translation.target
|
||
problems.append(used)
|
||
elif result.target != translation.target:
|
||
problems.append(Problem(CHANGED, path, key[1], f"was {translation.target!r}", result.target or ""))
|
||
return problems, removed
|
||
|
||
|
||
def kotlin_possible_formats() -> list[str]:
|
||
"""The placeholders adjustFormatting accepts, read from the build script it is defined in."""
|
||
match = POSSIBLE_FORMAT.search(read_file(KOTLIN_BUILD_SCRIPT))
|
||
if match is None:
|
||
raise UpdateError(f"possibleFormat not found in {KOTLIN_BUILD_SCRIPT}")
|
||
return [item.replace("\\$", "$") for item in re.findall(r'"([^"]*)"', match.group(1))]
|
||
|
||
|
||
def substring_after(text: str, delimiter: str) -> str:
|
||
"""Kotlin's substringAfter: the whole text when the delimiter is missing."""
|
||
_, found, rest = text.partition(delimiter)
|
||
return rest if found else text
|
||
|
||
|
||
def remove_cdata(element: str) -> str:
|
||
if "<![CDATA" not in element:
|
||
return element
|
||
return element.replace("<![CDATA[", "").replace("]]></string>", "</string>")
|
||
|
||
|
||
def kotlin_formats(element: str, possible: list[str]) -> list[str]:
|
||
"""Port of formatting() in adjustFormatting, so the result decides the build the same way."""
|
||
if "%" not in element:
|
||
return []
|
||
value = substring_after(element, '">').rpartition("</string>")[0]
|
||
formats = []
|
||
rest = substring_after(value, "%")
|
||
while True:
|
||
found = next((candidate for candidate in possible if rest.startswith(candidate)), None)
|
||
if found is None:
|
||
raise KotlinFormatError(f"unknown placeholder {'%' + rest[:3]!r}, adjustFormatting fails the build")
|
||
formats.append(found)
|
||
previous = rest
|
||
rest = substring_after(rest, "%")
|
||
if len(previous) == len(rest):
|
||
break
|
||
if not any(item.startswith(KOTLIN_POSITIONS) for item in formats):
|
||
return formats
|
||
if not all(item.partition("$")[0].isdigit() for item in formats):
|
||
raise KotlinFormatError("positional and non-positional placeholders mixed, adjustFormatting fails the build")
|
||
return sorted(formats, key=lambda item: int(item.partition("$")[0]))
|
||
|
||
|
||
def kotlin_add_cdata(line: str) -> str:
|
||
"""Port of addCDATA in adjustFormatting: wraps HTML strings in CDATA, raises for HTML it rejects."""
|
||
# ANY_HTML needs a "<", ">", "<" or ">" besides the two tags of the string itself.
|
||
if line.count("<") + line.count(">") <= KOTLIN_STRING_TAG_BRACKETS and "<" not in line and ">" not in line:
|
||
return line
|
||
if not ANY_HTML.fullmatch(line):
|
||
return line
|
||
prepared = line
|
||
if FONT_LT_GT.fullmatch(line) or UNBRACKETED_COLOR.search(line):
|
||
prepared = line.replace("<", "<").replace(">", ">")
|
||
prepared = UNBRACKETED_COLOR.sub(lambda m: m.group(0).replace("color=#", 'color="#') + '"', prepared)
|
||
opening = prepared.count("<")
|
||
if opening != prepared.count(">") or opening != prepared.count("</") * 2 or not CORRECT_HTML.fullmatch(prepared):
|
||
raise KotlinFormatError(KOTLIN_HTML_ERROR)
|
||
wrapped = KOTLIN_START_STRING.sub(lambda m: m.group(0) + "<![CDATA[", prepared)
|
||
return KOTLIN_END_STRING.sub(lambda m: "]]>" + m.group(0), wrapped)
|
||
|
||
|
||
@functools.cache
|
||
def kotlin_text(raw_value: str) -> str:
|
||
"""The text Android shows: CDATA unwrapped, XML entities and Android escapes resolved."""
|
||
unwrapped = CDATA.sub(lambda m: m.group(1), raw_value)
|
||
text = html.unescape(unwrapped)
|
||
|
||
def replace(match: re.Match[str]) -> str:
|
||
escaped = match.group(1)
|
||
return chr(int(escaped[1:], 16)) if escaped.startswith("u") else STRINGS_ESCAPES.get(escaped, escaped)
|
||
|
||
return KOTLIN_ESCAPE.sub(replace, text)
|
||
|
||
|
||
def describe_kotlin_formats(formats: list[str]) -> str:
|
||
return ", ".join(f"%{item}" for item in formats) or "none"
|
||
|
||
|
||
def kotlin_string_files() -> list[Path]:
|
||
return [
|
||
path
|
||
for path in sorted(KOTLIN_RESOURCES.glob("*/strings.xml"))
|
||
if path.parent.name == KOTLIN_BASE or KOTLIN_LOCALE.match(path.parent.name)
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class KotlinString:
|
||
name: str
|
||
# The string as formatting() in adjustFormatting sees it: after removeCDATA and addCDATA, lines joined.
|
||
line: str
|
||
raw_value: str
|
||
html_error: str | None
|
||
|
||
|
||
@dataclass
|
||
class KotlinFile:
|
||
lines: list[str]
|
||
changed: bool
|
||
strings: list[KotlinString]
|
||
unclosed: str | None
|
||
|
||
|
||
def kotlin_lines(text: str) -> list[str]:
|
||
lines = KOTLIN_LINE_BREAK.split(text)
|
||
if lines and lines[-1] == "":
|
||
lines.pop()
|
||
return lines
|
||
|
||
|
||
def gradle_string_id(line: str) -> str:
|
||
return line.replace('<string name="', "").strip().partition('"')[0]
|
||
|
||
|
||
def adjust_kotlin(text: str) -> KotlinFile:
|
||
"""Port of the line loop of adjustFormatting: the adjusted lines and the strings it compares."""
|
||
initial = kotlin_lines(text)
|
||
final: list[str] = []
|
||
strings: list[KotlinString] = []
|
||
multiline: list[str] = []
|
||
|
||
def adjust(original: list[str]) -> list[str]:
|
||
joined = "\n".join(original)
|
||
error = None
|
||
try:
|
||
fixed = kotlin_add_cdata(remove_cdata(joined))
|
||
except KotlinFormatError as format_error:
|
||
fixed, error = joined, str(format_error)
|
||
fixed_lines = fixed.split("\n")
|
||
flat = "".join(fixed_lines)
|
||
match = KOTLIN_STRING.search(joined)
|
||
strings.append(KotlinString(gradle_string_id(flat), flat, match["value"] if match else "", error))
|
||
return fixed_lines
|
||
|
||
for line in initial:
|
||
if KOTLIN_STRING_LINE.fullmatch(line):
|
||
final += adjust([line])
|
||
elif not multiline and KOTLIN_START_STRING.search(line):
|
||
multiline.append(line)
|
||
elif multiline and KOTLIN_END_STRING.search(line):
|
||
multiline.append(line)
|
||
final += adjust(multiline)
|
||
multiline = []
|
||
elif multiline:
|
||
multiline.append(line)
|
||
else:
|
||
final.append(line)
|
||
unclosed = "\n".join(multiline) if multiline else None
|
||
return KotlinFile(final, final != initial, strings, unclosed)
|
||
|
||
|
||
def kotlin_string_problems(
|
||
path: Path, string: KotlinString, base: KotlinString | None, base_formats: list[str] | None, possible: list[str]
|
||
) -> list[Problem]:
|
||
name = string.name
|
||
text = kotlin_text(string.raw_value)
|
||
if base is None:
|
||
return [Problem(UNKNOWN, path, name, "not in base/strings.xml, adjustFormatting fails the build", text)]
|
||
if text == "":
|
||
return [Problem(BLANK, path, name, "empty string, shown blank in the app")]
|
||
problems = []
|
||
try:
|
||
formats = kotlin_formats(string.line, possible)
|
||
if base_formats is not None and formats != base_formats:
|
||
detail = f"expected {describe_kotlin_formats(base_formats)}, found {describe_kotlin_formats(formats)}"
|
||
problems.append(Problem(PLACEHOLDER, path, name, detail, text))
|
||
except KotlinFormatError as error:
|
||
problems.append(Problem(PLACEHOLDER, path, name, str(error), text))
|
||
if string.html_error:
|
||
problems.append(Problem(HTML, path, name, string.html_error, text))
|
||
outside_cdata = CDATA.sub("", string.raw_value)
|
||
if UNESCAPED_APOSTROPHE.search(outside_cdata) and not (outside_cdata.startswith('"') and outside_cdata.endswith('"')):
|
||
problems.append(Problem(SYNTAX, path, name, "unescaped apostrophe, Android needs \\'", text))
|
||
link = link_problem(kotlin_text(base.raw_value), text)
|
||
if link:
|
||
problems.append(Problem(LINK, path, name, link, text))
|
||
return problems
|
||
|
||
|
||
def check_kotlin_file(
|
||
path: Path, adjusted: KotlinFile, base: dict[str, KotlinString], base_formats: dict[str, list[str]], possible: list[str]
|
||
) -> list[Problem]:
|
||
problems = []
|
||
if adjusted.unclosed:
|
||
problems.append(Problem(SYNTAX, path, "", "unclosed string tag, adjustFormatting fails the build", adjusted.unclosed))
|
||
seen: set[str] = set()
|
||
for string in adjusted.strings:
|
||
if string.name in seen:
|
||
problems.append(Problem(DUPLICATE, path, string.name, "duplicate string name, the app uses only one of them"))
|
||
seen.add(string.name)
|
||
problems += kotlin_string_problems(path, string, base.get(string.name), base_formats.get(string.name), possible)
|
||
return problems
|
||
|
||
|
||
def invalid_xml(path: Path, text: str) -> Problem | None:
|
||
try:
|
||
ET.fromstring(text)
|
||
except ET.ParseError as error:
|
||
return Problem(SYNTAX, path, "", f"invalid XML: {error}")
|
||
return None
|
||
|
||
|
||
def check_kotlin_files(stats: dict[str, LanguageStats], write: bool = False) -> tuple[list[Problem], list[Path]]:
|
||
"""Problems of all strings.xml files and, when writing, the files rewritten as adjustFormatting does."""
|
||
possible = kotlin_possible_formats()
|
||
files = kotlin_string_files()
|
||
base_path = next((path for path in files if path.parent.name == KOTLIN_BASE), None)
|
||
if base_path is None:
|
||
raise UpdateError(f"{KOTLIN_RESOURCES}/{KOTLIN_BASE}/strings.xml not found, run from the repository root")
|
||
texts = {path: read_file(path) for path in files}
|
||
adjusted = {path: adjust_kotlin(text) for path, text in texts.items()}
|
||
problems: list[Problem] = []
|
||
base = {string.name: string for string in adjusted[base_path].strings}
|
||
base_formats: dict[str, list[str]] = {}
|
||
for string in base.values():
|
||
try:
|
||
base_formats[string.name] = kotlin_formats(string.line, possible)
|
||
except KotlinFormatError as error:
|
||
problems.append(Problem(PLACEHOLDER, base_path, string.name, str(error), kotlin_text(string.raw_value)))
|
||
if string.html_error:
|
||
problems.append(Problem(HTML, base_path, string.name, string.html_error, kotlin_text(string.raw_value)))
|
||
base_fails = bool(problems)
|
||
rewritten = []
|
||
for path in files:
|
||
xml_problem = invalid_xml(path, texts[path])
|
||
if xml_problem:
|
||
problems.append(xml_problem)
|
||
if path != base_path:
|
||
stats.setdefault(path.parent.name, LanguageStats()).translated = 0
|
||
continue
|
||
file_problems = [] if path == base_path else check_kotlin_file(path, adjusted[path], base, base_formats, possible)
|
||
if path != base_path:
|
||
stats.setdefault(path.parent.name, LanguageStats()).translated = len(adjusted[path].strings)
|
||
problems += file_problems
|
||
builds = not base_fails and not adjusted[path].unclosed and not any(p.kind in KOTLIN_BUILD_FAILURES for p in file_problems)
|
||
if write and builds and adjusted[path].changed and path.parent.name not in KOTLIN_UNADJUSTED_LOCALES:
|
||
# adjustFormatting writes the lines back without a final line break.
|
||
write_file(path, "\n".join(adjusted[path].lines))
|
||
rewritten.append(path)
|
||
return problems, rewritten
|
||
|
||
|
||
class Steps:
|
||
def __init__(self, total: int) -> None:
|
||
self.total = total
|
||
self.current = 0
|
||
|
||
@contextlib.contextmanager
|
||
def step(self, title: str) -> Iterator[None]:
|
||
self.current += 1
|
||
print(paint(f"[{self.current}/{self.total}] ", BOLD, CYAN) + paint(title, BOLD), flush=True)
|
||
started = time.monotonic()
|
||
yield
|
||
print(paint(f" done in {format_duration(time.monotonic() - started)}", GREEN), flush=True)
|
||
|
||
|
||
def format_duration(seconds: float) -> str:
|
||
minutes, seconds = divmod(int(round(seconds)), 60)
|
||
return f"{minutes}m {seconds:02d}s" if minutes else f"{seconds}s"
|
||
|
||
|
||
def print_summary(stats: dict[str, LanguageStats], problems: list[Problem], in_app: set[str]) -> None:
|
||
counts: dict[str, dict[str, int]] = {}
|
||
for problem in problems:
|
||
language_counts = counts.setdefault(problem.language, {})
|
||
language_counts[problem.kind] = language_counts.get(problem.kind, 0) + 1
|
||
headers = [*SUMMARY_FIXED_COLUMNS, "restored", "removed", *PROBLEM_KINDS]
|
||
rows = []
|
||
for language in sorted(set(stats) | set(counts)):
|
||
entry = stats.get(language, LanguageStats())
|
||
language_counts = counts.get(language, {})
|
||
cells = [
|
||
(language, BOLD if language_counts else ""),
|
||
("yes", "") if language in in_app else ("no", DIM),
|
||
(str(entry.translated), ""),
|
||
(str(entry.restored or ""), YELLOW),
|
||
(str(entry.removed or ""), DIM),
|
||
]
|
||
cells += [(str(language_counts.get(kind, "")), KIND_COLORS[kind]) for kind in PROBLEM_KINDS]
|
||
rows.append(cells)
|
||
always_shown = range(len(SUMMARY_FIXED_COLUMNS))
|
||
shown = [i for i in range(len(headers)) if i in always_shown or any(row[i][0] for row in rows)]
|
||
headers = [headers[i] for i in shown]
|
||
rows = [[row[i] for i in shown] for row in rows]
|
||
widths = [max([len(headers[i]), *(len(row[i][0]) for row in rows)]) for i in range(len(headers))]
|
||
print()
|
||
print(" " + " ".join(paint(h.ljust(w) if i == 0 else h.rjust(w), DIM) for i, (h, w) in enumerate(zip(headers, widths))))
|
||
for row in rows:
|
||
padded = [(text.ljust(w) if i == 0 else text.rjust(w), color) for i, ((text, color), w) in enumerate(zip(row, widths))]
|
||
print(" " + " ".join(paint(text, color) if text.strip() and color else text for text, color in padded))
|
||
|
||
|
||
def preview(text: str) -> str:
|
||
return text if len(text) <= KEY_PREVIEW_LENGTH else text[:KEY_PREVIEW_LENGTH] + "…"
|
||
|
||
|
||
def print_problems(problems: list[Problem]) -> None:
|
||
by_path: dict[Path, list[Problem]] = {}
|
||
for problem in sorted(problems, key=lambda p: (p.language, str(p.path))):
|
||
by_path.setdefault(problem.path, []).append(problem)
|
||
for path, path_problems in by_path.items():
|
||
print("\n " + paint(f"[{path_problems[0].language}]", BOLD, CYAN) + " " + paint(str(path), BOLD))
|
||
for grouped_kind in sorted(GROUPED_KINDS, key=PROBLEM_KINDS.index):
|
||
grouped = [p for p in path_problems if p.kind == grouped_kind]
|
||
if not grouped:
|
||
continue
|
||
examples = ", ".join(repr(p.key) for p in grouped[:GROUP_EXAMPLES])
|
||
more = f" and {len(grouped) - GROUP_EXAMPLES} more" if len(grouped) > GROUP_EXAMPLES else ""
|
||
print(f" {paint(f'{grouped_kind:<11}', KIND_COLORS[grouped_kind])} {len(grouped)} x {grouped[0].detail}")
|
||
print(paint(f" {examples}{more}", DIM))
|
||
for problem in path_problems:
|
||
if problem.kind in GROUPED_KINDS:
|
||
continue
|
||
kind = paint(f"{problem.kind:<11}", KIND_COLORS[problem.kind])
|
||
print(f" {kind} {preview(problem.key)!r}" if problem.key else f" {kind}")
|
||
print(f" {problem.detail}")
|
||
if problem.text:
|
||
print(paint(f" -> {problem.text!r}", DIM))
|
||
|
||
|
||
def count_line(label: str, problems: list[Problem]) -> str:
|
||
kinds: dict[str, int] = {}
|
||
for problem in problems:
|
||
kinds[problem.kind] = kinds.get(problem.kind, 0) + 1
|
||
counts = ", ".join(paint(f"{kinds[kind]} {kind}", KIND_COLORS[kind]) for kind in PROBLEM_KINDS if kind in kinds)
|
||
return paint(f"{len(problems)} {label}: ", BOLD) + counts
|
||
|
||
|
||
def report(stats: dict[str, LanguageStats], problems: list[Problem], in_app: set[str]) -> int:
|
||
"""Exit code 1 for problems; warnings are listed but do not fail."""
|
||
print_summary(stats, problems, in_app)
|
||
print_problems(problems)
|
||
errors = [p for p in problems if p.kind not in WARNING_KINDS]
|
||
warnings = [p for p in problems if p.kind in WARNING_KINDS]
|
||
print()
|
||
if warnings:
|
||
print(count_line("warning(s)", warnings))
|
||
if not errors:
|
||
print(paint("No problems found.", BOLD, GREEN))
|
||
return 0
|
||
print(count_line("problem(s)", errors))
|
||
print("Fix them in Weblate, so the next Weblate commit does not bring them back.")
|
||
return 1
|
||
|
||
|
||
def require_update_environment() -> None:
|
||
if platform.system() != "Darwin" or shutil.which("xcodebuild") is None:
|
||
raise UpdateError("update needs macOS with Xcode; check runs on any platform")
|
||
if not PROJECT.is_dir():
|
||
raise UpdateError(f"{PROJECT} not found, run from the repository root")
|
||
status = subprocess.run(["git", "status", "--porcelain", "--", str(IOS_DIR)], capture_output=True, text=True, check=True)
|
||
if status.stdout.strip():
|
||
raise UpdateError(f"uncommitted changes under {IOS_DIR}, commit or stash them first")
|
||
|
||
|
||
def xcodebuild(arguments: list[str], log: Path) -> None:
|
||
with log.open("w", encoding="utf-8") as output:
|
||
result = subprocess.run(["xcodebuild", *arguments], stdout=output, stderr=subprocess.STDOUT)
|
||
if result.returncode != 0:
|
||
tail = log.read_text(encoding="utf-8", errors="replace").splitlines()[-LOG_TAIL_LINES:]
|
||
raise UpdateError(f"xcodebuild {arguments[0]} failed, full log: {log}\n" + "\n".join(tail))
|
||
|
||
|
||
def update_pbxproj_libraries() -> None:
|
||
subprocess.run(["sh", str(UPDATE_PBXPROJ)], check=True, stdout=subprocess.DEVNULL)
|
||
|
||
|
||
@contextlib.contextmanager
|
||
def local_core_libraries() -> Iterator[None]:
|
||
"""Xcode links the app to extract strings, so the project temporarily references the local core libraries."""
|
||
pbxproj = PBXPROJ.read_bytes()
|
||
try:
|
||
update_pbxproj_libraries()
|
||
yield
|
||
finally:
|
||
PBXPROJ.write_bytes(pbxproj)
|
||
|
||
|
||
def import_languages(languages: list[str], log_dir: Path) -> None:
|
||
for language in languages:
|
||
started = time.monotonic()
|
||
xcodebuild(
|
||
["-importLocalizations", "-project", str(PROJECT), "-localizationPath", str(xcloc_path(language)), "-skipPackageUpdates"],
|
||
log_dir / f"import-{language}.log",
|
||
)
|
||
print(f" {language:<8} " + paint(format_duration(time.monotonic() - started), DIM), flush=True)
|
||
|
||
|
||
def export_languages(languages: list[str], log_dir: Path) -> None:
|
||
export_arguments = [argument for language in languages for argument in ("-exportLanguage", language)]
|
||
xcodebuild(
|
||
["-exportLocalizations", "-project", str(PROJECT), "-localizationPath", str(LOCALIZATIONS_DIR), *export_arguments],
|
||
log_dir / "export.log",
|
||
)
|
||
|
||
|
||
def read_xliff(path: Path) -> list[Segment]:
|
||
try:
|
||
return split_xliff(read_file(path))
|
||
except (ET.ParseError, ValueError) as error:
|
||
raise UpdateError(f"{path}: {error}") from error
|
||
|
||
|
||
def update() -> int:
|
||
require_update_environment()
|
||
languages = app_languages()
|
||
translated = [language for language in languages if language != SOURCE_LANGUAGE]
|
||
stats = {language: LanguageStats() for language in translated}
|
||
steps = Steps(5)
|
||
|
||
with steps.step(f"Preparing {len(translated)} translation files"):
|
||
weblate: dict[str, dict[UnitKey, SavedTranslation]] = {}
|
||
stripped_total = 0
|
||
for language in translated:
|
||
segments = read_xliff(xliff_path(language))
|
||
weblate[language] = saved_translations(segments)
|
||
count = strip_empty_targets(segments)
|
||
if count:
|
||
write_file(xliff_path(language), join_xliff(segments))
|
||
stripped_total += count
|
||
print(paint(f" removed {stripped_total} empty targets that the import would show blank", DIM))
|
||
|
||
log_dir = Path(tempfile.mkdtemp(prefix="ios-localizations-"))
|
||
try:
|
||
with local_core_libraries():
|
||
with steps.step(f"Importing {len(languages)} languages with Xcode (logs: {log_dir})"):
|
||
import_languages(languages, log_dir)
|
||
with steps.step(f"Exporting {len(languages)} languages with Xcode"):
|
||
export_languages(languages, log_dir)
|
||
except (UpdateError, subprocess.CalledProcessError) as error:
|
||
raise UpdateError(f"{error}\nRestore the files with: git checkout -- {IOS_DIR}") from error
|
||
|
||
problems: list[Problem] = []
|
||
with steps.step("Restoring translations dropped by Xcode and removing untranslated units"):
|
||
literals = swift_string_literals()
|
||
source_keys = {key for key, _ in parse_units(read_file(xliff_path(SOURCE_LANGUAGE)))}
|
||
for language in translated:
|
||
path = xliff_path(language)
|
||
exported = read_xliff(path)
|
||
stats[language].restored = restore_translations(exported, weblate[language])
|
||
result = drop_untranslated_units(exported)
|
||
write_file(path, join_xliff(result))
|
||
changes, stats[language].removed = translation_changes(path, weblate[language], result, source_keys, literals)
|
||
problems += changes
|
||
|
||
with steps.step("Linting .strings files and checking translations"):
|
||
problems += check_files(stats, literals)
|
||
|
||
return report(stats, problems, set(languages))
|
||
|
||
|
||
def check() -> int:
|
||
stats: dict[str, LanguageStats] = {}
|
||
steps = Steps(1)
|
||
with steps.step("Linting .strings files and checking translations"):
|
||
problems = check_files(stats, swift_string_literals())
|
||
return report(stats, problems, set(app_languages()))
|
||
|
||
|
||
def check_kotlin() -> int:
|
||
stats: dict[str, LanguageStats] = {}
|
||
steps = Steps(1)
|
||
with steps.step("Checking Android and desktop strings.xml files"):
|
||
problems, _ = check_kotlin_files(stats)
|
||
return report(stats, problems, set(stats))
|
||
|
||
|
||
def update_kotlin() -> int:
|
||
stats: dict[str, LanguageStats] = {}
|
||
steps = Steps(1)
|
||
with steps.step("Applying adjustFormatting to strings.xml files and checking them"):
|
||
problems, rewritten = check_kotlin_files(stats, write=True)
|
||
names = ", ".join(path.parent.name for path in rewritten) or "none"
|
||
print(paint(f" rewrote {len(rewritten)} files as adjustFormatting does: {names}", DIM))
|
||
return report(stats, problems, set(stats))
|
||
|
||
|
||
def main(argv: list[str] | None = None) -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
commands = parser.add_subparsers(dest="command", required=True)
|
||
commands.add_parser("update", help="import, export, lint and check (macOS with Xcode)")
|
||
commands.add_parser("check", help="lint and check the iOS files only (any platform)")
|
||
commands.add_parser("check-kotlin", help="check the Android and desktop strings.xml files (any platform)")
|
||
commands.add_parser(
|
||
"update-kotlin", help="rewrite the strings.xml files as adjustFormatting does, then check them (any platform)"
|
||
)
|
||
arguments = parser.parse_args(argv)
|
||
run = {"update": update, "check": check, "check-kotlin": check_kotlin, "update-kotlin": update_kotlin}[arguments.command]
|
||
try:
|
||
return run()
|
||
except UpdateError as error:
|
||
print(paint("error: ", BOLD, RED) + str(error), file=sys.stderr)
|
||
return 2
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|