Extract ODM strings, moar translation work

pull/943/head
Piero Toffanin 2020-12-18 16:54:00 -05:00
rodzic 27379a6b2d
commit 2aa1c12970
13 zmienionych plików z 175 dodań i 52 usunięć

Wyświetl plik

@ -61,7 +61,7 @@ class ThemeModelForm(forms.ModelForm):
label=_("HTML (after header)"),
required=False,
widget=CodeMirrorEditor(options={'mode': 'xml', 'lineNumbers': True}))
html_after_body = forms.CharField(help_text=_("HTML that will be displayed after the </body> tag"),
html_after_body = forms.CharField(help_text=_("HTML that will be displayed after the body tag"),
label=_("HTML (after body)"),
required=False,
widget=CodeMirrorEditor(options={'mode': 'xml', 'lineNumbers': True}))

Wyświetl plik

@ -4,78 +4,79 @@
import re
from functools import lru_cache
from django.utils.translation import gettext_lazy as _
algos = {
'NDVI': {
'expr': '(N - R) / (N + R)',
'help': 'Normalized Difference Vegetation Index shows the amount of green vegetation.'
'help': _('Normalized Difference Vegetation Index shows the amount of green vegetation.')
},
'NDVI (Blue)': {
'expr': '(N - B) / (N + B)',
'help': 'Normalized Difference Vegetation Index shows the amount of green vegetation.'
'help': _('Normalized Difference Vegetation Index shows the amount of green vegetation.')
},
'ENDVI':{
'expr': '((N + G) - (2 * B)) / ((N + G) + (2 * B))',
'help': 'Enhanced Normalized Difference Vegetation Index is like NDVI, but uses Blue and Green bands instead of only Red to isolate plant health.'
'help': _('Enhanced Normalized Difference Vegetation Index is like NDVI, but uses Blue and Green bands instead of only Red to isolate plant health.')
},
'VARI': {
'expr': '(G - R) / (G + R - B)',
'help': 'Visual Atmospheric Resistance Index shows the areas of vegetation.',
'help': _('Visual Atmospheric Resistance Index shows the areas of vegetation.'),
'range': (-1, 1)
},
'EXG': {
'expr': '(2 * G) - (R + B)',
'help': 'Excess Green Index emphasizes the greenness of leafy crops such as potatoes.',
'help': _('Excess Green Index emphasizes the greenness of leafy crops such as potatoes.',)
},
'BAI': {
'expr': '1.0 / (((0.1 - R) ** 2) + ((0.06 - N) ** 2))',
'help': 'Burn Area Index hightlights burned land in the red to near-infrared spectrum.'
'help': _('Burn Area Index hightlights burned land in the red to near-infrared spectrum.')
},
'GLI': {
'expr': '((G * 2) - R - B) / ((G * 2) + R + B)',
'help': 'Green Leaf Index shows greens leaves and stems.',
'help': _('Green Leaf Index shows greens leaves and stems.'),
'range': (-1, 1)
},
'GNDVI':{
'expr': '(N - G) / (N + G)',
'help': 'Green Normalized Difference Vegetation Index is similar to NDVI, but measures the green spectrum instead of red.'
'help': _('Green Normalized Difference Vegetation Index is similar to NDVI, but measures the green spectrum instead of red.')
},
'GRVI':{
'expr': 'N / G',
'help': 'Green Ratio Vegetation Index is sensitive to photosynthetic rates in forests.'
'help': _('Green Ratio Vegetation Index is sensitive to photosynthetic rates in forests.')
},
'SAVI':{
'expr': '(1.5 * (N - R)) / (N + R + 0.5)',
'help': 'Soil Adjusted Vegetation Index is similar to NDVI but attempts to remove the effects of soil areas using an adjustment factor (0.5).'
'help': _('Soil Adjusted Vegetation Index is similar to NDVI but attempts to remove the effects of soil areas using an adjustment factor (0.5).')
},
'MNLI':{
'expr': '((N ** 2 - R) * 1.5) / (N ** 2 + R + 0.5)',
'help': 'Modified Non-Linear Index improves the Non-Linear Index algorithm to account for soil areas.'
'help': _('Modified Non-Linear Index improves the Non-Linear Index algorithm to account for soil areas.')
},
'MSR': {
'expr': '((N / R) - 1) / (sqrt(N / R) + 1)',
'help': 'Modified Simple Ratio is an improvement of the Simple Ratio (SR) index to be more sensitive to vegetation.'
'help': _('Modified Simple Ratio is an improvement of the Simple Ratio (SR) index to be more sensitive to vegetation.')
},
'RDVI': {
'expr': '(N - R) / sqrt(N + R)',
'help': 'Renormalized Difference Vegetation Index uses the difference between near-IR and red, plus NDVI to show areas of healthy vegetation.'
'help': _('Renormalized Difference Vegetation Index uses the difference between near-IR and red, plus NDVI to show areas of healthy vegetation.')
},
'TDVI': {
'expr': '1.5 * ((N - R) / sqrt(N ** 2 + R + 0.5))',
'help': 'Transformed Difference Vegetation Index highlights vegetation cover in urban environments.'
'help': _('Transformed Difference Vegetation Index highlights vegetation cover in urban environments.')
},
'OSAVI': {
'expr': '(N - R) / (N + R + 0.16)',
'help': 'Optimized Soil Adjusted Vegetation Index is based on SAVI, but tends to work better in areas with little vegetation where soil is visible.'
'help': _('Optimized Soil Adjusted Vegetation Index is based on SAVI, but tends to work better in areas with little vegetation where soil is visible.')
},
'LAI': {
'expr': '3.618 * (2.5 * (N - R) / (N + 6*R - 7.5*B + 1)) * 0.118',
'help': 'Leaf Area Index estimates foliage areas and predicts crop yields.',
'help': _('Leaf Area Index estimates foliage areas and predicts crop yields.'),
'range': (-1, 1)
},
'EVI': {
'expr': '2.5 * (N - R) / (N + 6*R - 7.5*B + 1)',
'help': 'Enhanced Vegetation Index is useful in areas where NDVI might saturate, by using blue wavelengths to correct soil signals.',
'help': _('Enhanced Vegetation Index is useful in areas where NDVI might saturate, by using blue wavelengths to correct soil signals.'),
'range': (-1, 1)
},

Wyświetl plik

@ -21,6 +21,7 @@ from nodeodm.models import ProcessingNode
from worker import tasks as worker_tasks
from .common import get_and_check_project
from app.security import path_traversal_check
from django.utils.translation import gettext_lazy as _
def flatten_files(request_files):
@ -175,7 +176,7 @@ class TaskViewSet(viewsets.ViewSet):
task.images_count = models.ImageUpload.objects.filter(task=task).count()
if task.images_count < 2:
raise exceptions.ValidationError(detail="You need to upload at least 2 images before commit")
raise exceptions.ValidationError(detail=_("You need to upload at least 2 images before commit"))
task.save()
worker_tasks.process_task.delay(task.id)
@ -197,7 +198,7 @@ class TaskViewSet(viewsets.ViewSet):
files = flatten_files(request.FILES)
if len(files) == 0:
raise exceptions.ValidationError(detail="No files uploaded")
raise exceptions.ValidationError(detail=_("No files uploaded"))
with transaction.atomic():
for image in files:
@ -220,7 +221,7 @@ class TaskViewSet(viewsets.ViewSet):
files = flatten_files(request.FILES)
if len(files) <= 1:
raise exceptions.ValidationError(detail="Cannot create task, you need at least 2 images")
raise exceptions.ValidationError(detail=_("Cannot create task, you need at least 2 images"))
with transaction.atomic():
task = models.Task.objects.create(project=project,
@ -325,10 +326,10 @@ class TaskDownloads(TaskNestedView):
try:
asset_path = task.get_asset_download_path(asset)
except FileNotFoundError:
raise exceptions.NotFound("Asset does not exist")
raise exceptions.NotFound(_("Asset does not exist"))
if not os.path.exists(asset_path):
raise exceptions.NotFound("Asset does not exist")
raise exceptions.NotFound(_("Asset does not exist"))
return download_file_response(request, asset_path, 'attachment')
@ -347,10 +348,10 @@ class TaskAssets(TaskNestedView):
try:
asset_path = path_traversal_check(task.assets_path(unsafe_asset_path), task.assets_path(""))
except SuspiciousFileOperation:
raise exceptions.NotFound("Asset does not exist")
raise exceptions.NotFound(_("Asset does not exist"))
if (not os.path.exists(asset_path)) or os.path.isdir(asset_path):
raise exceptions.NotFound("Asset does not exist")
raise exceptions.NotFound(_("Asset does not exist"))
return download_file_response(request, asset_path, 'inline')
@ -366,13 +367,13 @@ class TaskAssetsImport(APIView):
files = flatten_files(request.FILES)
import_url = request.data.get('url', None)
task_name = request.data.get('name', 'Imported Task')
task_name = request.data.get('name', _('Imported Task'))
if not import_url and len(files) != 1:
raise exceptions.ValidationError(detail="Cannot create task, you need to upload 1 file")
raise exceptions.ValidationError(detail=_("Cannot create task, you need to upload 1 file"))
if import_url and len(files) > 0:
raise exceptions.ValidationError(detail="Cannot create task, either specify a URL or upload 1 file.")
raise exceptions.ValidationError(detail=_("Cannot create task, either specify a URL or upload 1 file."))
with transaction.atomic():
task = models.Task.objects.create(project=project,

Wyświetl plik

@ -0,0 +1,29 @@
from django.core.management.commands.makemessages import Command as MMCommand
# https://medium.com/@hugosousa/hacking-djangos-makemessages-for-better-translations-matching-in-jsx-components-1174b57a13b1
class Command(MMCommand):
"""
This is a wrapper for the makemessages command and
it is used to force makemessages call xgettext with the language
provided as input
The solution is really hacky and takes advantage of the fact
that in makemessages TranslatableFile process()
the options in command.xgettext_options are appended to the end
of the xgettext command.
"""
def add_arguments(self, parser):
parser.add_argument(
'--language',
'-lang',
default='Python',
dest='language',
help='Language to be used by xgettext'
)
super(Command, self).add_arguments(parser)
def handle(self, *args, **options):
language = options.get('language')
self.xgettext_options.append('--language={lang}'.format(
lang=language
))
super(Command, self).handle(*args, **options)

Wyświetl plik

@ -8,7 +8,6 @@ from django.db.models import signals
from django.dispatch import receiver
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from webodm import settings
@ -71,7 +70,7 @@ class Setting(models.Model):
super(Setting, self).save(*args, **kwargs)
def __str__(self):
return gettext("Application")
return str(_("Application"))
class Meta:
verbose_name = _("Settings")

Wyświetl plik

@ -0,0 +1,84 @@
#!/usr/bin/python3
import argparse, os, urllib.request, ast, sys
from io import StringIO
parser = argparse.ArgumentParser(description='Extract ODM strings.')
parser.add_argument('input', type=str,
help='URL to ODM\'s config.py')
parser.add_argument('output', type=str,
help='Where to write resulting translation file')
args = parser.parse_args()
url = args.input
outfile = args.output
strings = []
print("Fetching %s ..." % url)
res = urllib.request.urlopen(url)
config_file = res.read().decode('utf-8')
# config_file = open("test.py").read()
options = {}
class ArgumentParserStub(argparse.ArgumentParser):
def add_argument(self, *args, **kwargs):
argparse.ArgumentParser.add_argument(self, *args, **kwargs)
options[args[0]] = {}
for name, value in kwargs.items():
options[args[0]][str(name)] = str(value)
def add_mutually_exclusive_group(self):
return ArgumentParserStub()
# Voodoo! :)
# ( parse AST, extract "def config()" function, set module to only
# contain that function, execute module in current scope,
# run config function)
root = ast.parse(config_file)
new_body = []
for stmt in root.body:
# Assignments
if hasattr(stmt, 'targets'):
new_body.append(stmt)
# Functions
elif hasattr(stmt, 'name'):
new_body.append(stmt)
root.body = new_body
exec(compile(root, filename="<ast>", mode="exec"))
# Misc variables needed to get config to run
__version__ = '?'
class context:
root_path = ''
num_cores = 4
class io:
def path_or_json_string_to_dict(s):
return s
def path_or_json_string(s):
return s
class log:
def ODM_ERROR(s):
pass
config(["--project-path", "/bogus", "name"], parser=ArgumentParserStub())
for opt in options:
h = options[opt].get('help')
if h:
h = h.replace("\n", "")
strings.append(h)
strings = list(set(strings))
print("Found %s ODM strings" % len(strings))
if len(strings) > 0:
with open(outfile, "w") as f:
f.write("// Auto-generated with extract_odm_strings.py, do not edit!\n\n")
for s in strings:
f.write("_(\"%s\");\n" % s.replace("\"", "\\\""))
print("Wrote %s" % outfile)
else:
print("No strings found")

Wyświetl plik

@ -1,9 +1,10 @@
{% extends "app/base.html" %}
{% load settings %}
{% load settings i18n %}
{% block page-wrapper %}
<div style="text-align: center;">
<h3>404 Page Not Found</h3>
<h5>Are you sure the address is correct?</h5>
<h3>{% trans '404 Page Not Found' %}</h3>
<h5>{% trans 'Are you sure the address is correct?' %}</h5>
<img src="/static/app/img/404.png" alt="404"/>
</div>

Wyświetl plik

@ -1,9 +1,9 @@
{% extends "app/base.html" %}
{% load settings %}
{% load settings i18n %}
{% block page-wrapper %}
<div style="text-align: center;">
<h3>500 Internal Server Error</h3>
<h5>Something happened. The server logs contain more information.</h5>
<h3>{% trans '500 Internal Server Error' %}</h3>
<h5>{% trans 'Something happened. The server logs contain more information.' %}</h5>
<img src="/static/app/img/500.png" alt="500"/>
</div>

Wyświetl plik

@ -84,12 +84,14 @@
<div id='stars2'></div>
<div id='stars3'></div>
{% load i18n %}
<div class="text-center about-text">
<h2 class="theme-secondary">WebODM {{ version }}</h2>
<div style="margin-bottom: 16px; margin-top: 16px;"><i class="fab fa-github"></i> <a target="_blank" href="https://github.com/OpenDroneMap/WebODM">https://github.com/OpenDroneMap/WebODM</a></div>
<div style="font-size: 90%; opacity: 0.9;" class="theme-secondary">This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See <a href="https://raw.githubusercontent.com/OpenDroneMap/WebODM/master/LICENSE.md">the GNU Affero General Public License</a> for more details.</div>
<div style="font-size: 90%; opacity: 0.9;" class="theme-secondary">{% blocktrans with link_start="<a href='https://raw.githubusercontent.com/OpenDroneMap/WebODM/master/LICENSE.md'>" link_end='</a>' %}This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See {{link_start}}the GNU Affero General Public License{{link_end}} for more details.{% endblocktrans %}</div>
</div>
{% endblock %}

Wyświetl plik

@ -43,7 +43,7 @@ def dashboard(request):
if Project.objects.count() == 0:
Project.objects.create(owner=request.user, name=_("First Project"))
return render(request, 'app/dashboard.html', {'title': 'Dashboard',
return render(request, 'app/dashboard.html', {'title': _('Dashboard'),
'no_processingnodes': no_processingnodes,
'no_tasks': no_tasks
})
@ -144,7 +144,7 @@ def welcome(request):
return render(request, 'app/welcome.html',
{
'title': 'Welcome',
'title': _('Welcome'),
'firstuserform': fuf
})

Wyświetl plik

@ -1,7 +1,8 @@
from __future__ import unicode_literals
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class NodeodmConfig(AppConfig):
name = 'nodeodm'
verbose_name = 'Node Management'
verbose_name = _('Node Management')

Wyświetl plik

@ -18,19 +18,23 @@ from datetime import timedelta
OFFLINE_MINUTES = 5 # Number of minutes a node hasn't been seen before it should be considered offline
# TODO
class ProcessingNode(models.Model):
hostname = models.CharField(max_length=255, help_text="Hostname or IP address where the node is located (can be an internal hostname as well). If you are using Docker, this is never 127.0.0.1 or localhost. Find the IP address of your host machine by running ifconfig on Linux or by checking your network settings.")
port = models.PositiveIntegerField(help_text="Port that connects to the node's API")
api_version = models.CharField(max_length=32, null=True, help_text="API version used by the node")
last_refreshed = models.DateTimeField(null=True, help_text="When was the information about this node last retrieved?")
queue_count = models.PositiveIntegerField(default=0, help_text="Number of tasks currently being processed by this node (as reported by the node itself)")
available_options = fields.JSONField(default=dict, help_text="Description of the options that can be used for processing")
token = models.CharField(max_length=1024, blank=True, default="", help_text="Token to use for authentication. If the node doesn't have authentication, you can leave this field blank.")
max_images = models.PositiveIntegerField(help_text="Maximum number of images accepted by this node.", blank=True, null=True)
engine_version = models.CharField(max_length=32, null=True, help_text="Engine version used by the node.")
label = models.CharField(max_length=255, default="", blank=True, help_text="Optional label for this node. When set, this label will be shown instead of the hostname:port name.")
engine = models.CharField(max_length=255, null=True, help_text="Engine used by the node.")
hostname = models.CharField(verbose_name=_("Hostname"), max_length=255, help_text=_("Hostname or IP address where the node is located (can be an internal hostname as well). If you are using Docker, this is never 127.0.0.1 or localhost. Find the IP address of your host machine by running ifconfig on Linux or by checking your network settings."))
port = models.PositiveIntegerField(verbose_name=_("Port"), help_text=_("Port that connects to the node's API"))
api_version = models.CharField(verbose_name=_("API Version"), max_length=32, null=True, help_text=_("API version used by the node"))
last_refreshed = models.DateTimeField(verbose_name=_("Last Refreshed"), null=True, help_text=_("When was the information about this node last retrieved?"))
queue_count = models.PositiveIntegerField(verbose_name=_("Queue Count"), default=0, help_text=_("Number of tasks currently being processed by this node (as reported by the node itself)"))
available_options = fields.JSONField(verbose_name=_("Available Options"), default=dict, help_text=_("Description of the options that can be used for processing"))
token = models.CharField(verbose_name=_("Token"), max_length=1024, blank=True, default="", help_text=_("Token to use for authentication. If the node doesn't have authentication, you can leave this field blank."))
max_images = models.PositiveIntegerField(verbose_name=_("Max Images"), help_text=_("Maximum number of images accepted by this node."), blank=True, null=True)
engine_version = models.CharField(verbose_name=_("Engine Version"), max_length=32, null=True, help_text=_("Engine version used by the node."))
label = models.CharField(verbose_name=_("Label"), max_length=255, default="", blank=True, help_text=_("Optional label for this node. When set, this label will be shown instead of the hostname:port name."))
engine = models.CharField(verbose_name=_("Engine"), max_length=255, null=True, help_text=_("Engine used by the node."))
class Meta:
verbose_name = _("Processing Node")
verbose_name_plural = _("Processing Nodes")
def __str__(self):
if self.label != "":

Wyświetl plik

@ -13,6 +13,7 @@ if [[ "$1" == "extract" ]]; then
mkdir -p locale
python3 app/scripts/extract_potree_strings.py app/static/app/js/vendor/potree/build/potree/resources/lang/en/translation.json app/static/app/js/translations/potree_autogenerated.js
python3 app/scripts/extract_odm_strings.py https://raw.githubusercontent.com/OpenDroneMap/ODM/master/opendm/config.py app/static/app/js/translations/odm_autogenerated.js
django-admin makemessages --keep-pot $locale_param --ignore=build --ignore=app/templates/app/registration/*
python manage.py makemessages_djangojs --keep-pot $locale_param -d djangojs --extension jsx --extension js --ignore=build --ignore app/static/app/js/vendor --ignore app/static/app/bundles --ignore node_modules --language Python