2018-06-02 01:32:56 +02:00
|
|
|
import json
|
2016-06-23 11:32:45 +02:00
|
|
|
import os
|
|
|
|
import re
|
2017-11-16 00:43:27 +01:00
|
|
|
from subprocess import CalledProcessError, check_output
|
2018-05-10 19:30:04 +02:00
|
|
|
from typing import Any, Dict, List
|
2016-06-23 11:32:45 +02:00
|
|
|
|
2020-08-07 01:09:47 +02:00
|
|
|
import orjson
|
2020-01-14 21:59:46 +01:00
|
|
|
import polib
|
2016-06-23 11:32:45 +02:00
|
|
|
from django.conf import settings
|
2017-10-20 07:48:47 +02:00
|
|
|
from django.conf.locale import LANG_INFO
|
2017-12-25 05:24:30 +01:00
|
|
|
from django.core.management.base import CommandParser
|
2017-11-16 00:43:27 +01:00
|
|
|
from django.core.management.commands import compilemessages
|
2020-09-01 23:16:00 +02:00
|
|
|
from django.utils.translation import override as override_language
|
|
|
|
from django.utils.translation import ugettext as _
|
2017-10-20 07:48:47 +02:00
|
|
|
from django.utils.translation.trans_real import to_language
|
2020-10-21 23:38:45 +02:00
|
|
|
from pyuca import Collator
|
2016-06-23 11:32:45 +02:00
|
|
|
|
2020-01-14 21:59:46 +01:00
|
|
|
|
2016-06-23 11:32:45 +02:00
|
|
|
class Command(compilemessages.Command):
|
|
|
|
|
2017-12-25 05:24:30 +01:00
|
|
|
def add_arguments(self, parser: CommandParser) -> None:
|
|
|
|
super().add_arguments(parser)
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
'--strict', '-s',
|
|
|
|
action='store_true',
|
|
|
|
help='Stop execution in case of errors.')
|
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def handle(self, *args: Any, **options: Any) -> None:
|
2017-10-27 08:28:23 +02:00
|
|
|
super().handle(*args, **options)
|
2017-12-25 05:24:30 +01:00
|
|
|
self.strict = options['strict']
|
2016-06-23 11:32:45 +02:00
|
|
|
self.extract_language_options()
|
2017-09-13 07:04:22 +02:00
|
|
|
self.create_language_name_map()
|
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def create_language_name_map(self) -> None:
|
2017-09-13 07:04:22 +02:00
|
|
|
join = os.path.join
|
2019-07-02 22:38:09 +02:00
|
|
|
deploy_root = settings.DEPLOY_ROOT
|
|
|
|
path = join(deploy_root, 'locale', 'language_options.json')
|
|
|
|
output_path = join(deploy_root, 'locale', 'language_name_map.json')
|
2017-09-13 07:04:22 +02:00
|
|
|
|
2020-08-07 01:09:47 +02:00
|
|
|
with open(path, "rb") as reader:
|
|
|
|
languages = orjson.loads(reader.read())
|
2017-09-13 07:04:22 +02:00
|
|
|
lang_list = []
|
|
|
|
for lang_info in languages['languages']:
|
2017-10-20 08:12:30 +02:00
|
|
|
lang_info['name'] = lang_info['name_local']
|
|
|
|
del lang_info['name_local']
|
2017-09-13 07:04:22 +02:00
|
|
|
lang_list.append(lang_info)
|
|
|
|
|
2020-10-21 23:38:45 +02:00
|
|
|
collator = Collator()
|
|
|
|
lang_list.sort(key=lambda lang: collator.sort_key(lang['name']))
|
2017-09-13 07:04:22 +02:00
|
|
|
|
2020-08-07 01:09:47 +02:00
|
|
|
with open(output_path, 'wb') as output_file:
|
|
|
|
output_file.write(
|
|
|
|
orjson.dumps(
|
|
|
|
{'name_map': lang_list},
|
|
|
|
option=orjson.OPT_APPEND_NEWLINE | orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS,
|
|
|
|
)
|
|
|
|
)
|
2016-06-23 11:32:45 +02:00
|
|
|
|
2018-05-10 19:30:04 +02:00
|
|
|
def get_po_filename(self, locale_path: str, locale: str) -> str:
|
2016-07-26 14:34:18 +02:00
|
|
|
po_template = '{}/{}/LC_MESSAGES/django.po'
|
|
|
|
return po_template.format(locale_path, locale)
|
|
|
|
|
2018-05-10 19:30:04 +02:00
|
|
|
def get_json_filename(self, locale_path: str, locale: str) -> str:
|
2020-06-09 00:25:09 +02:00
|
|
|
return f"{locale_path}/{locale}/translations.json"
|
2016-07-26 14:34:18 +02:00
|
|
|
|
2018-05-10 19:30:04 +02:00
|
|
|
def get_name_from_po_file(self, po_filename: str, locale: str) -> str:
|
2020-10-09 03:52:49 +02:00
|
|
|
try:
|
|
|
|
team = polib.pofile(po_filename).metadata["Language-Team"]
|
|
|
|
return team[:team.rindex(" (")]
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
raise Exception(f"Unknown language {locale}")
|
2017-10-20 07:48:47 +02:00
|
|
|
|
2018-05-10 19:30:04 +02:00
|
|
|
def get_locales(self) -> List[str]:
|
2019-11-13 03:24:14 +01:00
|
|
|
output = check_output(['git', 'ls-files', 'locale'])
|
|
|
|
tracked_files = output.decode().split()
|
2019-07-02 22:38:09 +02:00
|
|
|
regex = re.compile(r'locale/(\w+)/LC_MESSAGES/django.po')
|
2017-10-23 07:31:04 +02:00
|
|
|
locales = ['en']
|
|
|
|
for tracked_file in tracked_files:
|
|
|
|
matched = regex.search(tracked_file)
|
|
|
|
if matched:
|
|
|
|
locales.append(matched.group(1))
|
|
|
|
|
|
|
|
return locales
|
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def extract_language_options(self) -> None:
|
2020-06-09 00:25:09 +02:00
|
|
|
locale_path = f"{settings.DEPLOY_ROOT}/locale"
|
|
|
|
output_path = f"{locale_path}/language_options.json"
|
2016-06-23 11:32:45 +02:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
data: Dict[str, List[Dict[str, Any]]] = {'languages': []}
|
2016-06-23 11:32:45 +02:00
|
|
|
|
2017-10-23 07:31:04 +02:00
|
|
|
try:
|
|
|
|
locales = self.get_locales()
|
|
|
|
except CalledProcessError:
|
|
|
|
# In case we are not under a Git repo, fallback to getting the
|
|
|
|
# locales using listdir().
|
|
|
|
locales = os.listdir(locale_path)
|
2017-11-02 09:22:26 +01:00
|
|
|
locales.append('en')
|
2017-10-23 07:31:04 +02:00
|
|
|
locales = list(set(locales))
|
2016-07-05 09:25:23 +02:00
|
|
|
|
|
|
|
for locale in locales:
|
2017-10-20 07:48:47 +02:00
|
|
|
if locale == 'en':
|
2016-07-26 14:34:18 +02:00
|
|
|
data['languages'].append({
|
2017-10-20 07:48:47 +02:00
|
|
|
'name': 'English',
|
|
|
|
'name_local': 'English',
|
|
|
|
'code': 'en',
|
2017-10-20 08:16:18 +02:00
|
|
|
'locale': 'en',
|
2016-07-26 14:34:18 +02:00
|
|
|
})
|
2016-06-23 11:32:45 +02:00
|
|
|
continue
|
2017-02-03 23:26:10 +01:00
|
|
|
|
2017-10-20 07:48:47 +02:00
|
|
|
lc_messages_path = os.path.join(locale_path, locale, 'LC_MESSAGES')
|
|
|
|
if not os.path.exists(lc_messages_path):
|
|
|
|
# Not a locale.
|
|
|
|
continue
|
2016-06-23 11:32:45 +02:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
info: Dict[str, Any] = {}
|
2017-10-20 07:48:47 +02:00
|
|
|
code = to_language(locale)
|
2016-07-26 14:34:18 +02:00
|
|
|
percentage = self.get_translation_percentage(locale_path, locale)
|
2017-10-20 07:48:47 +02:00
|
|
|
try:
|
|
|
|
name = LANG_INFO[code]['name']
|
|
|
|
name_local = LANG_INFO[code]['name_local']
|
|
|
|
except KeyError:
|
|
|
|
# Fallback to getting the name from PO file.
|
|
|
|
filename = self.get_po_filename(locale_path, locale)
|
|
|
|
name = self.get_name_from_po_file(filename, locale)
|
2020-09-01 23:16:00 +02:00
|
|
|
with override_language(code):
|
|
|
|
name_local = _(name)
|
2016-07-26 14:34:18 +02:00
|
|
|
|
2016-06-23 11:32:45 +02:00
|
|
|
info['name'] = name
|
2017-10-20 07:48:47 +02:00
|
|
|
info['name_local'] = name_local
|
2017-10-20 08:16:18 +02:00
|
|
|
info['code'] = code
|
|
|
|
info['locale'] = locale
|
2016-07-26 14:34:18 +02:00
|
|
|
info['percent_translated'] = percentage
|
2017-10-20 07:48:47 +02:00
|
|
|
data['languages'].append(info)
|
2016-06-23 11:32:45 +02:00
|
|
|
|
|
|
|
with open(output_path, 'w') as writer:
|
2018-06-02 01:32:56 +02:00
|
|
|
json.dump(data, writer, indent=2, sort_keys=True)
|
2018-01-26 06:24:49 +01:00
|
|
|
writer.write('\n')
|
2016-07-26 14:34:18 +02:00
|
|
|
|
2018-05-10 19:30:04 +02:00
|
|
|
def get_translation_percentage(self, locale_path: str, locale: str) -> int:
|
2016-07-30 05:03:57 +02:00
|
|
|
|
2016-07-26 14:34:18 +02:00
|
|
|
# backend stats
|
|
|
|
po = polib.pofile(self.get_po_filename(locale_path, locale))
|
|
|
|
not_translated = len(po.untranslated_entries())
|
|
|
|
total = len(po.translated_entries()) + not_translated
|
|
|
|
|
|
|
|
# frontend stats
|
2020-08-07 01:09:47 +02:00
|
|
|
with open(self.get_json_filename(locale_path, locale), "rb") as reader:
|
|
|
|
for key, value in orjson.loads(reader.read()).items():
|
2016-07-26 14:34:18 +02:00
|
|
|
total += 1
|
2017-10-04 10:09:24 +02:00
|
|
|
if value == '':
|
2016-07-26 14:34:18 +02:00
|
|
|
not_translated += 1
|
|
|
|
|
2017-12-25 05:24:30 +01:00
|
|
|
# mobile stats
|
2020-08-07 01:09:47 +02:00
|
|
|
with open(os.path.join(locale_path, 'mobile_info.json'), "rb") as mob:
|
|
|
|
mobile_info = orjson.loads(mob.read())
|
2017-12-25 05:24:30 +01:00
|
|
|
try:
|
|
|
|
info = mobile_info[locale]
|
|
|
|
except KeyError:
|
|
|
|
if self.strict:
|
|
|
|
raise
|
|
|
|
info = {'total': 0, 'not_translated': 0}
|
|
|
|
|
|
|
|
total += info['total']
|
|
|
|
not_translated += info['not_translated']
|
|
|
|
|
2016-08-02 14:25:32 +02:00
|
|
|
return (total - not_translated) * 100 // total
|