mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 08:37:30 +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>
669 lines
37 KiB
Python
669 lines
37 KiB
Python
#!/usr/bin/env python3
|
||
from __future__ import annotations
|
||
|
||
import contextlib
|
||
import io
|
||
import os
|
||
import re
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from pathlib import Path
|
||
from unittest import mock
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
import localizations as l10n # noqa: E402
|
||
|
||
LOCALIZABLE = "en.lproj/Localizable.strings"
|
||
NOTE = "No comment provided by engineer."
|
||
|
||
|
||
def unit_xml(unit_id: str, source: str, target: str | None = None, state: str | None = None) -> str:
|
||
lines = [f' <trans-unit id="{unit_id}" xml:space="preserve">\n', f" <source>{source}</source>\n"]
|
||
if target is not None:
|
||
attributes = f' state="{state}"' if state else ""
|
||
lines.append(f" <target{attributes}>{target}</target>\n" if target else f" <target{attributes}></target>\n")
|
||
lines += [f" <note>{NOTE}</note>\n", " </trans-unit>\n"]
|
||
return "".join(lines)
|
||
|
||
|
||
def xliff_xml(language: str, units: list[str], original: str = LOCALIZABLE) -> str:
|
||
return (
|
||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||
'<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">\n'
|
||
f' <file original="{original}" source-language="en" target-language="{language}" datatype="plaintext">\n'
|
||
" <body>\n" + "".join(units) + " </body>\n"
|
||
" </file>\n"
|
||
"</xliff>\n"
|
||
)
|
||
|
||
|
||
class RepositoryTestCase(unittest.TestCase):
|
||
"""Runs each test in a temporary directory laid out like the repository root."""
|
||
|
||
def setUp(self) -> None:
|
||
self.previous_cwd = os.getcwd()
|
||
self.directory = tempfile.TemporaryDirectory()
|
||
os.chdir(self.directory.name)
|
||
for language in ("en", "de", "uk"):
|
||
(l10n.IOS_DIR / f"{language}.lproj").mkdir(parents=True)
|
||
l10n.xliff_path(language).parent.mkdir(parents=True)
|
||
|
||
def tearDown(self) -> None:
|
||
os.chdir(self.previous_cwd)
|
||
self.directory.cleanup()
|
||
|
||
def write(self, path: Path, text: str) -> None:
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
l10n.write_file(path, text)
|
||
|
||
|
||
class PlaceholderTest(unittest.TestCase):
|
||
def test_parse_sequential_positional_and_spelling(self) -> None:
|
||
parsed = l10n.parse_placeholders("%@ sent %lld files, %02d left, %.1f%%")
|
||
self.assertEqual(parsed.arguments, {1: "@", 2: "lld", 3: "02d", 4: ".1f"})
|
||
self.assertTrue(parsed.escaped_percent)
|
||
self.assertEqual(parsed.invalid, [])
|
||
self.assertFalse(parsed.mixed)
|
||
self.assertEqual(l10n.parse_placeholders("%2$@ %1$lld").arguments, {1: "lld", 2: "@"})
|
||
|
||
def test_parse_invalid_mixed_and_conflicting(self) -> None:
|
||
self.assertEqual(l10n.parse_placeholders("%их файл").invalid, ["%и"])
|
||
self.assertEqual(l10n.parse_placeholders("%LLD groups").invalid, ["%L"])
|
||
self.assertEqual(l10n.parse_placeholders("% % off").invalid, ["% %"])
|
||
self.assertTrue(l10n.parse_placeholders("%1$@ and %@").mixed)
|
||
self.assertEqual(l10n.parse_placeholders("%1$@ %1$d").invalid, ["%1$d"])
|
||
self.assertFalse(l10n.parse_placeholders("100 percent").is_format)
|
||
|
||
def test_problems_found_in_weblate(self) -> None:
|
||
cases = {
|
||
("%lld messages moderated by %@", "%@ %lld üzenetet moderált"): "expected %1$lld, %2$@, found %1$@, %2$lld",
|
||
("%@ at %@:", "1$@, %2$@'de:"): "expected %1$@, %2$@, found %2$@",
|
||
("%d file(s) were deleted.", "%их файл(ів) було видалено."): "invalid placeholders '%и'",
|
||
("Desktop app version %@ is not compatible.", 'versión %" no es compatible.'): "invalid placeholders '%\"'",
|
||
("Error: %@", "错误: %@"): "expected %1$@, found none",
|
||
("You are already connecting!", "Ви вже підключаєтеся до %@."): "placeholders not in the source: %1$@",
|
||
("%lld new interface languages", "%d nové jazyky rozhraní"): "expected %1$lld, found %1$d",
|
||
("changed your role to %@", "你修改了 %1$@ 你的身份為 %@"): "positional and non-positional placeholders mixed",
|
||
("Sent at: %@", "Posláno v: % @"): "expected %1$@, found %1$ @",
|
||
("%1$@ changed to %2$@", "Role %1$@ byla změněna na %2$ @"): "expected %1$@, %2$@, found %1$@, %2$ @",
|
||
("%1$02d hrs", "%1$2d h"): "expected %1$02d, found %1$2d",
|
||
}
|
||
for (source, target), expected in cases.items():
|
||
with self.subTest(target=target):
|
||
self.assertEqual(l10n.placeholder_problem(source, target), expected)
|
||
|
||
def test_valid_translations_pass(self) -> None:
|
||
cases = [
|
||
("%lld messages moderated by %@", "%2$@ %1$lld üzenetet moderált"),
|
||
("%1$@ at %2$@:", "%1$@, %2$@'de:"),
|
||
("%lld%% off", "%lld%% Rabatt"),
|
||
("Battery at 100%", "Akku bei 100 %"),
|
||
("No placeholders", "Keine Platzhalter"),
|
||
("Join [group](simplex:/g#a%3Db%2F) by %@", "%@ tritt [Gruppe](simplex:/g#a%3Db%2F) bei"),
|
||
]
|
||
for source, target in cases:
|
||
with self.subTest(target=target):
|
||
self.assertIsNone(l10n.placeholder_problem(source, target))
|
||
|
||
def test_describe_arguments(self) -> None:
|
||
self.assertEqual(l10n.describe_arguments({2: "@", 1: "lld"}), "%1$lld, %2$@")
|
||
self.assertEqual(l10n.describe_arguments({}), "none")
|
||
|
||
|
||
class LinkTest(unittest.TestCase):
|
||
SOURCE = "Improved [directory bot](simplex:/contact#/?v=1-4%26dh%3DMCow) and https://simplex.chat/docs."
|
||
|
||
def test_links_and_masking(self) -> None:
|
||
self.assertEqual(l10n.links(self.SOURCE), (("simplex:/contact#/?v=1-4%26dh%3DMCow",), ("https://simplex.chat/docs.",)))
|
||
self.assertEqual(l10n.links("见 https://github.com/simplex-chat)!"), ((), ("https://github.com/simplex-chat",)))
|
||
self.assertEqual(l10n.mask_links(self.SOURCE), "Improved [directory bot](<link>) and <link>")
|
||
|
||
def test_link_problems(self) -> None:
|
||
intact = "Подобрен [бот](simplex:/contact#/?v=1-4%26dh%3DMCow) и https://simplex.chat/docs."
|
||
broken = "Подобрен [бот](simplex:/contact#/?v=1-4% 26dh%3DMCow) и https://simplex.chat/docs."
|
||
self.assertIsNone(l10n.link_problem(self.SOURCE, intact))
|
||
self.assertEqual(l10n.link_problem(self.SOURCE, broken), "markdown link changed or missing: 'simplex:/contact#/?v=1-4%26dh%3DMCow'")
|
||
self.assertEqual(l10n.link_problem("No link", "See https://evil.example"), "URL not in the source: 'https://evil.example'")
|
||
full_width = "Подобрен [бот](simplex:/contact#/?v=1-4%26dh%3DMCow) и https://simplex.chat/docs."
|
||
self.assertEqual(l10n.link_problem(self.SOURCE, full_width), "markdown link changed or missing: 'simplex:/contact#/?v=1-4%26dh%3DMCow'")
|
||
self.assertEqual(l10n.link_problem("[a](https://x.example) [b](https://x.example)", "[a](https://x.example)"), "markdown link not in the source: 'https://x.example'")
|
||
self.assertIsNone(l10n.link_problem("No link", "Kein Link"))
|
||
|
||
def test_broken_link_is_not_reported_as_placeholder(self) -> None:
|
||
broken = "Подобрен [бот](simplex:/contact#/?v=1-4% 26dh%3DMCow) и https://simplex.chat/docs."
|
||
self.assertIsNone(l10n.placeholder_problem(self.SOURCE, broken))
|
||
problems = l10n.translation_problems(Path("bg.xliff"), "key", self.SOURCE, broken)
|
||
self.assertEqual([p.kind for p in problems], [l10n.LINK])
|
||
|
||
|
||
class StringsParserTest(unittest.TestCase):
|
||
def test_parses_escapes_and_comments(self) -> None:
|
||
text = '/* note */\n"a\\"b" = "line\\nnext \\U00e9";\n// comment\n"_italic_" = "\\\\_kursiv_";\n'
|
||
self.assertEqual(l10n.parse_strings(text), [('a"b', "line\nnext é"), ("_italic_", "\\_kursiv_")])
|
||
|
||
def test_empty_file(self) -> None:
|
||
self.assertEqual(l10n.parse_strings("/* only a comment */\n"), [])
|
||
|
||
def test_syntax_errors(self) -> None:
|
||
for text in ['"key" = "value"', '"key" "value";', '"unterminated = "x";\n', "/* open"]:
|
||
with self.subTest(text=text), self.assertRaises(l10n.StringsSyntaxError):
|
||
l10n.parse_strings(text)
|
||
|
||
|
||
class XliffTransformTest(unittest.TestCase):
|
||
def test_split_and_join_keep_the_text_byte_for_byte(self) -> None:
|
||
text = xliff_xml("de", [unit_xml("A", "A", "Ä", "translated"), unit_xml("B", "B")])
|
||
segments = l10n.split_xliff(text)
|
||
self.assertEqual(l10n.join_xliff(segments), text)
|
||
blocks = list(l10n.unit_blocks(segments))
|
||
self.assertEqual([b.key for b in blocks], [(LOCALIZABLE, "A"), (LOCALIZABLE, "B")])
|
||
self.assertEqual(blocks[0].unit, l10n.Unit("A", "Ä"))
|
||
self.assertIsNone(blocks[1].unit.target)
|
||
|
||
def test_split_rejects_units_sharing_lines(self) -> None:
|
||
text = xliff_xml("de", [' <trans-unit id="A"><source>A</source></trans-unit><trans-unit id="B"><source>B</source></trans-unit>\n'])
|
||
with self.assertRaises(ValueError):
|
||
l10n.split_xliff(text)
|
||
|
||
def test_strip_empty_targets(self) -> None:
|
||
segments = l10n.split_xliff(xliff_xml("de", [unit_xml("A", "A", "", "needs-translation"), unit_xml("B", "B", "Bee")]))
|
||
self.assertEqual(l10n.strip_empty_targets(segments), 1)
|
||
self.assertEqual(l10n.join_xliff(segments), xliff_xml("de", [unit_xml("A", "A"), unit_xml("B", "B", "Bee")]))
|
||
self.assertEqual(l10n.strip_empty_targets(segments), 0)
|
||
|
||
def test_drop_untranslated_units(self) -> None:
|
||
segments = l10n.split_xliff(xliff_xml("de", [unit_xml("A", "A"), unit_xml("B", "B", "Bee"), unit_xml("C", "C", "")]))
|
||
self.assertEqual(l10n.join_xliff(l10n.drop_untranslated_units(segments)), xliff_xml("de", [unit_xml("B", "B", "Bee")]))
|
||
|
||
def test_multiline_target_counts_as_translated(self) -> None:
|
||
unit = ' <trans-unit id="M" xml:space="preserve">\n <source>M</source>\n <target>\nzwei Zeilen</target>\n </trans-unit>\n'
|
||
text = xliff_xml("de", [unit])
|
||
segments = l10n.split_xliff(text)
|
||
self.assertEqual(l10n.join_xliff(l10n.drop_untranslated_units(segments)), text)
|
||
self.assertEqual(l10n.saved_translations(segments)[(LOCALIZABLE, "M")].lines, [" <target>\n", "zwei Zeilen</target>\n"])
|
||
|
||
def test_restore_translations_dropped_by_xcode(self) -> None:
|
||
weblate = xliff_xml("de", [unit_xml("A", "A", "Entwurf", "needs-translation"), unit_xml("B", "B", "Bee"), unit_xml("C", "Old C", "Zeh")])
|
||
exported = l10n.split_xliff(xliff_xml("de", [unit_xml("A", "A"), unit_xml("B", "B", "Bee"), unit_xml("C", "New C")]))
|
||
self.assertEqual(l10n.restore_translations(exported, l10n.saved_translations(l10n.split_xliff(weblate))), 1)
|
||
expected = xliff_xml("de", [unit_xml("A", "A", "Entwurf", "needs-translation"), unit_xml("B", "B", "Bee"), unit_xml("C", "New C")])
|
||
self.assertEqual(l10n.join_xliff(exported), expected)
|
||
|
||
def test_restore_ignores_duplicate_ids_already_translated(self) -> None:
|
||
text = xliff_xml("ar", [unit_xml("Invite", "Invite"), unit_xml("Invite", "Invite", "دعوة")])
|
||
segments = l10n.split_xliff(text)
|
||
self.assertEqual(l10n.restore_translations(segments, l10n.saved_translations(segments)), 0)
|
||
self.assertEqual(l10n.join_xliff(segments), text)
|
||
|
||
def test_translation_changes(self) -> None:
|
||
path = Path("de.xliff")
|
||
before = xliff_xml("de", [unit_xml("A", "A", "Ah"), unit_xml("B", "B", "Bee"), unit_xml("Gone", "Gone", "Weg"), unit_xml("U", "U")])
|
||
after = l10n.split_xliff(xliff_xml("de", [unit_xml("B", "B", "Beh")]))
|
||
source_keys = {(LOCALIZABLE, "A"), (LOCALIZABLE, "B"), (LOCALIZABLE, "U")}
|
||
problems, removed = l10n.translation_changes(path, l10n.saved_translations(l10n.split_xliff(before)), after, source_keys, {})
|
||
self.assertEqual(removed, 1)
|
||
self.assertEqual([(p.kind, p.key, p.text) for p in problems], [(l10n.LOST, "A", "Ah"), (l10n.CHANGED, "B", "Beh")])
|
||
|
||
|
||
class FilesTest(RepositoryTestCase):
|
||
def test_app_languages_put_the_source_first(self) -> None:
|
||
self.assertEqual(l10n.app_languages(), ["en", "de", "uk"])
|
||
|
||
def test_language_of(self) -> None:
|
||
self.assertEqual(l10n.language_of(Path("apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings")), "zh-Hans")
|
||
self.assertEqual(l10n.language_of(l10n.xliff_path("pt-BR")), "pt-BR")
|
||
|
||
def test_check_files_reports_each_kind(self) -> None:
|
||
self.write(l10n.xliff_path("en"), xliff_xml("en", [unit_xml("%@ server", "%@ server", "%@ server")]))
|
||
self.write(l10n.xliff_path("de"), xliff_xml("de", [unit_xml("%@ server", "%@ server", "Server"), unit_xml("Ok", "Ok", "")]))
|
||
self.write(l10n.IOS_DIR / "de.lproj/Localizable.strings", '"Blank" = "";\n"%@ server" = "%@ Server";\n')
|
||
self.write(l10n.IOS_DIR / "uk.lproj/Localizable.strings", '"Broken" = "x"\n')
|
||
self.write(l10n.xliff_path("uk"), "<xliff><file>\n")
|
||
self.write(l10n.IOS_DIR / "en.lproj/Localizable.strings", '"Ignored" = "";\n')
|
||
self.write(l10n.xcloc_path("de") / "Source Contents/en.lproj/Localizable.strings", '"Ignored" = "";\n')
|
||
stats: dict[str, l10n.LanguageStats] = {}
|
||
with mock.patch.object(l10n.shutil, "which", return_value=None):
|
||
problems = l10n.check_files(stats, {})
|
||
self.assertEqual(
|
||
[(p.kind, p.language, p.key) for p in problems],
|
||
[
|
||
(l10n.STALE, "de", "Ok"),
|
||
(l10n.PLACEHOLDER, "de", "%@ server"),
|
||
(l10n.BLANK, "de", "Ok"),
|
||
(l10n.SYNTAX, "uk", ""),
|
||
(l10n.BLANK, "de", "Blank"),
|
||
(l10n.SYNTAX, "uk", ""),
|
||
],
|
||
)
|
||
self.assertEqual(stats["de"].translated, 1)
|
||
|
||
def test_strings_problems_already_in_the_xliff_are_listed_once(self) -> None:
|
||
self.write(l10n.xliff_path("de"), xliff_xml("de", [unit_xml("%@ server", "%@ server", "Server")]))
|
||
self.write(l10n.IOS_DIR / "de.lproj/Localizable.strings", '"%@ server" = "Server";\n"%@ file" = "Datei";\n')
|
||
with mock.patch.object(l10n.shutil, "which", return_value=None):
|
||
problems = l10n.check_files({}, {})
|
||
self.assertEqual([(p.path.suffix, p.key) for p in problems], [(".xliff", "%@ server"), (".strings", "%@ file")])
|
||
|
||
def test_check_xliff_reports_duplicates_and_stale_units(self) -> None:
|
||
path = l10n.xliff_path("de")
|
||
self.write(path, xliff_xml("de", [unit_xml("A", "A"), unit_xml("A", "A", "Ah"), unit_xml("Old", "Old", "Alt")]))
|
||
problems, translated = l10n.check_xliff(path, {(LOCALIZABLE, "A")}, {})
|
||
self.assertEqual(translated, 2)
|
||
self.assertEqual(
|
||
[(p.kind, p.key, p.detail) for p in problems],
|
||
[
|
||
(l10n.DUPLICATE, "A", f"2 units with this id in {LOCALIZABLE}"),
|
||
(l10n.STALE, "Old", "not in en.xliff, Xcode's export removes it"),
|
||
],
|
||
)
|
||
self.assertEqual(l10n.check_xliff(path, set(), {})[0][0].kind, l10n.DUPLICATE)
|
||
self.assertEqual(len(l10n.check_xliff(path, set(), {})[0]), 1)
|
||
|
||
def test_lint_with_plutil_reports_failures(self) -> None:
|
||
output = "a.strings: OK\nb.strings: Unexpected character \" at line 2\n"
|
||
completed = mock.Mock(stdout=output, stderr="")
|
||
with mock.patch.object(l10n.shutil, "which", return_value="/usr/bin/plutil"), mock.patch.object(l10n.subprocess, "run", return_value=completed) as run:
|
||
problems = l10n.lint_with_plutil([Path("a.strings"), Path("b.strings")])
|
||
run.assert_called_once()
|
||
self.assertEqual([(p.kind, str(p.path), p.detail) for p in problems], [(l10n.SYNTAX, "b.strings", 'plutil: Unexpected character " at line 2')])
|
||
with mock.patch.object(l10n.shutil, "which", return_value=None):
|
||
self.assertEqual(l10n.lint_with_plutil([Path("a.strings")]), [])
|
||
|
||
|
||
class ReportTest(unittest.TestCase):
|
||
def test_report_exit_codes_and_blank_grouping(self) -> None:
|
||
path = Path("apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff")
|
||
problems = [l10n.Problem(l10n.BLANK, path, f"Key {i}", "empty target") for i in range(5)]
|
||
problems.append(l10n.Problem(l10n.PLACEHOLDER, path, "%@ server", "expected %1 object, found none", "Server"))
|
||
output = io.StringIO()
|
||
with contextlib.redirect_stdout(output), mock.patch.object(l10n, "app_languages", return_value=["en", "de"]):
|
||
self.assertEqual(l10n.report({"de": l10n.LanguageStats(translated=7)}, problems, {"en", "de"}), 1)
|
||
text = output.getvalue()
|
||
self.assertIn("5 x empty target", text)
|
||
self.assertIn("'Key 0', 'Key 1', 'Key 2' and 2 more", text)
|
||
self.assertIn("'%@ server'", text)
|
||
self.assertIn("6 problem(s): 1 placeholder, 5 blank", text)
|
||
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(l10n, "app_languages", return_value=["en"]):
|
||
self.assertEqual(l10n.report({}, [], {"en"}), 0)
|
||
|
||
def test_warnings_do_not_fail(self) -> None:
|
||
path = Path("apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff")
|
||
problems = [l10n.Problem(l10n.STALE, path, f"Old {i}", "not in en.xliff") for i in range(4)]
|
||
problems.append(l10n.Problem(l10n.DUPLICATE, path, "A", "2 units with this id"))
|
||
output = io.StringIO()
|
||
with contextlib.redirect_stdout(output), mock.patch.object(l10n, "app_languages", return_value=["en"]):
|
||
self.assertEqual(l10n.report({}, problems, {"en"}), 0)
|
||
text = output.getvalue()
|
||
self.assertIn("4 x not in en.xliff", text)
|
||
self.assertIn("'Old 0', 'Old 1', 'Old 2' and 1 more", text)
|
||
self.assertIn("5 warning(s): 1 duplicate, 4 stale", text)
|
||
self.assertIn("No problems found.", text)
|
||
|
||
def test_preview_shortens_long_keys(self) -> None:
|
||
self.assertEqual(l10n.preview("short"), "short")
|
||
self.assertEqual(l10n.preview("x" * 150), "x" * l10n.KEY_PREVIEW_LENGTH + "…")
|
||
|
||
def test_format_duration(self) -> None:
|
||
self.assertEqual(l10n.format_duration(4.4), "4s")
|
||
self.assertEqual(l10n.format_duration(125), "2m 05s")
|
||
|
||
def test_paint_respects_color_setting(self) -> None:
|
||
with mock.patch.object(l10n, "USE_COLOR", False):
|
||
self.assertEqual(l10n.paint("x", l10n.RED), "x")
|
||
with mock.patch.object(l10n, "USE_COLOR", True):
|
||
self.assertEqual(l10n.paint("x", l10n.BOLD, l10n.RED), "\033[1;31mx\033[0m")
|
||
|
||
|
||
class UpdateTest(RepositoryTestCase):
|
||
def setUp(self) -> None:
|
||
super().setUp()
|
||
self.pbxproj = b"// original project\n"
|
||
self.write(l10n.PBXPROJ, self.pbxproj.decode())
|
||
self.weblate_de = xliff_xml("de", [unit_xml("A", "A", "Entwurf", "needs-translation"), unit_xml("B", "B", ""), unit_xml("%@ x", "%@ x", "%@ y")])
|
||
self.write(l10n.xliff_path("de"), self.weblate_de)
|
||
self.write(l10n.xliff_path("uk"), xliff_xml("uk", [unit_xml("A", "A", "А")]))
|
||
self.write(l10n.xliff_path("en"), xliff_xml("en", [unit_xml("A", "A", "A"), unit_xml("B", "B", "B"), unit_xml("%@ x", "%@ x", "%@ x")]))
|
||
self.imported: dict[str, str] = {}
|
||
|
||
def fake_xcodebuild(self, arguments: list[str], log: Path) -> None:
|
||
l10n.write_file(l10n.PBXPROJ, "// rewritten by Xcode\n")
|
||
if arguments[0] == "-importLocalizations":
|
||
language = Path(arguments[arguments.index("-localizationPath") + 1]).stem
|
||
self.imported[language] = l10n.read_file(l10n.xliff_path(language))
|
||
return
|
||
languages = [arguments[i + 1] for i, argument in enumerate(arguments) if argument == "-exportLanguage"]
|
||
self.assertEqual(languages, ["en", "de", "uk"])
|
||
self.write(l10n.xliff_path("de"), xliff_xml("de", [unit_xml("A", "A"), unit_xml("B", "B"), unit_xml("%@ x", "%@ x", "%@ y"), unit_xml("New", "New")]))
|
||
|
||
def run_update(self) -> tuple[int, str]:
|
||
output = io.StringIO()
|
||
with contextlib.ExitStack() as stack:
|
||
stack.enter_context(mock.patch.object(l10n, "require_update_environment"))
|
||
stack.enter_context(mock.patch.object(l10n, "update_pbxproj_libraries"))
|
||
stack.enter_context(mock.patch.object(l10n, "xcodebuild", side_effect=self.fake_xcodebuild))
|
||
stack.enter_context(mock.patch.object(l10n.shutil, "which", return_value=None))
|
||
stack.enter_context(contextlib.redirect_stdout(output))
|
||
code = l10n.update()
|
||
return code, output.getvalue()
|
||
|
||
def test_update_round_trip(self) -> None:
|
||
code, output = self.run_update()
|
||
self.assertEqual(code, 0, output)
|
||
self.assertEqual(self.imported["de"], xliff_xml("de", [unit_xml("A", "A", "Entwurf", "needs-translation"), unit_xml("B", "B"), unit_xml("%@ x", "%@ x", "%@ y")]))
|
||
self.assertEqual(
|
||
l10n.read_file(l10n.xliff_path("de")),
|
||
xliff_xml("de", [unit_xml("A", "A", "Entwurf", "needs-translation"), unit_xml("%@ x", "%@ x", "%@ y")]),
|
||
)
|
||
self.assertEqual(l10n.PBXPROJ.read_bytes(), self.pbxproj)
|
||
self.assertIn("removed 1 empty targets", output)
|
||
self.assertIn("No problems found.", output)
|
||
|
||
def test_update_restores_the_project_when_xcode_fails(self) -> None:
|
||
def failing_xcodebuild(arguments: list[str], log: Path) -> None:
|
||
l10n.write_file(l10n.PBXPROJ, "// rewritten by Xcode\n")
|
||
raise l10n.UpdateError("xcodebuild -importLocalizations failed")
|
||
|
||
with mock.patch.object(l10n, "require_update_environment"), mock.patch.object(l10n, "update_pbxproj_libraries"), mock.patch.object(
|
||
l10n, "xcodebuild", side_effect=failing_xcodebuild
|
||
), contextlib.redirect_stdout(io.StringIO()):
|
||
with self.assertRaisesRegex(l10n.UpdateError, "git checkout -- apps/ios"):
|
||
l10n.update()
|
||
self.assertEqual(l10n.PBXPROJ.read_bytes(), self.pbxproj)
|
||
|
||
def test_update_reports_a_malformed_xliff(self) -> None:
|
||
self.write(l10n.xliff_path("uk"), "<xliff><file>\n")
|
||
with mock.patch.object(l10n, "require_update_environment"), contextlib.redirect_stdout(io.StringIO()):
|
||
with self.assertRaisesRegex(l10n.UpdateError, "uk.xliff: "):
|
||
l10n.update()
|
||
|
||
def test_require_update_environment_outside_macos(self) -> None:
|
||
with mock.patch.object(l10n.platform, "system", return_value="Linux"):
|
||
with self.assertRaisesRegex(l10n.UpdateError, "macOS with Xcode"):
|
||
l10n.require_update_environment()
|
||
|
||
|
||
class SwiftUsageTest(RepositoryTestCase):
|
||
def test_swift_string_literals(self) -> None:
|
||
swift = 'let a = "\\n"\nText("Hello, \\"you\\"")\nlet b = "\\(count) items"\nalert("Error\\u{2026}")\n'
|
||
self.write(l10n.IOS_DIR / "Shared/View.swift", swift)
|
||
self.assertEqual(
|
||
l10n.swift_string_literals(),
|
||
{'Hello, "you"': "apps/ios/Shared/View.swift:2", "Error\u2026": "apps/ios/Shared/View.swift:4"},
|
||
)
|
||
|
||
def test_unescape_swift(self) -> None:
|
||
self.assertEqual(l10n.unescape_swift('a\\"b\\\\ \\u{1F600}\\t'), 'a"b\\ \U0001F600\t')
|
||
|
||
def test_stale_units_still_used_in_code(self) -> None:
|
||
path = l10n.xliff_path("de")
|
||
self.write(path, xliff_xml("de", [unit_xml("Gone", "Gone", "Weg"), unit_xml("Used", "Used", "Benutzt")]))
|
||
problems, _ = l10n.check_xliff(path, {(LOCALIZABLE, "Other")}, {"Used": "apps/ios/A.swift:3"})
|
||
self.assertEqual(
|
||
[(p.kind, p.key, p.detail) for p in problems],
|
||
[
|
||
(l10n.STALE, "Gone", "not in en.xliff, Xcode's export removes it"),
|
||
(l10n.IN_CODE, "Used", "not in en.xliff, but the Swift code has this string at apps/ios/A.swift:3"),
|
||
],
|
||
)
|
||
|
||
def test_removed_translation_still_used_in_code(self) -> None:
|
||
saved = l10n.saved_translations(l10n.split_xliff(xliff_xml("de", [unit_xml("Used", "Used", "Benutzt"), unit_xml("Gone", "Gone", "Weg")])))
|
||
problems, removed = l10n.translation_changes(Path("de.xliff"), saved, [], set(), {"Used": "apps/ios/A.swift:3"})
|
||
self.assertEqual(removed, 2)
|
||
self.assertEqual([(p.kind, p.key, p.text) for p in problems], [(l10n.IN_CODE, "Used", "Benutzt")])
|
||
|
||
|
||
BUILD_SCRIPT = """ val possibleFormat = listOf("s", "d", "1\\$s", "2\\$s", "1\\$d", "1\\$02d", "f")\n"""
|
||
|
||
|
||
def kotlin_xml(strings: list[str]) -> str:
|
||
return "<resources>\n" + "".join(f" {string}\n" for string in strings) + "</resources>\n"
|
||
|
||
|
||
class KotlinRulesTest(unittest.TestCase):
|
||
POSSIBLE = ["s", "d", "1$s", "2$s", "3$s", "4$s", "1$d", "2$d", "3$d", "4$d", "1$02d", "2$02d", "3$02d", "2s", "f"]
|
||
|
||
def formats(self, value: str) -> list[str]:
|
||
return l10n.kotlin_formats(f'<string name="x">{value}</string>', self.POSSIBLE)
|
||
|
||
def test_kotlin_formats(self) -> None:
|
||
self.assertEqual(self.formats("No placeholders"), [])
|
||
self.assertEqual(self.formats("%s sent %d"), ["s", "d"])
|
||
self.assertEqual(self.formats("%2$s by %1$s"), ["1$s", "2$s"])
|
||
self.assertEqual(self.formats("%1$02d:%2$02d"), ["1$02d", "2$02d"])
|
||
|
||
def test_kotlin_formats_rejected_by_adjust_formatting(self) -> None:
|
||
cases = {
|
||
"% s": "unknown placeholder '% s', adjustFormatting fails the build",
|
||
"50%% off": "unknown placeholder '%% o', adjustFormatting fails the build",
|
||
"%1$s and %s": "positional and non-positional placeholders mixed, adjustFormatting fails the build",
|
||
"%5$s": "unknown placeholder '%5$s', adjustFormatting fails the build",
|
||
}
|
||
for value, expected in cases.items():
|
||
with self.subTest(value=value), self.assertRaisesRegex(l10n.KotlinFormatError, re.escape(expected)):
|
||
self.formats(value)
|
||
|
||
def test_kotlin_add_cdata(self) -> None:
|
||
cases = {
|
||
'<string name="x">Plain text</string>': '<string name="x">Plain text</string>',
|
||
' <string name="x">Be <b>bold</b></string>': ' <string name="x"><![CDATA[Be <b>bold</b>]]></string>',
|
||
'<string name="x"><font color=#0088ff>Blue</font></string>': '<string name="x"><![CDATA[<font color="#0088ff">Blue</font>]]></string>',
|
||
'<string name="x"><font color="#0088ff">Blue</font></string>': '<string name="x"><![CDATA[<font color="#0088ff">Blue</font>]]></string>',
|
||
}
|
||
for line, expected in cases.items():
|
||
with self.subTest(line=line):
|
||
self.assertEqual(l10n.kotlin_add_cdata(line), expected)
|
||
|
||
def test_kotlin_add_cdata_rejects_what_adjust_formatting_rejects(self) -> None:
|
||
invalid = [
|
||
'<string name="x">Open <a href="https://x.example">link</a></string>',
|
||
'<string name="x">Broken <b>bold</string>',
|
||
'<string name="x">1 < 2</string>',
|
||
]
|
||
for line in invalid:
|
||
with self.subTest(line=line), self.assertRaisesRegex(l10n.KotlinFormatError, "adjustFormatting fails the build"):
|
||
l10n.kotlin_add_cdata(line)
|
||
|
||
def test_kotlin_add_cdata_fast_path_skips_only_lines_any_html_rejects(self) -> None:
|
||
repository = Path(__file__).resolve().parents[2]
|
||
lines = [line for path in (repository / l10n.KOTLIN_RESOURCES).glob("*/strings.xml") for line in path.read_text(encoding="utf-8").splitlines()]
|
||
lines += ['<string name="x">a</string>', '<string name="x">a > b</string>', '<string name="x">1 > 0</string>']
|
||
self.assertGreater(len(lines), 1000)
|
||
for line in lines:
|
||
skipped = line.count("<") + line.count(">") <= l10n.KOTLIN_STRING_TAG_BRACKETS and "<" not in line and ">" not in line
|
||
if skipped:
|
||
self.assertIsNone(l10n.ANY_HTML.fullmatch(line), line)
|
||
|
||
def test_kotlin_text(self) -> None:
|
||
self.assertEqual(l10n.kotlin_text("<![CDATA[<b>Don\\'t</b>]]> & \\\"x\\\" \\u00e9"), '<b>Don\'t</b> & "x" é')
|
||
|
||
def test_substring_after(self) -> None:
|
||
self.assertEqual(l10n.substring_after("a%b%c", "%"), "b%c")
|
||
self.assertEqual(l10n.substring_after("abc", "%"), "abc")
|
||
|
||
def test_remove_cdata(self) -> None:
|
||
self.assertEqual(l10n.remove_cdata('<string name="x"><![CDATA[<b>a</b>]]></string>'), '<string name="x"><b>a</b></string>')
|
||
self.assertEqual(l10n.remove_cdata("<string>a</string>"), "<string>a</string>")
|
||
|
||
|
||
class KotlinFilesTest(RepositoryTestCase):
|
||
def setUp(self) -> None:
|
||
super().setUp()
|
||
self.write(l10n.KOTLIN_BUILD_SCRIPT, BUILD_SCRIPT)
|
||
self.write(
|
||
l10n.KOTLIN_RESOURCES / "base/strings.xml",
|
||
kotlin_xml(
|
||
[
|
||
'<string name="sent">%1$s sent %2$s</string>',
|
||
'<string name="hello">Hello</string>',
|
||
'<string name="docs">Read [docs](https://simplex.chat/docs)</string>',
|
||
'<string name="bold"><![CDATA[Be <b>bold</b>]]></string>',
|
||
]
|
||
),
|
||
)
|
||
|
||
def test_possible_formats_come_from_the_build_script(self) -> None:
|
||
self.assertEqual(l10n.kotlin_possible_formats(), ["s", "d", "1$s", "2$s", "1$d", "1$02d", "f"])
|
||
self.write(l10n.KOTLIN_BUILD_SCRIPT, "// nothing\n")
|
||
with self.assertRaisesRegex(l10n.UpdateError, "possibleFormat not found"):
|
||
l10n.kotlin_possible_formats()
|
||
|
||
def test_check_kotlin_files_reports_each_kind(self) -> None:
|
||
self.write(
|
||
l10n.KOTLIN_RESOURCES / "de/strings.xml",
|
||
kotlin_xml(
|
||
[
|
||
'<string name="sent">%2$s von %1$s gesendet</string>',
|
||
'<string name="hello"></string>',
|
||
'<string name="hello">Hallo</string>',
|
||
'<string name="docs">Lies die [Doku] (https://simplex.chat/docs)</string>',
|
||
"<string name=\"bold\"><![CDATA[Sei <b>mutig</b>]]> l'ami</string>",
|
||
'<string name="removed">Weg</string>',
|
||
]
|
||
),
|
||
)
|
||
self.write(l10n.KOTLIN_RESOURCES / "fr/strings.xml", kotlin_xml(['<string name="sent">%1$s a envoyé % s</string>']))
|
||
self.write(l10n.KOTLIN_RESOURCES / "uk/strings.xml", "<resources><string name='a'>\n")
|
||
stats: dict[str, l10n.LanguageStats] = {}
|
||
problems, rewritten = l10n.check_kotlin_files(stats)
|
||
self.assertEqual(rewritten, [])
|
||
self.assertEqual(
|
||
[(p.language, p.kind, p.key) for p in problems],
|
||
[
|
||
("de", l10n.BLANK, "hello"),
|
||
("de", l10n.DUPLICATE, "hello"),
|
||
("de", l10n.LINK, "docs"),
|
||
("de", l10n.HTML, "bold"),
|
||
("de", l10n.SYNTAX, "bold"),
|
||
("de", l10n.UNKNOWN, "removed"),
|
||
("fr", l10n.PLACEHOLDER, "sent"),
|
||
("uk", l10n.SYNTAX, ""),
|
||
],
|
||
)
|
||
self.assertEqual(problems[6].detail, "unknown placeholder '% s', adjustFormatting fails the build")
|
||
self.assertEqual((stats["de"].translated, stats["fr"].translated, stats["uk"].translated), (6, 1, 0))
|
||
|
||
def test_placeholder_mismatch_against_the_base(self) -> None:
|
||
self.write(l10n.KOTLIN_RESOURCES / "de/strings.xml", kotlin_xml(['<string name="sent">%1$s gesendet</string>']))
|
||
problems, _ = l10n.check_kotlin_files({})
|
||
self.assertEqual([(p.kind, p.detail) for p in problems], [(l10n.PLACEHOLDER, "expected %1$s, %2$s, found %1$s")])
|
||
|
||
def test_missing_base_file(self) -> None:
|
||
(l10n.KOTLIN_RESOURCES / "base/strings.xml").unlink()
|
||
with self.assertRaisesRegex(l10n.UpdateError, "base/strings.xml not found"):
|
||
l10n.check_kotlin_files({})
|
||
|
||
def test_language_of_strings_xml(self) -> None:
|
||
self.assertEqual(l10n.language_of(l10n.KOTLIN_RESOURCES / "pt-rBR/strings.xml"), "pt-rBR")
|
||
|
||
|
||
class KotlinAdjustTest(RepositoryTestCase):
|
||
def test_kotlin_lines_split_like_read_line(self) -> None:
|
||
self.assertEqual(l10n.kotlin_lines("a\nb\n"), ["a", "b"])
|
||
self.assertEqual(l10n.kotlin_lines("a\r\nb"), ["a", "b"])
|
||
self.assertEqual(l10n.kotlin_lines("a\n\n"), ["a", ""])
|
||
self.assertEqual(l10n.kotlin_lines("a\u2028b"), ["a\u2028b"])
|
||
self.assertEqual(l10n.kotlin_lines(""), [])
|
||
|
||
def test_gradle_string_id(self) -> None:
|
||
self.assertEqual(l10n.gradle_string_id(' <string name="send_file">Send</string>'), "send_file")
|
||
|
||
def test_adjust_kotlin_normalizes_cdata(self) -> None:
|
||
text = kotlin_xml(
|
||
[
|
||
'<string name="plain"><![CDATA[No HTML here]]></string>',
|
||
'<string name="bold">Be <b>bold</b></string>',
|
||
'<string name="kept">Kept</string>',
|
||
]
|
||
)
|
||
adjusted = l10n.adjust_kotlin(text)
|
||
self.assertTrue(adjusted.changed)
|
||
self.assertEqual(
|
||
adjusted.lines,
|
||
[
|
||
"<resources>",
|
||
' <string name="plain">No HTML here</string>',
|
||
' <string name="bold"><![CDATA[Be <b>bold</b>]]></string>',
|
||
' <string name="kept">Kept</string>',
|
||
"</resources>",
|
||
],
|
||
)
|
||
self.assertEqual([string.name for string in adjusted.strings], ["plain", "bold", "kept"])
|
||
self.assertFalse(l10n.adjust_kotlin("\n".join(adjusted.lines)).changed)
|
||
|
||
def test_adjust_kotlin_multiline_strings(self) -> None:
|
||
text = '<resources>\n <string name="two">First line\nsecond %s line</string>\n</resources>\n'
|
||
adjusted = l10n.adjust_kotlin(text)
|
||
self.assertFalse(adjusted.changed)
|
||
self.assertEqual(adjusted.strings[0].name, "two")
|
||
self.assertEqual(adjusted.strings[0].line, ' <string name="two">First linesecond %s line</string>')
|
||
self.assertEqual(adjusted.strings[0].raw_value, "First line\nsecond %s line")
|
||
|
||
def test_adjust_kotlin_unclosed_string(self) -> None:
|
||
adjusted = l10n.adjust_kotlin('<resources>\n <string name="open">Never closed\n</resources>\n')
|
||
self.assertEqual(adjusted.unclosed, ' <string name="open">Never closed\n</resources>')
|
||
|
||
def test_update_writes_like_adjust_formatting(self) -> None:
|
||
self.write(l10n.KOTLIN_BUILD_SCRIPT, BUILD_SCRIPT)
|
||
self.write(l10n.KOTLIN_RESOURCES / "base/strings.xml", kotlin_xml(['<string name="a">%s</string>', '<string name="b">B</string>']))
|
||
unchanged = kotlin_xml(['<string name="a">%s</string>'])
|
||
normalized = kotlin_xml(['<string name="a"><![CDATA[%s]]></string>'])
|
||
self.write(l10n.KOTLIN_RESOURCES / "de/strings.xml", normalized)
|
||
self.write(l10n.KOTLIN_RESOURCES / "fr/strings.xml", unchanged)
|
||
self.write(l10n.KOTLIN_RESOURCES / "lv/strings.xml", normalized)
|
||
self.write(l10n.KOTLIN_RESOURCES / "uk/strings.xml", kotlin_xml(['<string name="a"><![CDATA[kein]]></string>']))
|
||
problems, rewritten = l10n.check_kotlin_files({}, write=True)
|
||
self.assertEqual([path.parent.name for path in rewritten], ["de"])
|
||
self.assertEqual(l10n.read_file(l10n.KOTLIN_RESOURCES / "de/strings.xml"), unchanged.rstrip("\n"))
|
||
self.assertEqual(l10n.read_file(l10n.KOTLIN_RESOURCES / "fr/strings.xml"), unchanged)
|
||
self.assertEqual(l10n.read_file(l10n.KOTLIN_RESOURCES / "lv/strings.xml"), normalized)
|
||
self.assertEqual(l10n.read_file(l10n.KOTLIN_RESOURCES / "uk/strings.xml"), kotlin_xml(['<string name="a"><![CDATA[kein]]></string>']))
|
||
self.assertEqual([(p.language, p.kind) for p in problems], [("uk", l10n.PLACEHOLDER)])
|
||
|
||
def test_update_kotlin_command(self) -> None:
|
||
self.write(l10n.KOTLIN_BUILD_SCRIPT, BUILD_SCRIPT)
|
||
self.write(l10n.KOTLIN_RESOURCES / "base/strings.xml", kotlin_xml(['<string name="a">A</string>']))
|
||
self.write(l10n.KOTLIN_RESOURCES / "de/strings.xml", kotlin_xml(['<string name="a"><![CDATA[Ä]]></string>']))
|
||
output = io.StringIO()
|
||
with contextlib.redirect_stdout(output):
|
||
self.assertEqual(l10n.main(["update-kotlin"]), 0)
|
||
self.assertIn("rewrote 1 files as adjustFormatting does: de", output.getvalue())
|
||
|
||
|
||
class SummaryTest(unittest.TestCase):
|
||
def test_empty_columns_are_hidden(self) -> None:
|
||
path = Path("apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff")
|
||
output = io.StringIO()
|
||
with contextlib.redirect_stdout(output):
|
||
l10n.print_summary({"de": l10n.LanguageStats(translated=3)}, [l10n.Problem(l10n.LINK, path, "k", "d")], {"de"})
|
||
header = output.getvalue().strip().splitlines()[0].split()
|
||
self.assertEqual(header, ["language", "in", "app", "translated", "link"])
|
||
|
||
|
||
class MainTest(RepositoryTestCase):
|
||
def test_check_command_exit_code(self) -> None:
|
||
self.write(l10n.xliff_path("de"), xliff_xml("de", [unit_xml("%@ x", "%@ x", "y")]))
|
||
with contextlib.redirect_stdout(io.StringIO()), mock.patch.object(l10n.shutil, "which", return_value=None):
|
||
self.assertEqual(l10n.main(["check"]), 1)
|
||
|
||
def test_check_kotlin_command_exit_code(self) -> None:
|
||
self.write(l10n.KOTLIN_BUILD_SCRIPT, BUILD_SCRIPT)
|
||
self.write(l10n.KOTLIN_RESOURCES / "base/strings.xml", kotlin_xml(['<string name="a">%s</string>']))
|
||
self.write(l10n.KOTLIN_RESOURCES / "de/strings.xml", kotlin_xml(['<string name="a">%s</string>']))
|
||
with contextlib.redirect_stdout(io.StringIO()):
|
||
self.assertEqual(l10n.main(["check-kotlin"]), 0)
|
||
self.write(l10n.KOTLIN_RESOURCES / "de/strings.xml", kotlin_xml(['<string name="a">kein</string>']))
|
||
with contextlib.redirect_stdout(io.StringIO()):
|
||
self.assertEqual(l10n.main(["check-kotlin"]), 1)
|
||
|
||
def test_update_error_exit_code(self) -> None:
|
||
stderr = io.StringIO()
|
||
with mock.patch.object(l10n.platform, "system", return_value="Linux"), contextlib.redirect_stderr(stderr):
|
||
self.assertEqual(l10n.main(["update"]), 2)
|
||
self.assertIn("update needs macOS", stderr.getvalue())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|